各種バージョン
- 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
を編集する。
/** @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
を追記する。
"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
ディレクトリを作成し、パッチファイルを作成する。
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 を使用するので秘密鍵は格納しません。
NEXT_PUBLIC_NODE_URL=http://sym-test-01.opening-line.jp:3000
サーバ側で動かす SDK コード
'use server'
を付けてサーバ側で動くようにし、以下の SymbolSDK を使用する 2 つのメソッドを記述します。
- ネットワークタイプの数値を文字に変換
- トランザクション生成
'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 を通じて親にネットワークタイプと公開鍵を渡します。
'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 で署名し、アナウンスまで行います。
細かいチェックはしてません。
'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つのページにします。
アカウントコンポーネントから貰った、ネットワークタイプと公開鍵をトランザクションコンポーネントに渡しています。
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
参考