cosmosjsでNameserviceに接続する

前回は Cosmos SDK チュートリアルの Namesevice チェーンをローカルにインストール,起動してみました.

今回は chainapsis が開発している cosmosjs というパッケージを使って,この Nameservice Chain に接続してみます.

前回の内容はこちら.

https://blog.mktia.com/build-nameservice-of-cosmos-tutorials

インストール

terminal

$ npm i @chainapsis/cosmosjs

コーデックの登録

Cosmos SDK でビルドされている Nameservice チェーンと相互にデータを読み書きするためには,エンコード / デコードが必要で,そのためにコーデックを登録します.

msgs.ts

import { Amino, Type } from '@node-a-team/ts-amino';
const { Field, DefineStruct } = Amino;
import { Msg } from '@chainapsis/cosmosjs/core/tx';
import { AccAddress } from '@chainapsis/cosmosjs/common/address';
import { Coin } from '@chainapsis/cosmosjs/common/coin';

@DefineStruct()
export class MsgSetName extends Msg {
  @Field.String(0, {
    jsonName: 'name'
  })
  public name: string;

  @Field.String(1, {
    jsonName: 'value'
  })
  public value: string;

  @Field.Defined(2, {
    jsonName: 'owner'
  })
  public owner: AccAddress;

  constructor(name: string, value: string, owner: AccAddress) {
    super();
    this.name = name;
    this.value = value;
    this.owner = owner;
  }

  public getSigners(): AccAddress[] {
    return [this.owner]
  }

  public validateBasic(): void {}
}

@DefineStruct()
export class MsgBuyName extends Msg {
  @Field.String(0, {
    jsonName: 'name'
  })
  public name: string

  @Field.Slice(
    1,
    { type: Type.Defined },
    {
      jsonName: 'bid'
    }
  )
  public bid: Coin[]
;

  @Field.Defined(2, {
    jsonName: 'buyer'
  })
  public buyer: AccAddress;

  constructor(name: string, bid: Coin[], buyer: AccAddress) {
    super();
    this.name = name;
    this.bid = bid;
    this.buyer = buyer;
  }

  public getSigners(): AccAddress[] {
    return [this.buyer];
  }

  public validateBasic(): void {}
}


@DefineStruct()
export class MsgDeleteName extends Msg {
  @Field.String(0, {
    jsonName: 'name'
  })
  public name: string;

  @Field.Defined(1, {
    jsonName: 'owner'
  })
  public owner: AccAddress;

  constructor(name: string, owner: AccAddress) {
    super();
    this.name = name;
    this.owner = owner;
  }

  public getSigners(): AccAddress[] {
    return [this.owner];
  }

  public validateBasic(): void {}
}

Nameservice チェーンのドキュメントと比較しながら適切に引数,コントラクトを書くとこのような感じになります.

引数を定義している @Field の型は @node-a-team/ts-amino で確認できます.AccAddress などの特殊な型は Defined と置くことで実装できるようです.String なのに Defined とかで実装すると,エラーが出ます.

ここで定義したクラスをコーデックに登録するようにします.

codec.ts

import { Codec } from '@node-a-team/ts-amino';
import { MsgSetName, MsgBuyName, MsgDeleteName } from './msgs';

export function registerCodec(codec: Codec) {
  codec.registerConcrete('nameservice/SetName', MsgSetName.prototype);
  codec.registerConcrete('nameservice/BuyName', MsgBuyName.prototype);
  codec.registerConcrete('nameservice/DeleteName', MsgDeleteName.prototype);
}

そして,これらのファイルを一括でインポートするためのファイルを用意します.

index.ts

export * from './msgs';
export * from './codec';

用意した3ファイルを nameservice ディレクトリに入れることで,クラスを呼び出す際に以下のように書くことができます.

import { MsgSetName, MsgBuyName, MsgDeleteName } from "/path/to/nameservice";

API の作成

cosmosjs では Gaia API がデフォルトになっていますが,新機能をコーデックに登録したときなどは API も新たに作らなければなりません.コーデックが登録されていないからです.

customApi.ts

import { Api, ApiConfig, CoreConfig } from '@chainapsis/cosmosjs/core/api';
import * as CmnCdc from '@chainapsis/cosmosjs/common/codec';
import * as Bank from '@chainapsis/cosmosjs/x/bank';
import * as Distribution from '@chainapsis/cosmosjs/x/distribution';
import * as Gov from '@chainapsis/cosmosjs/x/gov';
import * as Slashing from '@chainapsis/cosmosjs/x/slashing';
import * as Staking from '@chainapsis/cosmosjs/x/staking';
import * as Nameservice from '/path/to/nameservice';
import { defaultTxEncoder } from '@chainapsis/cosmosjs/common/stdTx';
import { stdTxBuilder } from '@chainapsis/cosmosjs/common/stdTxBuilder';
import { Context } from '@chainapsis/cosmosjs/core/context';
import { Account } from '@chainapsis/cosmosjs/core/account';
import { BIP44 } from '@chainapsis/cosmosjs/core/bip44';
import { defaultBech32Config } from '@chainapsis/cosmosjs/core/bech32Config';
import { Codec } from '@chainapsis/ts-amino';
import { queryAccount } from '@chainapsis/cosmosjs/core/query';
import * as Crypto from '@chainapsis/cosmosjs/crypto';

import { CustomRest } from './customRest';
import * as Nameservice from '/path/to/nameservice';

export class CustomApi extends Api<CustomRest> {
  constructor(
    config: ApiConfig,
    coreConfig: Partial<CoreConfig<CustomRest>> = {}
  ) {
    super(config, {
      ...{
        txEncoder: defaultTxEncoder,
        txBuilder: stdTxBuilder,
        restFactory: (context: Context) => {
          return new CustomRest(context);
        },
        queryAccount: (
          context: Context,
          address: string | Uint8Array
        ): Promise<Account> => {
          return queryAccount(
            context.get('rpcInstance'),
            address,
            coreConfig.bech32Config
              ? coreConfig.bech32Config.bech32PrefixAccAddr
              : 'cosmos'
          );
        },
        bech32Config: defaultBech32Config('cosmos'),
        bip44: new BIP44(44, 118, 0),
        registerCodec: (codec: Codec) => {
          CmnCdc.registerCodec(codec)
          Crypto.registerCodec(codec)
          Bank.registerCodec(codec)
          Distribution.registerCodec(codec)
          Gov.registerCodec(codec)
          Slashing.registerCodec(codec)
          Staking.registerCodec(codec)
          Nameservice.registerCodec(codec)
        }
      },
      ...coreConfig
    });
  }
}

先ほど作成した nameservice をインポートしてコーデックに登録します.

これで SetName, BuyName, DeleteName の機能を利用することができるようになりました.

Rest も Gaia REST を使わずにカスタマイズするので,この後に用意しますが先にインポートしておきます.

REST の作成

コーデックを登録することで Nameservice チェーンにトランザクションの内容を反映させることはできるようになりました.

後は,GET で Nameservice チェーンからデータを取得できれば OK です.

customRest.ts

import { Rest } from '@chainapsis/cosmosjs/core/rest';
import { Account } from '@chainapsis/cosmosjs/core/account';
import { AccAddress } from '@chainapsis/cosmosjs/common/address';
import { CustomBaseAccount } from '../customBaseAccount';

export class CustomRest extends Rest {
  public async getAccount(
    account: string | Uint8Array,
    bech32PrefixAccAddr?: string
  ): Promise<Account> {
    if (typeof account === 'string' && !bech32PrefixAccAddr) {
      throw new Error('Empty bech32 prefix');
    }

    const accAddress: AccAddress =
      typeof account === 'string'
        ? AccAddress.fromBech32(account, bech32PrefixAccAddr)
        : new AccAddress(account, bech32PrefixAccAddr!);

    const result = await this.instance.get(
      `auth/accounts/${accAddress.toBech32()}`
    );
    return CustomBaseAccount.fromJSON(result.data);
  }

  public async resolveName(name: string): Promise<string> {
    const result = await this.instance.get(`nameservice/names/${name}`);
    return result.data;
  }

  public async whois(name: string): Promise<string> {
    const result = await this.instance.get(`nameservice/names/${name}/whois`);
    return result.data;
  }

  public async getNames(): Promise<string> {
    const result = await this.instance.get('nameservice/names');
    return result.data;
  }
}

データは JSON で返ってきますが,とりあえず内容を確認するだけということでそのまま返しています.this.instance は axios を呼んでいるので,簡単に実装できます.

customBaseAccount を呼んでいますが,これはアカウントタイプがなぜか undefined になってしまう問題を回避するために,全く同じ内容でローカルに用意しているだけなので割愛します.

ここまでくれば準備完了です.

TX の作成と取引実行

import { defaultBech32Config } from "@chainapsis/cosmosjs/core/bech32Config";
import { LocalWalletProvider } from "@chainapsis/cosmosjs/core/walletProvider";
import { AccAddress } from "@chainapsis/cosmosjs/common/address";
import { Coin } from "@chainapsis/cosmosjs/common/coin";
import { Int } from "@chainapsis/cosmosjs/common/int";
import bigInteger from "big-integer";
import { BIP44 } from "@chainapsis/cosmosjs/core/bip44";

import { CustomApi } from "/path/to/customApi";
import { CustomRest } from "/path/to/customRest";
import { MsgSetName, MsgBuyName, MsgDeleteName } from "/path/to/nameservice";

(async () => {
  const wallet = new LocalWalletProvider("cosmos", this.jackMnemonic);
  const api = new CustomApi({
    chainId: "namechain",
    walletProvider: wallet,
    rpc: "http://localhost:26657",
    rest: "http://localhost:1317",
  });

  // You should sign in before using your wallet
  await api.enable();

  const key = (await api.getKeys())[0];
  const accAddress = new AccAddress(key.address, "cosmos");
  const rest = new CustomRest(api.context);
  const account = await rest.getAccount(key.address, "cosmos");

  const resultBroadcastTxCommit = await api.sendMsgs(
    [
      new MsgBuyName(
        "jack1.id",
        [new Coin("nametoken", new Int("5"))],
        AccAddress.fromBech32(this.jackWallet.address, "cosmos")
      ),
      new MsgSetName(
        "jack1.id",
        "8.8.4.4",
        AccAddress.fromBech32(this.jackWallet.address, "cosmos")
      ),
    ],
    {
      gas: bigInteger(80000),
      memo: "test",
      fee: new Coin("nametoken", new Int("11")),
    },
    "commit"
  );
  console.log(resultBroadcastTxCommit);

  const result = await rest.resolveName("jack1.id");
  console.log(result);
  // output: 8.8.4.4
})();

今回は LedgerWalletProvider ではなく LocalWalletProvider を利用します.

チュートリアルにあった処理を CLI ではなく JS 上で実装すると,このようになります.

最後に

Cosmos SDK と合わせて使うことでブロックチェーンを利用したサービスを比較的簡単に作れそうです.