マッピング

SubQuery Team約5分

マッピング

マッピング関数は、 schema.graphql ファイルで定義したチェーンデータを最適化した GraphQL エンティティに変換する方法を定義します。

  • マッピングは src/mappings ディレクトリに定義され、関数としてエクスポートされます。
  • これらのマッピングは src/index.ts にもエクスポートされます。
  • マッピングファイルはマッピングハンドラの下の project.yaml 内で参照されます。

マッピング関数には次の 3 つのクラスがあります。 ブロックハンドラ, イベントハンドラ, 呼び出しハンドラ

ブロックハンドラ

新しいブロックが Substrate チェーンに接続されるたびに、ブロックハンドラを使用して情報を取得できます。 これを実行するために、定義されたブロックハンドラが各ブロックに対して 1 回呼び出されます。

import { SubstrateBlock } from "@subql/types";

export async function handleBlock(block: SubstrateBlock): Promise<void> {
  // Create a new StarterEntity with the block hash as it's ID
  const record = new starterEntity(block.block.header.hash.toString());
  record.field1 = block.block.header.number.toNumber();
  await record.save();
}

SubstrateBlockopen in new windowsignedBlockopen in new windowの拡張インターフェースタイプで、specVersiontimestampも含まれています。

イベント ハンドラ

特定のイベントが新しいブロックに含まれる場合、イベントハンドラを使用して情報を取得できます。 デフォルトの Substrate ランタイムとブロックの一部であるイベントには、複数のイベントが含まれます。

処理中、イベントハンドラは substrate イベントを受け取り、イベントの型付けされた入出力を持つ引数として受け取ります。 どのようなタイプのイベントでもマッピングのトリガーとなり、データソースでのアクティビティをキャプチャすることができます。 イベントをフィルタリングするには、マニフェストで マッピングフィルタ を使用し、データのインデックス化とマッピングのパフォーマンスを向上させる必要があります。

import {SubstrateEvent} from "@subql/types";

export async function handleEvent(event: SubstrateEvent): Promise<void> {
    const {event: {data: [account, balance]}} = event;
    // Retrieve the record by its ID
    const record = new starterEntity(event.extrinsic.block.block.header.hash.toString());
    record.field2 = account.toString();
    record.field3 = (balance as Balance).toBigInt();
    await record.save();

SubstrateEventopen in new windowEventRecordopen in new window の拡張インターフェイス型です。 イベントデータのほかに、 id (このイベントが属するブロック) と、このブロックの内部にある外部データも含まれています。

呼び出しハンドラ

呼び出しハンドラは、特定の Substrate 外部関数の情報をキャプチャするときに使用されます。

export async function handleCall(extrinsic: SubstrateExtrinsic): Promise<void> {
  const record = new starterEntity(
    extrinsic.block.block.header.hash.toString()
  );
  record.field4 = extrinsic.block.timestamp;
  await record.save();
}

SubstrateExtinsicopen in new windowGenericExtrinsicopen in new window を拡張します。 このブロックには、 id (この外部が属するブロック) が割り当てられており、このブロックの中でイベントを拡張する外部プロパティを提供します。 さらに、この外部関数のステータスを記録します。

クエリの状態

私たちの目標は、マッピングハンドラ(上記の 3 つのインターフェースイベントタイプだけでなく)のために、ユーザーのすべてのデータソースをカバーすることです。 したがって、私たちは @polkadot/api インタフェースのいくつかを公開しています。

これらは現在サポートされているインターフェースです:

これらは現在サポート されていない インターフェイスです:

  • api.tx.*
  • api.derive.*
  • api.query.<module>.<method>.at
  • api.query.<module>.<method>.entriesAt
  • api.query.<module>.<method>.entriesPaged
  • api.query.<module>.<method>.hash
  • api.query.<module>.<method>.keysAt
  • api.query.<module>.<method>.keysPaged
  • api.query.<module>.<method>.range
  • api.query.<module>.<method>.sizeAt

validator-thresholdopen in new window のユースケースでこの API を使用する例をご覧ください。

RPC コール

また、マッピング関数が実際のノード、クエリー、および送信を行うことを可能にするリモートコールである API RPC 関数もサポートしています。 SubQuery は決定論的であることを前提としているため、結果の一貫性を保つために、過去の RPC コールのみを許可しています。

JSON-RPCopen in new windowのドキュメントでは、BlockHashを入力パラメータとして受け取るいくつかのメソッド(例:at?: BlockHash)がありますが、これが許可されるようになりました。 また、これらの関数は、現在のインデックスブロックハッシュをデフォルトで受け取るように変更しました。

// Let's say we are currently indexing a block with this hash number
const blockhash = `0x844047c4cf1719ba6d54891e92c071a41e3dfe789d064871148e9d41ef086f6a`;

// Original method has an optional input is block hash
const b1 = await api.rpc.chain.getBlock(blockhash);

// It will use the current block has by default like so
const b2 = await api.rpc.chain.getBlock();

モジュールとライブラリ

SubQuery のデータ処理能力を向上させるには sandboxでマッピング関数を実行するための NodeJS の組み込みモジュールの一部を許可しました。

これは 実験的機能 であり、マッピング関数に悪影響を与えるバグや問題が発生する可能性があります。 Issue を GitHubopen in new window で作成することで、バグを報告してください。

組み込みモジュール

現在、次の NodeJS モジュールを許可しています:assert, buffer, crypto, util, path

モジュール全体をインポートするのではなく、必要なメソッドだけをインポートすることをお勧めします。 これらのモジュールのいくつかのメソッドにはサポートされていない依存関係があり、インポート時に失敗する可能性があります。

import { hashMessage } from "ethers/lib/utils"; // Good way
import { utils } from "ethers"; // Bad way

サードパーティライブラリ

サンドボックス内の仮想マシンが制限されているため、現在、 CommonJS によって書かれたサードパーティ製ライブラリのみをサポートしています。

hybrid ライブラリ(例えば @polkadot/* )もサポートしており、ESM をデフォルトとして使用しています。 しかし、他のライブラリがESM形式のモジュールに依存している場合、仮想マシンはコンパイルされず、エラーを返します。

カスタム Substrate チェーン

SubQuery は、Polkadot や Kusama だけではなく、Substrate-based chain 上で使用できます。

Substrate ベースのカスタムチェーンを使用することができ、@polkadot/typegenopen in new windowを使用してタイプ、インターフェイス、追加関数を自動的にインポートするツールを提供しています。

以下のセクションでは、 キティの例open in new window を使用して統合プロセスを説明します。

準備

プロジェクトの src フォルダの下に新しいディレクトリ api-interfaces を作成し、必要なファイルと生成されたファイルをすべて保存します。 kitties モジュールから API にデコレーションを追加するため、 api-interfaces/kitties ディレクトリを作成します。

メタデータ

実際の API エンドポイントを生成するにはメタデータが必要です。 キティの例では、ローカルのテストネットからのエンドポイントを使用し、追加の型を提供します。 PolkadotJS metadata setupopen in new window の手順に従い、 HTTP endpoint からノードのメタデータを取得します。

curl -H "Content-Type: application/json" -d '{"id":"1", "jsonrpc":"2.0", "method": "state_getMetadata", "params":[]}' http://localhost:9933

または、websocketエンドポイントからwebsocatopen in new windowの助けを借ります。

//Install the websocat
brew install websocat

//Get metadata
echo state_getMetadata | websocat 'ws://127.0.0.1:9944' --jsonrpc

次に、JSON ファイルに出力結果をコピーし、貼り付けます。 キティの例open in new windowでは、 api-interface/kitty.json を作成しました。

型の定義

ここでは、ユーザーがチェーンから特定のタイプと RPC サポートを知っており、それがManifestで定義されていることを想定しています。

types setupopen in new windowに続いて作成します

  • src/api-interfaces/definitions.ts - 全てのサブフォルダ定義をエクスポートします
export { default as kitties } from "./kitties/definitions";
  • src/api-interfaces/kities/definitions.ts - kitties モジュールの型定義
export default {
  // custom types
  types: {
    Address: "AccountId",
    LookupSource: "AccountId",
    KittyIndex: "u32",
    Kitty: "[u8; 16]",
  },
  // custom rpc : api.rpc.kitties.getKittyPrice
  rpc: {
    getKittyPrice: {
      description: "Get Kitty price",
      params: [
        {
          name: "at",
          type: "BlockHash",
          isHistoric: true,
          isOptional: false,
        },
        {
          name: "kittyIndex",
          type: "KittyIndex",
          isOptional: false,
        },
      ],
      type: "Balance",
    },
  },
};

パッケージ

  • package.jsonファイルでは、@polkadot/typegenを開発用の依存関係に、@polkadot/apiを通常の依存関係(理想的には同じバージョン)に追加してください。 また、スクリプトを実行するために、開発の依存性として ts-node も必要です。
  • 両方の型を実行するスクリプトを追加します; generate:defs と メタデータジェネレータgenerate:metadata (この順序で、メタデータが型を使用できるようにします)

以下は package.json の簡略化されたバージョンです。 スクリプト セクションでパッケージ名が正しく、ディレクトリが有効であることを確認します。

{
  "name": "kitty-birthinfo",
  "scripts": {
    "generate:defs": "ts-node --skip-project node_modules/.bin/polkadot-types-from-defs --package kitty-birthinfo/api-interfaces --input ./src/api-interfaces",
    "generate:meta": "ts-node --skip-project node_modules/.bin/polkadot-types-from-chain --package kitty-birthinfo/api-interfaces --endpoint ./src/api-interfaces/kitty.json --output ./src/api-interfaces --strict"
  },
  "dependencies": {
    "@polkadot/api": "^4.9.2"
  },
  "devDependencies": {
    "typescript": "^4.1.3",
    "@polkadot/typegen": "^4.9.2",
    "ts-node": "^8.6.2"
  }
}

型の生成

これで準備が完了し、型とメタデータを生成する準備が整いました。 以下のコマンドを実行します。

# Yarn to install new dependencies
yarn

# Generate types
yarn generate:defs

各モジュールのフォルダ(例:/kitties)には、このモジュールの定義からすべてのインターフェイスを定義したtypes.tsと、それらすべてをエクスポートするindex.tsが生成されているはずです。

# Generate metadata
yarn generate:meta

このコマンドは、API のメタデータと新しい拡張機能を生成します。 組み込みの API を使用したくないので、tsconfig.jsonに明示的なオーバーライドを追加して置き換える必要があります。 更新後、設定内のパスは次のようになります(コメントなし):

{
  "compilerOptions": {
    // this is the package name we use (in the interface imports, --package for generators) */
    "kitty-birthinfo/*": ["src/*"],
    // here we replace the @polkadot/api augmentation with our own, generated from chain
    "@polkadot/api/augment": ["src/interfaces/augment-api.ts"],
    // replace the augmented types with our own, as generated from definitions
    "@polkadot/types/augment": ["src/interfaces/augment-types.ts"]
  }
}

使用法

マッピング関数では、メタデータと型が実際に API をどのようにデコレートするかを示すことができます。 RPC エンドポイントは、上記で宣言したモジュールと関数をサポートします。 カスタム RPC 呼び出しを使用するには、カスタムチェーン RPC 呼び出しのセクションを参照してください。

export async function kittyApiHandler(): Promise<void> {
  //return the KittyIndex type
  const nextKittyId = await api.query.kitties.nextKittyId();
  // return the Kitty type, input parameters types are AccountId and KittyIndex
  const allKitties = await api.query.kitties.kitties("xxxxxxxxx", 123);
  logger.info(`Next kitty id ${nextKittyId}`);
  //Custom rpc, set undefined to blockhash
  const kittyPrice = await api.rpc.kitties.getKittyPrice(
    undefined,
    nextKittyId
  );
}

このプロジェクトをエクスプローラに公開したい場合は、生成されたファイルを src/api-interface に含めてください。

カスタムチェーン RPC コール

カスタマイズされたチェーン RPC 呼び出しをサポートするには、仕様ごとの設定を可能にする typesBundleに手動で RPC 定義を挿入する必要があります。 project.ymltypesBundle を定義できます。 そして、 isHistoric タイプの呼び出しのみがサポートされていることを覚えておいてください。

...
  types: {
    "KittyIndex": "u32",
    "Kitty": "[u8; 16]",
  }
  typesBundle: {
    spec: {
      chainname: {
        rpc: {
          kitties: {
            getKittyPrice:{
                description: string,
                params: [
                  {
                    name: 'at',
                    type: 'BlockHash',
                    isHistoric: true,
                    isOptional: false
                  },
                  {
                    name: 'kittyIndex',
                    type: 'KittyIndex',
                    isOptional: false
                  }
                ],
                type: "Balance",
            }
          }
        }
      }
    }
  }