SolanaのWeb3.jsサンプル実装

ライブラリの Solana Web3 JavaScript API があるため、実装自体はかんたんにできる。

補足

  • devnetやtestnetを使えば、Solanaノードサーバーをローカル起動(% solana-test-validator)する必要なし
  • JSのため、Nodeを使ってターミナルから「% node [JSファイル]」で実行する。Nodeが動けば場所はどこでもよい
  • 今回、結果のログは「console.log()」でターミナル上に出力
  • 今回はNodeを使うが、JSが動けばCodePenやChrome検証ツールなどでも実行可能。その場合は、scriptタグでSolanaのweb3.jsを読み込む形となる
  • 今回のサンプル実装はSolana公式サイトのほぼコピペ

準備

  1. Nodeをインストールする
  2. Install をもとに、Yarnまたはnpmで「@solana/web3.js」をインストールする

solanaWeb3.jsのサンプル出力

Usage をサンプル実装。

% vi solanaWeb3.js
(viで以下を記述する)

const solanaWeb3 = require('@solana/web3.js');
console.log(solanaWeb3);

Nodeを起動すると、ターミナルにSolanaブロックチェーン情報が表示される。

% node solanaWeb3.js
{
  Account: [class Account],
  Authorized: [class Authorized],
  BLOCKHASH_CACHE_TIMEOUT_MS: 30000,
  BPF_LOADER_DEPRECATED_PROGRAM_ID: PublicKey {
    _bn: <BN: *****************************>
  },
  BPF_LOADER_PROGRAM_ID: PublicKey {
    _bn: <BN: *****************************>>
  },
  BpfLoader: [class BpfLoader],
  Connection: [class Connection],
  Ed25519Program: [class Ed25519Program] {
    programId: PublicKey {
      _bn: <BN: *****************************>>
    }
  },
  Enum: [class Enum extends Struct],
  EpochSchedule: [class EpochSchedule],
  FeeCalculatorLayout: NearUInt64 { span: 8, property: 'lamportsPerSignature' },
  Keypair: [class Keypair],
  LAMPORTS_PER_SOL: 1000000000,
  Loader: [class Loader] { chunkSize: 932 },
  Lockup: [class Lockup] {
    default: Lockup { unixTimestamp: 0, epoch: 0, custodian: [PublicKey] }
  },
  MAX_SEED_LENGTH: 32,

    ・・・省略

Keypair作成の場合

Connecting to a Wallet のKeypair作成であれば、サンプルどおりに記述するだけで動く。

% vi keypair.js
(viで以下を記述する)

const {Keypair} = require("@solana/web3.js");
let keypair = Keypair.generate();
console.log(keypair);

Nodeを起動すると、Keypairがランダム作成される。

% node keypair.js
Keypair {
  _keypair: {
    publicKey: Uint8Array(32) [
      169, 183,  61, 209,  30, 177,  24,  56,
       61, 228, 138,  84, 130, 220,  57, 157,
       87, 120, 159, 120, 143, 241, 166,  19,
      139, 230,  72, 108, 110,  86, 229, 102
    ],
    secretKey: Uint8Array(64) [
       35,  78, 180, 190, 104,  77,   3, 174, 245,  37, 190,
      180,  63, 108, 221, 244,  68,   4,  99,  15, 138, 130,
       75, 176, 228,  85, 115, 213,  29, 200, 170, 151, 169,
      183,  61, 209,  30, 177,  24,  56,  61, 228, 138,  84,
      130, 220,  57, 157,  87, 120, 159, 120, 143, 241, 166,
       19, 139, 230,  72, 108, 110,  86, 229, 102
    ]
  }
}

トランザクションの実装

送金する場合の実装。
Interacting with Custom Programs のほぼコピペ(最後に載っている完成形のソース)で、以下だけ追記。

  • awaitエラー対策としてfunction化
  • console.log出力(特にトランザクション結果のSinatureが取得できないと結果がわからないため)
  • testnetで実際にトランザクションを走らせるため、結果までに時間がかかるため、数分は待つ必要あり
% vi transactions.js
(viで以下を記述する)

const {struct,u32, ns64} = require("@solana/buffer-layout");
const {Buffer} = require('buffer');
const web3 = require("@solana/web3.js");
const cluster = 'testnet';

async function main() {
  let keypair = web3.Keypair.generate();
  let payer = web3.Keypair.generate();

  let connection = new web3.Connection(web3.clusterApiUrl(cluster));

  let airdropSignature = await connection.requestAirdrop(
    payer.publicKey,
    web3.LAMPORTS_PER_SOL,
  );

  await connection.confirmTransaction(airdropSignature);

  let allocateTransaction = new web3.Transaction({
    feePayer: payer.publicKey
  });
  let keys = [{pubkey: keypair.publicKey, isSigner: true, isWritable: true}];
  let params = { space: 100 };

  let allocateStruct = {
    index: 8,
    layout: struct([
      u32('instruction'),
      ns64('space'),
    ])
  };

  let data = Buffer.alloc(allocateStruct.layout.span);
  let layoutFields = Object.assign({instruction: allocateStruct.index}, params);
  allocateStruct.layout.encode(layoutFields, data);

  allocateTransaction.add(new web3.TransactionInstruction({
    keys,
    programId: web3.SystemProgram.programId,
    data,
  }));

  let signature = await web3.sendAndConfirmTransaction(connection, allocateTransaction, [payer, keypair]);

  console.log('Cluster -> ', cluster);
  console.log('From -> ', keypair.publicKey.toString());
  console.log('To -> ', payer.publicKey.toString());
  console.log('Signature -> ', signature);
}

main();

Nodeでトランザクションを実行する。

% node transactions.js
Cluster ->  testnet
From ->  FLjBZzd7qEmodu4WrTyiTXCaLsGUjjPRHRCMYAgZkp2c
To ->  FNxQPD9aFpUVFEWkqfF6rwdNWQZhLXUehuvVGGc83DKX
Signature -> 5MhJWsX8Ru2CKe1tUH8KpPgDXUVRonYKT9aNYAUegw7fp152AKJKxjE878PB2C99crXPsPVG5368kfmyf6BMgzrF

トランザクション結果はtestnetで確認できる。ターミナルに出力されたSignatureで検索する。

Solana Explorer: 5MhJWsX8Ru2CKe1tUH8KpPgDXUVRonYKT9aNYAUegw7fp152AKJKxjE878PB2C99crXPsPVG5368kfmyf6BMgzrF

トランザクションが失敗する場合(調査中)

現象

まだ調査中だが、以下のようにトランザクションが失敗するケースがある。

% node transactions.js
/Users/user/node_modules/@solana/web3.js/lib/index.cjs.js:5631
      throw new Error(`Transaction was not confirmed in ${duration.toFixed(2)} seconds. It is unknown if it succeeded or failed. Check signature ${signature} using the Solana Explorer or CLI tools.`);
            ^

Error: Transaction was not confirmed in 60.01 seconds. It is unknown if it succeeded or failed. Check signature 3y4Y2wBJMaamfTPbxUY5pJ9YU9deDeCsSrBEXUR2Ncgkt3eQR5oAnMoQ3AZRmt679SUiqx9Rz7cn8VbD32FXUGpo using the Solana Explorer or CLI tools.
    at Connection.confirmTransaction (/Users/user/node_modules/@solana/web3.js/lib/index.cjs.js:5631:13)
    at async main (/Users/user/Desktop/blockchain/nodeServer/transactions.js:16:3)

成功/失敗は Solana Explorer で、ターミナルに出力されたSignatureを検索すると、確認できる。

対策1(時間を置く)

もしかしたら、立て続けに連続実行するとエラーになる? 数分時間を置いてから実行すると成功率が高い気がする。

対策2(Clusterをlocalhostにする)

testnetやdevnetではなく、localhostに接続することもできる。newするときにConnectionでURLを指定するだけ。
抜粋すると以下のようなイメージ。

const web3 = require('@solana/web3.js');
let connection = new web3.Connection('http://localhost:8899');

実装方法

1.solana-test-validatorでノードを起動する
2.solana logsでログをリッスンモードにする(ログを見たい場合)
3.ソースを以下のように修正
(変更前)

let connection = new web3.Connection(web3.clusterApiUrl(cluster));

(変更後)

let connection = new web3.Connection('http://localhost:8899');

4.JSを実行する

% node transactions.js

localなので処理速度も早いため、連続実行したい場合はlocalhost指定がよい。

% solana logs
Transaction executed in slot 8515:
  Signature: 4utcoP4eN1nFqGsX4Ktn3TVgiqEtSPsdRArb6UcDtBcBBZog51osBy6Esn9vsndWU17V7X3TQwX1zyQGDYvhpUx7
  Status: Ok
  Log Messages:
    Program 11111111111111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 success
Transaction executed in slot 8548:
  Signature: 4js3J88tqxEcoTn9z9kG3JFeNfBWNbRyTR1f3KrpA52TRyxBBKpyr31Scxqw6wDu92FyyvBT2Urwa6jAWFueLDGr
  Status: Ok
  Log Messages:
    Program 11111111111111111111111111111111 invoke [1]
    Program 11111111111111111111111111111111 success

awaitがある箇所の実装(エラー対策)

awaitがあるとSolana公式サイトをコピペ実装してもとエラーになる。

% node transactions.js
/Users/user/Desktop/blockchain/nodeServer/tx2.js:10
let airdropSignature = await connection.requestAirdrop(
                       ^^^^^

SyntaxError: await is only valid in async functions and the top level bodies of modules
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1031:15)
    at Module._compile (node:internal/modules/cjs/loader:1065:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47

awaitは関数内で使用する必要があるため、以下のようにすることで実行可能になる。

async function main() {
    (・・・サンプルコードを記述)
}

main();

コード

GitHubにサンプルコードを格納しておきました。
256hax/solana-rust-js-vanilla-example - GitHub

参考