Chapter 10無料公開

REST APIクライアント

Kimura Yu
Kimura Yu
2021.10.16に更新

REST APIクライアント

https://github.com/cosmos-client/cosmos-client-ts

著者を中心として開発されたCosmos SDKのREST API用のクライアントライブラリ"cosmos-client"を利用すると、プログラミング言語TypeScriptを利用して非常にかんたんにCosmos SDKのRESTを介したCosmos SDKブロックチェーンのクライアントアプリを開発することができます。

Node.JSがインストールされている環境で、Cosmos SDKのクライアント機能を実装したいプロジェクトの中で、以下のコマンドを実行することでプロジェクトにインストールすることができます。

npm install cosmos-client

以下が、単純にコインを送金する処理のテストとして書かれたコードです。
ニーモニック、アドレスは自分のものに変更し、テストコードのための記述(describeitexpectなど)を適宜外してください。

import { cosmosclient, rest, proto } from 'cosmos-client';

describe('bank', () => {
  it('send', async () => {
    expect.hasAssertions();

    const sdk = new cosmosclient.CosmosSDK('http://localhost:1317', 'testchain');

    const privKey = new proto.cosmos.crypto.secp256k1.PrivKey({
      key: await cosmosclient.generatePrivKeyFromMnemonic('joke door law post fragile cruel torch silver siren mechanic flush surround'),
    });
    const pubKey = privKey.pubKey();
    const address = cosmosclient.AccAddress.fromPublicKey(pubKey);

    expect(address.toString()).toStrictEqual('cosmos14ynfqqa6j5k3kcqm2ymf3l66d9x07ysxgnvdyx');

    const fromAddress = address;
    const toAddress = address;

    // get account info
    const account = await rest.cosmos.auth
      .account(sdk, fromAddress)
      .then((res) => res.data.account && cosmosclient.codec.unpackCosmosAny(res.data.account))
      .catch((_) => undefined);

    if (!(account instanceof proto.cosmos.auth.v1beta1.BaseAccount)) {
      console.log(account);
      return;
    }

    // build tx
    const msgSend = new proto.cosmos.bank.v1beta1.MsgSend({
      from_address: fromAddress.toString(),
      to_address: toAddress.toString(),
      amount: [{ denom: 'token', amount: '1' }],
    });

    const txBody = new proto.cosmos.tx.v1beta1.TxBody({
      messages: [cosmosclient.codec.packAny(msgSend)],
    });
    const authInfo = new proto.cosmos.tx.v1beta1.AuthInfo({
      signer_infos: [
        {
          public_key: cosmosclient.codec.packAny(pubKey),
          mode_info: {
            single: {
              mode: proto.cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT,
            },
          },
          sequence: account.sequence,
        },
      ],
      fee: {
        gas_limit: cosmosclient.Long.fromString('200000'),
      },
    });

    // sign
    const txBuilder = new cosmosclient.TxBuilder(sdk, txBody, authInfo);
    const signDocBytes = txBuilder.signDocBytes(account.account_number);
    txBuilder.addSignature(privKey.sign(signDocBytes));

    // broadcast
    const res = await rest.cosmos.tx.broadcastTx(sdk, {
      tx_bytes: txBuilder.txBytes(),
      mode: rest.cosmos.tx.BroadcastTxMode.Block,
    });
    console.log(res);

    expect(res.data.tx_response?.raw_log?.match('failed')).toBeFalsy();
  });
});

以下のステップに分けて説明をします。

  • アカウント情報を取得
  • 未署名トランザクションを取得
  • トランザクションに署名
  • 署名済みトランザクションをブロードキャスト

アカウント情報を取得

// get account info

と書かれている箇所を見てください。

ブロックチェーンにおいてアカウントごとに記録されているaccount_numbersequenceという値が、トランザクションを作成するために必要となります。

そのため、事前に取得しておく必要があり、このような取得の処理を行っています。

UnpackCosmosAny

Cosmos SDK製ブロックチェーンのREST APIはJSONのフォーマットでレスポンスが返されますが、このJSONにおいて、「共用体」となるデータ項目が多々あります。

複数の種類の型のうちのどれかが入り得るデータ項目のことです。

今回のアカウント取得処理も、Cosmos SDK内部ではAccountというインターフェースを備えた型を返すことしか決まっておらず、BaseAccountという型が返ってくる保証はどこにもありません。ModuleAccountいう型もありますから、それが返って来る可能性もあるのです。

BaseAccount型は本来、

  • address
  • pub_key
  • account_number
  • sequence

の4つのフィールドを持つ型なのですが、このように共用体であるデータ項目内でBaseAccountのデータが返された場合、

  • @type
  • address
  • pub_key
  • account_number
  • sequence

このように@typeが追加されて5つのフィールドを持ったJSONが返されるようになっています。

@typeの中身は、BaseAccount型なら"/cosmos.auth.v1beta1.BaseAccount"になることが、ModuleAccount型なら"/cosmos.auth.v1beta1.ModuleAccount"になることが決まっています。

要するに@typeを付与することによって、共用体であっても型を区別できるようになっているのです。(このJSONデータ形式を筆者は個人的にCosmosAnyと名付けています。)

cosmos-clientに実装されているUnpackCosmosAny関数は、JSONをパースし、@typeの中身に応じて、その型に対応するクラスのインスタンスを生成してくれます。オブジェクトではなくクラスのインスタンスです。
クラスのインスタンスなので、instanceofを使うことによって、型を区別することができます。便利ですね。

今回の場合は、JSONのパース時にエラーが起きるか、もしくはModuleAccountであった場合には処理を終了するようになっています。

トランザクションデータの構築

// build tx

と書かれている箇所を見てください。

処理の流れとしては、

  • TxBodyに含めるMsgを作成
  • TxBodyを作成
  • AuthInfoを作成

となっています。順を追って説明しましょう。

このMsgも、データ型の観点でみると先ほどのAccountと同じように共用体です。MsgにはMsgSend以外にも様々な型があるからです。

したがって「Msgの配列」は共用体の配列です。

共用体なので、実際に入っている型を区別するための情報を付与する必要があります。そこでcosmosclient.codec.packAny関数を使います。

packCosmosAnyではなくpackAnyであることに注意が必要です。

なぜかというと、Cosmos SDK製ブロックチェーンのv0.40以降では、トランザクションのデータを、 Protocol Buffersのデータ形式でエンコードすることになっています。JSONのデータ形式で送るのではないのです。
このpackAnyは、Protocol buffersのデータ形式で、共用体の中のデータを区別するための情報を付与する処理を行っています。
JSONではなく、Protocol buffersのデータ形式で区別するために、packAnyを使うというわけです。

Cosmos AnyはREST APIの返却値としてのJSONの話で、AnyはProtocol Buffersの話、と整理してもらうとわかりやすいかもしれません。

トランザクションに署名

// sign

と書かれている箇所を見てください。

やっていることは簡単で、まずtxBodyauthInfoaccount_numberをもとに、「デジタル署名を施す対象のバイナリデータ」となるsignDocBytesを生成しています(内部的な話をするとこれはProtocol Buffersでエンコードされたバイナリデータです)。

次に、デジタル署名を作成し、それを署名の配列に追加しています。

計算により署名をつくるので、通信は発生しません。

sdk.chainIDを署名に利用することで、リプレイプロテクションを行っています。

※リプレイアタックとは、ブロックチェーンのネットワークが分裂した際に、分裂した先でトランザクションを再発生させる攻撃です。

また、sequenceにより、署名したまま放置された太古のトランザクションが悪用されるといった可能性を防ぐことができます。

署名済みトランザクションをブロードキャスト

// broadcast

と書かれている箇所を見てください。

ブロードキャストモードには、3種類があります。

  • Async: REST APIサーバは、なにもチェックせず即座にtx hashを返却する
  • Sync: REST APIサーバは、署名が正しいかどうかのチェックのみ行い、それの成否を返却する
  • Block: REST APIサーバは、署名が正しいかどうかに加え、トランザクションが承認されるまでの待機を行い、トランザクション承認後の処理の成否を返却する

つまり、Asyncでは正しい署名をしていなくても、成功したかのようにデータが返却されます。したがって事後的に署名が正常に確認されたことと、トランザクション承認と承認後の処理が実行されたか確認する必要があります。

Syncは、正しい署名をしていなかった場合は失敗したものとしてデータが返却されますが、トランザクションが承認されるまでの待機は行わないので、事後的にトランザクション承認と承認後の処理が実行されたか確認する必要があります。

Blockは事後的な確認をする必要がない点では便利なAPIですが、REST APIサーバの処理負荷という観点では好ましくないと考えられています(公式ドキュメントにおいて)。