はじめに
symbol-sdk@3を使用してWebアプリを作ろうと思ったのですが、VueやNuxtで試してみたもののうまくいきませんでした。
そこで、NEM/Symbolに詳しい@Anthony14wに質問しました。
Next.jsでの使用ができるということで、実際に使用している例を教えてもらいました。
これを参考にして、手元でもやってみようと思います。
作る内容としてはとりあえず、ボタンを押すとトランザクションを送信するような簡単アプリを作成します。
手順
インストール
まずは、Next.jsをインストールしていきます。
Next.jsのインストールガイドにもあるのですが、Node.jsがv18.17以上でなければなりません。
私の環境は、v18.19でした。
$ node -v
v18.19.0
それではNext.jsのプロジェクトを作成します。
$ npx create-next-app@latest
上記コマンドを入力すると、いろいろなオプションを対話形式で設定できます。シンプルな形で行きたいので悉くNoで行きます。
√ What is your project named? ... next-sample-simple
√ Would you like to use TypeScript? ... No
√ Would you like to use ESLint? ... No
√ Would you like to use Tailwind CSS? ... No
√ Would you like to use `src/` directory? ... No
√ Would you like to use App Router? (recommended) ... No
√ Would you like to customize the default import alias (@/*)? ... No
Creating a new Next.js app in
Using npm.
...
Success! Created next-sample at
空のプロジェクトが作成されたので、フォルダに移動します。
$ cd next-sample-simple
早速symbol-sdkをインストールします。
$ npm i symbol-sdk@3
symbol-sdkをブラウザ向けにビルドするためのパッケージをインストールします。
$ npm i symbol-crypto-wasm-web
そのほかに必要なパッケージをインストールします。
$ npm i axios
ファイルの編集
next.config.js
を以下のようにします。
/** @type {import('next').NextConfig} */
const webpack = require('webpack')
const nextConfig = {
reactStrictMode: true,
webpack: (config, { isServer }) => {
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/symbol-crypto-wasm-node/,
'../../../symbol-crypto-wasm-web/symbol_crypto_wasm.js'
));
config.experiments = { asyncWebAssembly: true, topLevelAwait: true, layers: true };
return config;
}
}
module.exports = nextConfig
.env.local
をプロジェクトルートに作成し、以下のように書きます。値はそれぞれ書き換えてください。
NEXT_PUBLIC_PRIVATE_KEY='<Your Private Key>'
NEXT_PUBLIC_NODE_URL='<Node/Rest API URL>'
pages/index.js
を以下のようにします。
import Head from 'next/head'
import Image from 'next/image'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import symbolSdk from 'symbol-sdk'
import axios from 'axios'
import { useState } from 'react'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
const PRIVATE_KEY = process.env.NEXT_PUBLIC_PRIVATE_KEY
const NODE_URL = process.env.NEXT_PUBLIC_NODE_URL
const [sendMessage, setSendMessage] = useState('ここに送信結果が表示されます')
const [statusMessage, setStatusMessage] = useState('ここに状況が表示されます')
const [hash, setHash] = useState('')
const node = axios.create({
baseURL: NODE_URL,
timeout: 1000,
headers: { 'Content-Type': 'application/json' }
})
const network = symbolSdk.symbol.Network.TESTNET
const facade = new symbolSdk.facade.SymbolFacade(network.name)
const privateKey = new symbolSdk.PrivateKey(PRIVATE_KEY)
const keyPair = new facade.constructor.KeyPair(privateKey)
const textEncoder = new TextEncoder()
const handleSend = async () => {
setSendMessage('送信中…')
setHash('')
const deadline = network.fromDatetime(new Date(Date.now() + 7200000)).timestamp
const transaction = facade.transactionFactory.create({
type: 'transfer_transaction_v1',
signerPublicKey: keyPair.publicKey.toString(),
fee: 1000000n,
deadline,
recipientAddress: 'TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY',
mosaics: [
{ mosaicId: 0x72C0212E67A08BCEn, amount: 1000000n },
],
message: new Uint8Array([0x00, ...textEncoder.encode('Hello, World!')])
})
const signature = facade.signTransaction(keyPair, transaction)
const jsonPayload = facade.transactionFactory.constructor.attachSignature(transaction, signature)
const hash = facade.hashTransaction(transaction).toString()
const sendResult = await node.put("/transactions", jsonPayload).then((res) => res.data);
setSendMessage(JSON.stringify(sendResult))
setHash(hash)
}
const handleStatus = async () => {
setStatusMessage('取得中…')
const statusResult = await node
.get("/transactionStatus/" + hash)
.then((res) => res.data)
.catch((e) => e.message)
setStatusMessage(JSON.stringify(statusResult))
}
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<div>
<h1>Symbol SDK v3 sample</h1>
<div className={styles.card}>
<div>
<button onClick={handleSend}>トランザクション送信</button>
</div>
<div>{sendMessage}</div>
<div>{hash}</div>
</div>
<div className={styles.card}>
<div>
<button onClick={handleStatus}>トランザクション確認</button>
</div>
<div>{statusMessage}</div>
</div>
</div>
</main>
</>
)
}
起動
ファイルの編集が終わったので、実行してみます。
$ npm run dev
http://localhost:3000
にブラウザでアクセスすると、以下のような画面になりました。
トランザクションを送信してみます。
トランザクションの送信結果とトランザクションハッシュが表示されました。
トランザクションハッシュを元に、ステータスを確認してみます。
Unconfirmedです。しばらく待って、もう一度ボタンを押してみます。
Confirmedになりました。
ブロックエクスプローラーでも見てみます。
ちゃんと存在していることがわかります。成功です。
TypeScriptでもやってみる
symbol-sdk@3.0.11からTypeScriptにも対応しているようなので、やってみます。
インストール
まずはNext.jsのインストールから開始します。
$ npx create-next-app@latest
TypeScriptだけYesにします。
√ What is your project named? ... next-sample-simple-ts
√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... No
√ Would you like to use Tailwind CSS? ... No
√ Would you like to use `src/` directory? ... No
√ Would you like to use App Router? (recommended) ... No
√ Would you like to customize the default import alias (@/*)? ... No
Creating a new Next.js app in
Using npm.
...
Success! Created next-sample at
空のプロジェクトが作成されたので、フォルダに移動します。
$ cd next-sample-simple-ts
symbol-sdkなどをインストールします。
$ npm i symbol-sdk@3 symbol-crypto-wasm-web axios
ファイルの編集
next.config.js
があるので、それをJavaScriptの時と同じように編集します。
.env.local
も、同じように作成しておきます。
page/index.js
を同じように編集しますが、このままではうまく動かないので、次項で変更点をあげていきます。
process.env
process.env
で始まる環境変数は、string | undefined
型になるため、強制的にstring
型にします。
- const PRIVATE_KEY = process.env.NEXT_PUBLIC_PRIVATE_KEY
- const NODE_URL = process.env.NEXT_PUBLIC_NODE_URL
+ const PRIVATE_KEY = process.env.NEXT_PUBLIC_PRIVATE_KEY as string
+ const NODE_URL = process.env.NEXT_PUBLIC_NODE_URL as string
constructorプロパティ使用の修正
constructor
のプロパティにおいて、TS2339: Property KeyPair does not exist on type Function
と警告が出てしまいます。ほかのメソッドを探して使用します。
- const keyPair = new facade.constructor.KeyPair(privateKey)
+ const keyPair = new symbolSdk.symbol.KeyPair(privateKey)
- const jsonPayload = facade.transactionFactory.constructor.attachSignature(transaction, signature)
+ const jsonPayload = symbolSdk.symbol.SymbolTransactionFactory.attachSignature(transaction, signature)
BigIntリテラルの修正
整数リテラルの末尾にn
を付ける方法が使えません。
TS2737: BigInt literals are not available when targeting lower than ES2020.
という警告が出ます。
以下のように修正します。
const transaction = facade.transactionFactory.create({
type: 'transfer_transaction_v1',
signerPublicKey: keyPair.publicKey.toString(),
- fee: 1000000n,
+ fee: BigInt('1000000'),
deadline,
recipientAddress: 'TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY',
mosaics: [
- { mosaicId: 0x72C0212E67A08BCEn, amount: 1000000n },
+ { mosaicId: BigInt('0x72C0212E67A08BCE'), amount: BigInt('1000000') },
],
Uint8Arrayのスプレッド演算子を修正
...
を使用して、Uint8Array
を分割代入している箇所が以下のような警告になります。
TS2802: Type Uint8Array can only be iterated through when using the --downlevelIteration flag or with a --target of es2015 or higher.
少し長くはなりますが、以下のように修正します。
+ const messageBytes = textEncoder.encode('Hello, World!')
+ const message = new Uint8Array(messageBytes.length + 1)
+ message.set(new Uint8Array([0]), 0)
+ message.set(messageBytes, 1)
const transaction = facade.transactionFactory.create({
...中略
- message: new Uint8Array([0x00, ...textEncoder.encode('Hello, World!')])
+ message
})
全体のコード
import Head from 'next/head'
import Image from 'next/image'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import symbolSdk from 'symbol-sdk'
import axios from 'axios'
import { useState } from 'react'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
const PRIVATE_KEY = process.env.NEXT_PUBLIC_PRIVATE_KEY as string
const NODE_URL = process.env.NEXT_PUBLIC_NODE_URL as string
const [sendMessage, setSendMessage] = useState('ここに送信結果が表示されます')
const [statusMessage, setStatusMessage] = useState('ここに状況が表示されます')
const [hash, setHash] = useState('')
const node = axios.create({
baseURL: NODE_URL,
timeout: 1000,
headers: { 'Content-Type': 'application/json' }
})
const network = symbolSdk.symbol.Network.TESTNET
const facade = new symbolSdk.facade.SymbolFacade(network.name)
const privateKey = new symbolSdk.PrivateKey(PRIVATE_KEY)
const keyPair = new symbolSdk.symbol.KeyPair(privateKey)
const textEncoder = new TextEncoder()
const handleSend = async () => {
setSendMessage('送信中…')
setHash('')
const deadline = network.fromDatetime(new Date(Date.now() + 7200000)).timestamp
const messageBytes = textEncoder.encode('Hello, World!')
const message = new Uint8Array(messageBytes.length + 1)
message.set(new Uint8Array([0]), 0)
message.set(messageBytes, 1)
const transaction = facade.transactionFactory.create({
type: 'transfer_transaction_v1',
signerPublicKey: keyPair.publicKey.toString(),
fee: BigInt('1000000'),
deadline,
recipientAddress: 'TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY',
mosaics: [
{ mosaicId: BigInt('0x72C0212E67A08BCE'), amount: BigInt('1000000') },
],
message
})
const signature = facade.signTransaction(keyPair, transaction)
const jsonPayload = symbolSdk.symbol.SymbolTransactionFactory.attachSignature(transaction, signature)
const hash = facade.hashTransaction(transaction).toString()
const sendResult = await node.put("/transactions", jsonPayload).then((res) => res.data);
setSendMessage(JSON.stringify(sendResult))
setHash(hash)
}
const handleStatus = async () => {
setStatusMessage('取得中…')
const statusResult = await node
.get("/transactionStatus/" + hash)
.then((res) => res.data)
.catch((e) => e.message)
setStatusMessage(JSON.stringify(statusResult))
}
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<div>
<h1>Symbol SDK v3 sample</h1>
<div className={styles.card}>
<div>
<button onClick={handleSend}>トランザクション送信</button>
</div>
<div>{sendMessage}</div>
<div>{hash}</div>
</div>
<div className={styles.card}>
<div>
<button onClick={handleStatus}>トランザクション確認</button>
</div>
<div>{statusMessage}</div>
</div>
</div>
</main>
</>
)
}
結果
問題なく動作することができました。
感想
tsconfig.json
をいい感じに修正すれば、上記の警告のいくつかは出なくなると思います。でも、そのあたりそんなに詳しくないので、できれば変更をしたくないのが本音。今回書いたコードでは、無事にそれを修正せずに済みました。
おわりに
Next.jsで使えることがわかりました。いままでVueばっかり使ってたので勉強しなきゃですね。