Next.jsでsymbol-sdk@3.1.0を使用してトランザクションを送信してみる

# はじめに

symbol-sdk@3を使用してWebアプリを作ろうと思ったのですが、VueやNuxtで試してみたもののうまくいきませんでした。

そこで、NEM/Symbolに詳しい[@Anthony14w](https://twitter.com/Anthony14w)に質問しました。

Next.jsでの使用ができるということで、実際に使用している例を教えてもらいました。

https://github.com/symbol/product/blob/dev/faucet/frontend/next.config.js

これを参考にして、手元でもやってみようと思います。

作る内容としてはとりあえず、ボタンを押すとトランザクションを送信するような簡単アプリを作成します。

# 手順

## インストール

まずは、Next.jsをインストールしていきます。

[Next.jsのインストールガイド](https://nextjs.org/docs/getting-started/installation)にもあるのですが、Node.jsがv18.17以上でなければなりません。

私の環境は、v18.19でした。

```shell
$ node -v
v18.19.0
```

それではNext.jsのプロジェクトを作成します。

```shell
$ npx create-next-app@latest
```

上記コマンドを入力すると、いろいろなオプションを対話形式で設定できます。シンプルな形で行きたいので悉くNoで行きます。

```shell
√ 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
```

空のプロジェクトが作成されたので、フォルダに移動します。

```shell
$ cd next-sample-simple
```

早速symbol-sdkをインストールします。

```shell
$ npm i symbol-sdk@3
```

symbol-sdkをブラウザ向けにビルドするためのパッケージをインストールします。

```shell
$ npm i symbol-crypto-wasm-web
```

そのほかに必要なパッケージをインストールします。

```shell
$ npm i axios
```

## ファイルの編集

`next.config.js`を以下のようにします。

```JavaScript: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`をプロジェクトルートに作成し、以下のように書きます。値はそれぞれ書き換えてください。

```text:.env.local
NEXT_PUBLIC_PRIVATE_KEY='<Your Private Key>'
NEXT_PUBLIC_NODE_URL='<Node/Rest API URL>'
```

`pages/index.js`を以下のようにします。

```jsx: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>
    </>
  )
}

```

## 起動

ファイルの編集が終わったので、実行してみます。

```shell
$ npm run dev
```

`http://localhost:3000`にブラウザでアクセスすると、以下のような画面になりました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/55795067-310f-e0cd-74bd-1820ebd4dfe8.png)

トランザクションを送信してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/dca8823e-962b-9a59-45e4-34ec06ac2f5b.png)

トランザクションの送信結果とトランザクションハッシュが表示されました。

トランザクションハッシュを元に、ステータスを確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/cf582b65-22fd-b1d4-3f5e-ae35085acaa7.png)

Unconfirmedです。しばらく待って、もう一度ボタンを押してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/719993ca-fcb3-358a-1905-cd1d9e4a5d65.png)

Confirmedになりました。

ブロックエクスプローラーでも見てみます。

https://testnet.symbol.fyi/transactions/5493BFC959560F7346D4501292F88A47406BBB7D06F68AFF784AD3CD3DE2A412

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/44ef9165-cf85-0047-db4a-8b23409923d8.png)


ちゃんと存在していることがわかります。成功です。

# TypeScriptでもやってみる

symbol-sdk@3.0.11からTypeScriptにも対応しているようなので、やってみます。

## インストール

まずはNext.jsのインストールから開始します。

```shell
$ npx create-next-app@latest
```

TypeScriptだけYesにします。

```shell
√ 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
```

空のプロジェクトが作成されたので、フォルダに移動します。

```shell
$ cd next-sample-simple-ts
```

symbol-sdkなどをインストールします。

```shell
$ 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`型にします。

```diff_tsx
-  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`と警告が出てしまいます。ほかのメソッドを探して使用します。

```diff_tsx
-  const keyPair = new facade.constructor.KeyPair(privateKey)
+  const keyPair = new symbolSdk.symbol.KeyPair(privateKey)
```

```diff_tsx
-  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.`という警告が出ます。

以下のように修正します。

```diff_tsx
    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.`

少し長くはなりますが、以下のように修正します。

```diff_tsx
+   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
    })
```

## 全体のコード

```tsx:pages/index.tsx
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>
    </>
  )
}
```

## 結果

問題なく動作することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/c8214be8-0f87-ea7a-157f-3d71c22fbbc4.png)

## 感想

`tsconfig.json`をいい感じに修正すれば、上記の警告のいくつかは出なくなると思います。でも、そのあたりそんなに詳しくないので、できれば変更をしたくないのが本音。今回書いたコードでは、無事にそれを修正せずに済みました。


# おわりに

Next.jsで使えることがわかりました。いままでVueばっかり使ってたので勉強しなきゃですね。