23
14

Next.js で Blockchain Symbol のトランザクションを作って SSS Extension で署名する

Last updated at Posted at 2024-01-21

各種バージョン

  • node@20.16.0
  • next@14.2.6
  • typescript@5.3.3
  • symbol-sdk@3.1.0
  • sss-module@1.0.4

「最新がいい!」って事で、 Next.js v14 + SymbolSDK v3 で作ります!

これ動くの?

Next.js + SymbolSDK v3

クライアントサイドで SDK は、ほぼ動きません。サーバサイドであれば動くので、トランザクションの生成等 SDK を使用する箇所はサーバサイド(サーバアクション)として動かします。

Next.js + SSS Extension

window を使用するので、サーバサイドでは動きません。クライアントサイドで動かします。

SymbolSDK v3 + SSS Extension

SSS Extension は、SDK v2 のモノなので使えないと思いきや、案外 string で何とかなります。
署名後のトランザクションが SignedTransaction 型で返ってきますが、問題ありません。

Next.js + TypeScript 環境構築

モジュールのインストールと修正

SymbolSDK 等をインストール

npm i symbol-sdk symbol-crypto-wasm-web sss-module axios

next.config.mjsを編集する。

next.config.mjs
/** @type {import('next').NextConfig} */
import { accessSync, symlinkSync } from "fs";
import path from "path";
import webpack from "webpack";

const nextConfig = {
  reactStrictMode: false,
  webpack: (config, { isServer }) => {
    config.experiments = { asyncWebAssembly: true, topLevelAwait: true, layers: true };
    config.plugins.push(
      new webpack.NormalModuleReplacementPlugin(
        /symbol-crypto-wasm-node/,
        '../../../symbol-crypto-wasm-web/symbol_crypto_wasm.js'
      )
    );
    config.plugins.push(
      new (class {
        apply(compiler) {
          compiler.hooks.afterEmit.tapPromise('SymlinkWebpackPlugin', async compiler => {
            if (isServer) {
              const from = path.join(compiler.options.output.path, '../static');
              const to = path.join(compiler.options.output.path, 'static');

              try {
                accessSync(from);
                return;
              } catch (error) {
                if (error.code === 'ENOENT') {
                  // No link exists
                } else {
                  throw error;
                }
              }

              symlinkSync(to, from, 'junction');
              console.log(`created symlink ${from} -> ${to}`);
            }
          });
        }
      })()
    );
    return config;
  }
};

export default nextConfig;

Error: More than one instance of bitcore-lib found.対応

そのままでは、エラーが出るのでbitcore-libモジュールにパッチを当てる。

npm i -D patch-package

scripts の最後にpostinstallを追記する。

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "format": "prettier --write --ignore-path .gitignore './**/*.{js,jsx,ts,tsx,json,css}' && next lint --fix",
    "postinstall": "patch-package"
  },

トップレベルにpatchesディレクトリを作成し、パッチファイルを作成する。

patches/bitcore-lib+10.2.1.patch
diff --git a/node_modules/bitcore-lib/index.js b/node_modules/bitcore-lib/index.js
index 4cbe6bf..a86a4b3 100644
--- a/node_modules/bitcore-lib/index.js
+++ b/node_modules/bitcore-lib/index.js
@@ -5,12 +5,7 @@ var bitcore = module.exports;
 // module information
 bitcore.version = 'v' + require('./package.json').version;
 bitcore.versionGuard = function(version) {
-  if (version !== undefined) {
-    var message = 'More than one instance of bitcore-lib found. ' +
-      'Please make sure to require bitcore-lib and check that submodules do' +
-      ' not also include their own bitcore-lib dependency.';
-    throw new Error(message);
-  }
+  return;
 };
 bitcore.versionGuard(global._bitcore);
 global._bitcore = bitcore.version;

SymbolSDK インストール前に準備しておけばnpm install完了時に適応される…と、色々なところに書いてあったけど当環境では動かなかったので手動で実行する。

npm run postinstall

コード

トップレベルに.env.localファイルを作成し、秘密鍵やノード URL 置き場にします。
今回は、SSS Extension を使用するので秘密鍵は格納しません。

.env.local
NEXT_PUBLIC_NODE_URL=http://sym-test-01.opening-line.jp:3000

サーバ側で動かす SDK コード

'use server'を付けてサーバ側で動くようにし、以下の SymbolSDK を使用する 2 つのメソッドを記述します。

  • ネットワークタイプの数値を文字に変換
  • トランザクション生成
src/app/symbol.ts
'use server';
import symbolSdk from 'symbol-sdk';

export const getNetworkTypeName = (networkType: number): string => {
  return symbolSdk.symbol.NetworkType.valueToKey(networkType);
};

export const createTransaction = async (
  networkType: number,
  signerPublicKey: string,
  recipientAddress: string,
  amount: bigint,
  message: string
): Promise<string> => {
  // facade生成
  const networkTypeName = getNetworkTypeName(networkType);
  const facade = new symbolSdk.facade.SymbolFacade(networkTypeName.toLowerCase());

  // メッセージ作成
  const messageData = new Uint8Array([0x00, ...new TextEncoder().encode(message)]);

  // 転送トランザクション生成
  const transferTx = facade.transactionFactory.create({
    type: 'transfer_transaction_v1',
    signerPublicKey: signerPublicKey,
    deadline: facade.network.fromDatetime(new Date()).addHours(2).timestamp,
    recipientAddress: recipientAddress,
    mosaics: [{ mosaicId: 0x72c0212e67a08bcen, amount: BigInt(amount) }],
    message: messageData,
  });
  // 手数料計算
  transferTx.fee = new symbolSdk.symbol.Amount(BigInt(transferTx.size * 100));

  // トランザクションシリアライズ化
  const serializedTx = Buffer.from(transferTx.serialize()).toString('hex');

  return serializedTx;
};

アカウント情報を表示するコンポーネント

クライアント側で動くアカウント情報を表示するコンポーネントです。
SSS Extension から情報を取得して、画面に表示しています。
トランザクションを発行するコンポーネントのために props を通じて親にネットワークタイプと公開鍵を渡します。

src/app/accountComponent.tsx
'use client';
import { useEffect, useState } from 'react';
import {
  getActiveAddress,
  getActiveName,
  getActiveNetworkType,
  getActivePublicKey,
  requestSSS,
} from 'sss-module';
import { getNetworkTypeName } from './symbol';

type Props = {
  sssExtHandler: (networkType: number, accountPublicKey: string) => void;
};

export const AccountComponent = ({ sssExtHandler }: Props) => {
  const [networkTypeName, setNetworkTypeName] = useState('');
  const [accountName, setAccountName] = useState('');
  const [accountAddress, setAccountAddress] = useState('');
  const [accountPublicKey, setAccountPublicKey] = useState('');

  useEffect(() => {
    if (requestSSS()) {
      // SSSエクステンションが有効の時
      setNetworkTypeName(getNetworkTypeName(getActiveNetworkType()));
      setAccountName(getActiveName());
      setAccountAddress(getActiveAddress());
      setAccountPublicKey(getActivePublicKey());

      // 親にネットワークタイプと公開鍵を渡す
      sssExtHandler(getActiveNetworkType(), getActivePublicKey());
    }
  }, [sssExtHandler]);

  return (
    <>
      <div className="py-5">
        <p>NetworkType: {networkTypeName}</p>
        <p>Account Name: {accountName}</p>
        <p>Address: {accountAddress}</p>
        <p>PublicKey: {accountPublicKey}</p>
      </div>
    </>
  );
};

トランザクションの発行と署名するコンポーネント

こちらもクライアント側で動きます。
親から props でアカウントコンポーネントのネットワークタイプと公開鍵をもらい、チェックに使用しています。
最初に作成したサーバアクションクラスで生成したトランザクションを SSS Extension で署名し、アナウンスまで行います。
細かいチェックはしてません。

src/app/transactionComponent.tsx
'use client';
import axios from 'axios';
import { useState } from 'react';
import {
  getActiveNetworkType,
  getActivePublicKey,
  requestSign,
  setTransactionByPayload,
} from 'sss-module';
import { createTransaction } from './symbol';

type Props = {
  displayNetworkType: number;
  displayAccountPublicKey: string;
};

export const TransactionComponent = ({ displayNetworkType, displayAccountPublicKey }: Props) => {
  const [errorMsg, setErrorMsg] = useState('');

  const sendTransaction = async (data: FormData) => {
    // 入力チェック
    if (
      displayNetworkType !== getActiveNetworkType() ||
      displayAccountPublicKey !== getActivePublicKey()
    ) {
      setErrorMsg('SSSエクステンションの設定が変更されています。リロードしてください。');
      return;
    }
    if (data.get('address') === '') {
      setErrorMsg('送信先アドレスを入力してください。');
      return;
    }

    let amount = '0';
    if (data.get('amount') !== '') {
      amount = Number(data.get('amount')!.toString()).toFixed(6);
      amount = amount.replace('.', '');
    }

    // トランザクション生成
    const unsignedTx = await createTransaction(
      getActiveNetworkType(),
      getActivePublicKey(),
      data.get('address')!.toString(),
      BigInt(amount),
      data.get('message')!.toString()
    );

    // SSS Extension で署名
    setTransactionByPayload(unsignedTx);
    const signedTx = await requestSign();
    const jsonPayload = `{"payload":"${signedTx.payload}"}`;

    // アナウンス
    const node = axios.create({
      baseURL: process.env.NEXT_PUBLIC_NODE_URL,
      timeout: 3000,
      headers: { 'Content-Type': 'application/json' },
    });
    const response = await node.put('/transactions', jsonPayload).then((res) => res.data);
    console.log(response);
  };

  return (
    <>
      <div className="py-5">
        <form action={sendTransaction}>
          <input
            type="text"
            name="address"
            placeholder="address"
            className="text-black my-2 w-96"
          />
          <br />
          <input
            type="text"
            name="amount"
            placeholder="amount"
            className="text-black my-2 text-right"
          />{' '}
          XYM
          <br />
          <textarea
            name="message"
            placeholder="message"
            className="text-black my-2 w-96 h-28"
          ></textarea>
          <br />
          <button className="border-2">トランザクション送信</button>
          <p>{errorMsg}</p>
        </form>
      </div>
    </>
  );
};

コンポーネントを束ねるページ

上記2つのコンポーネントを束ねて1つのページにします。
アカウントコンポーネントから貰った、ネットワークタイプと公開鍵をトランザクションコンポーネントに渡しています。

src/app/page.tsx
import { AccountComponent } from './accountComponent';
import { TransactionComponent } from './transactionComponent';

let activeNetworkType = 0;
let activeAccountPublicKey = '';

export default function Home() {
  const sssExtHandler = async (networkType: number, accountPublicKey: string): Promise<void> => {
    'use server';
    activeNetworkType = networkType;
    activeAccountPublicKey = accountPublicKey;
  };

  return (
    <main className="min-h-screen items-center justify-between p-24">
      <div>
        <AccountComponent sssExtHandler={sssExtHandler} />
        <TransactionComponent
          displayNetworkType={activeNetworkType}
          displayAccountPublicKey={activeAccountPublicKey}
        />
      </div>
    </main>
  );
}

コードは Github に置いておきます。

実行

npm run dev

スクリーンショット 2024-01-20 000116.png

参考

23
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
14