16
6

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

Last updated at Posted at 2023-12-19

はじめに

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を以下のようにします。

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

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

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

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にブラウザでアクセスすると、以下のような画面になりました。

image.png

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

image.png

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

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

image.png

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

image.png

Confirmedになりました。

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

image.png

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

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
    })

全体のコード

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

感想

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

おわりに

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

16
6
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
16
6