28
20

More than 1 year has passed since last update.

ブロックチェーンでトランザクションを送信してみた

Last updated at Posted at 2022-11-02

はじめに

ブロックチェーンを使って自分のアカウントから他のアカウントに対してトークンを送信するトランザクションを送信してみました。
本記事はトランザクションの送信を行う過程で学んだ、トランザクションの送信に必要なデータ構造の理解を深めることを目的とした内容を記載しています。

背景

業務でブロックチェーンに携わることはあるものの他社が用意したAPIを利用することが多い環境にあります。
自身でトランザクションを送信するコア寄りの部分を経験してみたく、トークンの送信を行うトランザクションの送信に挑戦です。

目標

自分のアカウントから他のアカウントに対してトークンを送信し、送信が完了したことを外部サイトで確認出来る事をゴールとしました。

使用したブロックチェーン

Symbolブロックチェーンを使用しました。
選定理由は下記です。

  • 1000台を超えるREST APIノードが存在しており参照先ノードの選択肢が豊富であり特定のノードに依存しない
  • API経由で操作が可能であり既存のシステムから繋ぎ込みやすい
  • SDKと有志による開発に役立つ周辺が公開されている
  • 複数階層に渡るマルチシグがプロトコルレベルで組み込まれており組織での運用を考慮した場合に実用的
  • 日本のコミュニティが強く、質問に対して素早い回答が期待できる

環境/主なパッケージ

今回はNEMTUSから公開されている型情報付きのTypeScript版SDKを使用しました。

  • Node.js 17.9.0
  • TypeScrypt 4.8.4
  • ts-node 10.9.1
  • nemtus/symbol-sdk-typescript
  • nemtus/symbol-sdk-openapi-generator-typescript-axios

コードの流れ

ブロックチェーンでトランザクションを送信する場合、基本的な流れは
トランザクションの作成 → 署名 → ブロックチェーンのネットワークに送信
となります。

今回のトランザクションを送信するプログラムでは上記に加えて送信完了後に確認をするための情報を出力しました。

方針

なるべくコード量の少ないシンプルな形を目指しました。
トランザクションの送信を成功させることを目標として、動的に取得可能なプロパティ値も直値で設定しています。
トランザクションに乗せるメッセージはoptionalなものですが、定番として付与しています。

コード

全体

import { SymbolFacade } from "@nemtus/symbol-sdk-typescript/esm/facade/SymbolFacade";
import { PrivateKey } from "@nemtus/symbol-sdk-typescript/esm/CryptoTypes";
import { KeyPair } from "@nemtus/symbol-sdk-typescript/esm/symbol/KeyPair";
import { Signature } from "@nemtus/symbol-sdk-typescript/esm/symbol/models";
import {
   Configuration,
   TransactionRoutesApi,
} from "@nemtus/symbol-sdk-openapi-generator-typescript-axios";

(async () => {
   // ネットワークタイプを指定してSDKを初期化
   const facade = new SymbolFacade("testnet");

   // トランザクションを送信するアカウントの鍵ペアを取得
   const privateKeyString = "AFB4B406B5BF047BE2045E5AB9EA81A7971A854B402D93C10DA2ABA0467271A5";
   const privateKey = new PrivateKey(privateKeyString);
   const keyPair = new KeyPair(privateKey);
   const publicKeyString = keyPair.publicKey.toString();

   // ブロックチェーンの初期ブロックが生成されたときの時間(UnixTime秒)
   const EPOCH_ADJUSTMENT = 1637848847;

   // トランザクションの有効期限
   const now = Date.now();
   const deadline = BigInt(now - EPOCH_ADJUSTMENT * 1000 + 2 * 60 * 60 * 1000);

   // トランザクションの送信先アドレス
   const targetAddressString = "TDMYLKCTEVPSRPTG4UXW47IQPCYNLW2OVWZMLGY";

   // 平文メッセージ
   const messageString = "Hello Symbol!!";
   const messageNumberArray = [0, ...(new TextEncoder()).encode(messageString)];
   const messageUint8Array = new Uint8Array(messageNumberArray);

   // 送信するトークンのID
   const TOKEN_ID = BigInt("0x3A8416DB2D53B6C8");

   // トランザクションのデータ生成
   const transaction = facade.transactionFactory.create({
      type: "transfer_transaction",
      signerPublicKey: publicKeyString,
      deadline,
      recipientAddress: targetAddressString,
      mosaics: [{ mosaicId: TOKEN_ID, amount: 1000000n }],
      message: messageUint8Array
   });

   // 手数料設定
   const feeMultiplier = 100;
   (transaction as any).fee.value = BigInt(
           (transaction as any).size * feeMultiplier
   );

   // 署名
   const signature = facade.signTransaction(keyPair, transaction);
   (transaction as any).signature = new Signature(signature.bytes);

   // トランザクションのハッシュを計算
   const hash = facade.hashTransaction(transaction);
   console.log(hash.toString());
   console.log(`https://testnet.symbol.fyi/transactions/${hash.toString()}`);

   // トランザクション送信時のpayload
   const transactionPayload = (
           facade.transactionFactory.constructor as any
   ).attachSignature(transaction, signature);

   // トランザクションを送信する際の設定情報
   const NODE_URL = "https://sym-test-02.opening-line.jp:3001";
   const configurationParameters = {
      basePath: NODE_URL,
   };
   const configuration = new Configuration(configurationParameters);

   // トランザクションのアナウンス実行(送信)
   try {
      const transactionRoutesApi = new TransactionRoutesApi(configuration);
      console.log(transactionPayload);
      const response = await transactionRoutesApi.announceTransaction({
         transactionPayload,
      });
      console.log(response.data);
      console.log(`${NODE_URL}/transactionStatus/${hash.toString()}`)
   } catch (err) {
      console.error(err);
   }
})();

各部

SDKの初期化

ネットワークタイプを指定してSDKを初期化します。指定の仕方は以下。

  • テストネット: "testnet"
  • メインネット: "mainnet"
// ネットワークタイプを指定してSDKを初期化
const facade = new SymbolFacade("testnet");

トランザクション送信元アカウントの鍵ペア情報の導出

署名に使うトランザクション送信元アカウントの鍵ペア情報を秘密鍵から導出します。

const privateKeyString = "AFB4B406B5BF047BE2045E5AB9EA81A7971A854B402D93C10DA2ABA0467271A5";
const privateKey = new PrivateKey(privateKeyString);
const keyPair = new KeyPair(privateKey);
const publicKeyString = keyPair.publicKey.toString();

トランザクションの有効期限の設定

トランザクションの有効期限を設定します。ここで設定した日時が到来してもブロックチェーンに承認されなかった場合そのトランザクションは破棄されます。
今回は2時間で設定します。

入れるべき値は初期ブロックからの経過時刻をミリ秒単位で指定です。

大変ややこしいですが以下のように入れるべき値を算出しています。

  1. Date.now() で現在のUnixTimestamp(単位:ミリ秒)を取得
  2. UnixTimestampをEPOCH_ADJUSTMENT(単位:秒)分補正する
  3. 2時間をミリ秒に変換

EPOCH_ADJUSTMENT ・・・
Symbolブロックチェーン独自の時刻的な概念で、初期ブロック目が誕生した時点を基準(0)として1秒ごとに1ずつカウントアップする値(UnixTimestampみたいなやつ)

  • テストネット: 1637848847 (2022/10/24現在)
  • メインネット: 1615853185

EPOCH_ADJUSTMENTの値は上記のとおり1ブロック目の誕生日時によって変動します。
テストネットは稀にリセットされて1ブロック目から再生成されることがあるため、実装時にはノードのエンドポイント/network/propertiesから取得 or 確認するか
継続的にメンテされている下記記事を確認しましょう。

ノードから取得する場合は末尾にsが付与されているため除外処理が必要になります。

// ブロックチェーンの初期ブロックが生成されたときの時間(UnixTime秒)
const EPOCH_ADJUSTMENT = 1637848847;

// トランザクションの有効期限
const now = Date.now();
const deadline = BigInt(now - EPOCH_ADJUSTMENT * 1000 + 2 * 60 * 60 * 1000);

送信先アドレスの設定

トランザクションの送信先アドレスを設定します。
今回はテストネット用のトークンの払出し元であるFaucetのアドレスを設定しています。

アドレス先頭1文字がTの場合はテストネット、Nの場合はメインネット用アドレスとして見分けることが可能です。

// トランザクションの送信先アドレス
const targetAddressString = "TDMYLKCTEVPSRPTG4UXW47IQPCYNLW2OVWZMLGY";

トランザクションメッセージの設定

トランザクションには1024Byteのデータの書き込み可能な領域があり、Symbolブロックチェーンではメッセージ領域と呼ばれています。
現在公開されている多くのSymbol Walletアプリでは自由記述のメッセージ領域として書き込み/読み出し可能になっています。
送信元または送信先の秘密鍵を知り得ないと復号化できない暗号化をかけて送信することも可能です。
今回は平文で送信しています。

messageUint8Arrayにした文字列を後述するトランザクションのデータ作成時のobjectのkeyがmessageのvalueとして設定することでトランザクションにメッセージを設定することが可能です。
Explorerで文字化けせずにメッセージ内容を表示させるには先頭に0を付与する必要があるため本記事記載のコードでは0を付与しています。
Explorerでの文字化けを気にせず1024Byteフルに使いたいという場合は0を除いた送信が可能です。

// 平文メッセージ
const messageString = "Hello Symbol!!";
const messageNumberArray = [0, ...(new TextEncoder()).encode(messageString)];
const messageUint8Array = new Uint8Array(messageNumberArray);

送信するトークンIDの設定

送信するトークンのIDを設定します。今回はSymbolブロックチェーンのネイティブトークンであるsymbol.xymを設定しています。
symbol.xymのトークンIDはノードのエンドポイント/network/propertiesで取得できるcurrencyMosaicIdプロパティで確認できます。
先頭の0xを除いた4文字間隔で'で区切られているため、動的に取得する場合はParseを忘れずに。

  • テストネット: 0x6BED913FA20223F8
  • メインネット: 0x3A8416DB2D53B6C8
// 送信するトークンID
const TOKEN_ID = BigInt("0x3A8416DB2D53B6C8");

トランザクションのデータ作成

送信するトランザクションのデータを作成します。設定している各値について見ていきます。

項目 内容 必須
type 今回はトランザクションの種類をシンプルな転送トランザクションとして設定しています。
Symbolブロックチェーンは20種類を超える種類のトランザクションタイプが定義されており、トークンの発行、アドレスやトークンにエイリアスの設定、複数のトランザクションを集約したトランザクションなど様々なトランザクションの発行が可能です。
signerPublicKey 前項で導出したトランザクションを送信するアカウントの鍵ペアの公開鍵を設定しています。設定必須項目です。
deadline 前項で導出したトランザクションの有効期限を設定しています
recipientAddress 送信先のアドレスを設定しています。
mosaics 送信するトークンのIDと数量を設定しています。
データは整数値として扱われるため可分性(小数点第何位まで数量の単位とするか)を考慮した値を設定する必要があります。
symbol.xymは可分性6のため1000000で数量1となります。
-
message 送信するメッセージを設定しています。 -
// トランザクションのデータ生成
const transaction = facade.transactionFactory.create({
    type: "transfer_transaction",
    signerPublicKey: publicKeyString,
    deadline,
    recipientAddress: targetAddressString,
    mosaics: [{ mosaicId: TOKEN_ID, amount: 1000000n }],
    message: messageUint8Array
});

手数料の設定

トランザクション送信時にかかる最大手数料を手数料係数を用いて設定しています。ここで設定された手数料を超えた手数料は発生しません。
実効手数料はトランザクションのサイズや接続先ノードの設定値によって変動します。
詳細は こちら をご確認ください。

 // 手数料設定
 const feeMultiplier = 100;
 (transaction as any).fee.value = BigInt(
     (transaction as any).size * feeMultiplier
 );

署名

前項で取得した鍵ペアでトランザクションに署名を行っています。

// 署名
const signature = facade.signTransaction(keyPair, transaction);
(transaction as any).signature = new Signature(signature.bytes);

トランザクションハッシュ値の計算

前項で作成したトランザクションのトランザクションハッシュ値を計算しています。
ここで算出したハッシュ値はトランザクション送信に使用しませんが送信後の確認で使用します。

 // トランザクションのハッシュを計算
 const hash = facade.hashTransaction(transaction);
 console.log(hash.toString());
 console.log(`https://testnet.symbol.fyi/transactions/${hash.toString()}`);

payloadの取得

トランザクション送信時のpayloadを取得しています。このpayloadを後項でノードに通知します。

// トランザクション送信時のpayload
const transactionPayload = (
  facade.transactionFactory.constructor as any
).attachSignature(transaction, signature);

トランザクション送信時の設定

トランザクション送信時に必要な情報を設定します。apiKeyやusername、accessTokenといった認証情報もoptionalであるようですが今回は特に認証は無いため接続先ノードのURLのみ設定します。

 // 接続先ノード情報
 const NODE_URL = "https://sym-test-02.opening-line.jp:3001";
 const configurationParameters = {
     basePath: NODE_URL,
 };
 const configuration = new Configuration(configurationParameters);

トランザクションの送信

最後に、作成したトランザクションをブロックチェーンに送信します。

// トランザクションのアナウンス実行(送信)
try {
  const transactionRoutesApi = new TransactionRoutesApi(configuration);
  console.log(transactionPayload);
  const response = await transactionRoutesApi.announceTransaction({
      transactionPayload,
  });
  console.log(response.data);
  console.log(`${NODE_URL}/transactionStatus/${hash.toString()}`)
} catch (err) {
  console.error(err);
}

注意点

トランザクション送信時のレスポンスメッセージで下記が返ってくる = 送信成功 ではない点にご注意下さい。
{ message: 'packet 9 was pushed to the network via /transactions' }
上記メッセージはアナウンスしたトランザクションのデータは正しいフォーマットとしてノードに受理された程度の確認にしかならず、送信先アドレスが存在しない、送信トークンの残高不足、整合性の取れたデータであるかなど様々な原因によって、ブロックチェーンに承認されるかどうかの保証は無く、送信失敗時にもレスポンスで返ってきます。
正しく送信できたかの確認は Explorer、なるべくノードを見に行くようにしましょう。

トランザクションの送信結果の確認

ノードでの確認

前項にある console.log(${NODE_URL}/transactionStatus/${hash.toString()}) でconsoleに出力されたURLにアクセスして送信結果を確認します。

表示例1 未承認

{"group":"unconfirmed","code":"Success","hash":"21E1AF8649E6A23FFF6C8C84371832BEE70E9C34F04A320F50E54E07BB843270","deadline":"29000398773","height":"0"}

送信直後はブロックチェーンに承認されておらず、groupunconfirmedになっています。
前項で設定したトランザクションの有効期限であるdeadlineも表示されていますね。この期限を迎えるまでに承認されなかった場合トランザクションが棄却されます。
未承認状態のためブロック高を表すheightはブロック高いくつに取り込まれるか未確定のため0となっています。

表示例2 承認

{"group":"confirmed","code":"Success","hash":"6C749FB3B485BBEFCAEAF87C890C6147ACDB809E72CD006C4B096BE4B132888B","deadline":"29000922855","height":"803674"}

送信後、トランザクションがブロックチェーンに承認されたためgroupconfirmedになっています。
ブロックに取り込まれてているためブロック高が確定しており、heightにトランザクションが取り込まれたブロック高が出力されています。

表示例3 失敗

{"hash":"22CE975B6EF156C27195A8D4DD30EAD990AD400E01917F57E13480C1DAE9A7B4","code":"Failure_Core_Insufficient_Balance","deadline":"29001116654","group":"failed"}

送信元アカウントの残高不足により送信失敗した場合の例です。
codeに残高不足を表すFailure_Core_Insufficient_Balanceが出力されています。

codeの種類についてはこちらをご覧ください。
https://github.com/symbol/symbol/blob/dev/client/rest/src/catapult-sdk/model/status.js

日本語で補足してくださっている記事

外部サイトでの確認

下記Explorerにアクセスして確認します。
テストネット: https://testnet.symbol.fyi/
メインネット: https://symbol.fyi/

本記事に記載のコードを実行するとconsoleにトランザクション内容を表示するExplorer内のURLが出力されます。
送信したトランザクションがブロックチェーンに承認されるまでdoes not existな旨が表示されるため、Explorerでトランザクション内容を確認する場合はトランザクションが承認される数秒〜30秒程度待ってアクセスしましょう。

まとめ

Symbolブロックチェーンを使用してトークンを送るというシンプルなトランザクションを送信しました。
ブロックチェーンと聞くと扱いが難しいと身構えてしまうかもしれませんが、SDKを利用することでデータ構造の理解はともかくシンプルなトランザクションの送信はそれほど難しくないものだと思いました。
選定理由にも書きましたがAPI経由で操作が可能な事、日本のコミュニティが強く、質問に対して素早い回答が期待できるSymbolはブロックチェーン入門のチェーンとして適当であると思いました。

参考

28
20
1

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
28
20