Zuordnung

2021-10-17 Ungefähr 7 min

# Zuordnung

Zuordnungsfunktionen definieren, wie Kettendaten in die optimierten GraphQL-Entitäten umgewandelt werden, die wir zuvor in der Datei schema.graphql definiert haben.

Mappings werden in einer Teilmenge von TypeScript namens AssemblyScript geschrieben, die in WASM (WebAssembly) kompiliert werden kann.

  • Mappings werden im Verzeichnis src/mappings definiert und als Funktion exportiert
  • Diese Zuordnungen werden auch in src/index.ts exportiert
  • Die Mapping-Dateien sind in project.yaml unter den Mapping-Handlern referenziert.

Es gibt drei Klassen von Zuordnungsfunktionen; Blockhandler, Ereignishandler und Anrufhandler.

# Blockhandler

Sie können Blockhandler verwenden, um jedes Mal Informationen zu erfassen, wenn ein neuer Block an die Substratkette angehängt wird, z. Blocknummer. Dazu wird für jeden Block einmal ein definierter BlockHandler aufgerufen.

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();
}
1
2
3
4
5
6
7
8

Ein SubstrateBlock (opens new window) ist ein erweiterter Schnittstellentyp von signedBlock (opens new window), beinhaltet aber auch die specVersion und den timestamp.

# Ereignishandler

Sie können Ereignishandler verwenden, um Informationen zu erfassen, wenn bestimmte Ereignisse in einem neuen Block enthalten sind. Die Ereignisse, die Teil der standardmäßigen Substrate-Laufzeit und ein Block sind, können mehrere Ereignisse enthalten.

Während der Verarbeitung erhält der Ereignishandler ein Substratereignis als Argument mit den typisierten Ein- und Ausgaben des Ereignisses. Jede Art von Ereignis löst das Mapping aus, sodass Aktivitäten mit der Datenquelle erfasst werden können. Sie sollten in Ihrem Manifest Zuordnungsfilter verwenden, um Ereignisse zu filtern, um die Zeit zum Indexieren von Daten zu verkürzen und die Zuordnungsleistung zu verbessern.

importiere {SubstrateEvent} aus "@subql/types";

export async function handleEvent(event: SubstrateEvent): Promise<void> {
     const {Ereignis: {Daten: [Konto, Kontostand]}} = Ereignis;
     // Abrufen des Datensatzes nach seiner ID
     const record = new starterEntity(event.extrinsic.block.block.header.hash.toString());
     record.field2 = account.toString();
     record.field3 = (saldo als Balance).toBigInt();
     warten record.save();
1
2
3
4
5
6
7
8
9

Ein SubstrateEvent (opens new window) ist ein erweiterter Schnittstellentyp des EventRecord (opens new window). Neben den Ereignisdaten enthält es auch eine id (der Block, zu dem dieses Ereignis gehört) und die Extrinsic innerhalb dieses Blocks.

# Call Handler

Call-Handler werden verwendet, wenn Sie Informationen zu bestimmten externen Substraten erfassen möchten.

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

Das SubstrateExtrinsic (opens new window) erweitert GenericExtrinsic (opens new window). Ihm wird eine id zugewiesen (der Block, zu dem diese Extrinsic gehört) und stellt eine extrinsische Eigenschaft bereit, die die Ereignisse innerhalb dieses Blocks erweitert. Darüber hinaus zeichnet es den Erfolgsstatus dieses Extrinsic auf.

# Abfragestatus

Unser Ziel ist es, alle Datenquellen für Benutzer für das Mapping von Handlern abzudecken (mehr als nur die drei oben genannten Schnittstellenereignistypen). Aus diesem Grund haben wir einige der @polkadot/api-Schnittstellen bereitgestellt, um die Fähigkeiten zu erweitern.

Dies sind die Schnittstellen, die wir derzeit unterstützen:

Dies sind die Schnittstellen, die wir derzeit NICHT unterstützen:

  • api.tx.*
  • api.derive.*
  • api.query.<module>.<method>.at
  • api.abfrage.<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

Sehen Sie sich ein Beispiel für die Verwendung dieser API in unserem validator-threshold (opens new window)-Beispielanwendungsfall an.

# RPC-Anrufe

Wir unterstützen auch einige API-RPC-Methoden, bei denen es sich um Remoteaufrufe handelt, die es der Zuordnungsfunktion ermöglichen, mit dem tatsächlichen Knoten, der Abfrage und der Übermittlung zu interagieren. Eine Kernprämisse von SubQuery ist, dass es deterministisch ist. Um die Ergebnisse konsistent zu halten, erlauben wir daher nur historische RPC-Aufrufe.

Dokumente in JSON-RPC (opens new window) stellen einige Methoden bereit, die BlockHash als Eingabeparameter verwenden (z. B. at?: BlockHash), die jetzt erlaubt sind. Wir haben diese Methoden auch geändert, um standardmäßig den aktuellen Indexierungsblock-Hash zu verwenden.

// Nehmen wir an, wir indizieren gerade einen Block mit dieser Hash-Nummer
const blockhash = `0x844047c4cf1719ba6d54891e92c071a41e3dfe789d064871148e9d41ef086f6a`;

// Originalmethode hat eine optionale Eingabe ist Blockhash
const b1 = warten api.rpc.chain.getBlock(blockhash);

// Es wird der aktuelle Block verwendet, der standardmäßig so ist
const b2 = api.rpc.chain.getBlock() erwarten;
1
2
3
4
5
6
7
8

# Module und Bibliotheken

Um die Datenverarbeitungsfähigkeiten von SubQuery zu verbessern, haben wir einige der integrierten Module von NodeJS zum Ausführen von Mapping-Funktionen in der Sandbox zugelassen und den Benutzern erlaubt, Bibliotheken von Drittanbietern aufzurufen.

Bitte beachten Sie, dass dies eine experimentelle Funktion ist und Sie möglicherweise auf Fehler oder Probleme stoßen, die sich negativ auf Ihre Mapping-Funktionen auswirken können. Bitte melden Sie alle Fehler, die Sie finden, indem Sie ein Problem in GitHub (opens new window) erstellen.

# Eingebaute Module

Derzeit erlauben wir die folgenden NodeJS-Module: assert, buffer, crypto, util und path.

Anstatt das gesamte Modul zu importieren, empfehlen wir, nur die erforderliche(n) Methode(n) zu importieren. Einige Methoden in diesen Modulen weisen möglicherweise nicht unterstützte Abhängigkeiten auf und schlagen beim Importieren fehl.

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

export async function handleCall(extrinsic: SubstrateExtrinsic): Promise<void> {
  const record = new starterEntity(extrinsic.block.block.header.hash.toString());
  record.field1 = hashMessage('Hello');
  await record.save();
}
1
2
3
4
5
6
7
8

# Bibliotheken von Drittanbietern

Due to the limitations of the virtual machine in our sandbox, currently, we only support third-party libraries written by CommonJS.

We also support a hybrid library like @polkadot/* that uses ESM as default. However, if any other libraries depend on any modules in ESM format, the virtual machine will NOT compile and return an error.

# Custom Substrate Chains

SubQuery can be used on any Substrate-based chain, not just Polkadot or Kusama.

You can use a custom Substrate-based chain and we provide tools to import types, interfaces, and additional methods automatically using @polkadot/typegen (opens new window).

In the following sections, we use our kitty example (opens new window) to explain the integration process.

# Preparation

Create a new directory api-interfaces under the project src folder to store all required and generated files. We also create an api-interfaces/kitties directory as we want to add decoration in the API from the kitties module.

# Metadata

We need metadata to generate the actual API endpoints. In the kitty example, we use an endpoint from a local testnet, and it provides additional types. Follow the steps in PolkadotJS metadata setup (opens new window) to retrieve a node's metadata from its HTTP endpoint.

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

or from its websocket endpoint with help from websocat (opens new window):

//Install the websocat
brew install websocat

//Get metadata
echo state_getMetadata | websocat 'ws://127.0.0.1:9944' --jsonrpc
1
2
3
4
5

Next, copy and paste the output to a JSON file. In our kitty example (opens new window), we have created api-interface/kitty.json.

# Type definitions

We assume that the user knows the specific types and RPC support from the chain, and it is defined in the Manifest.

Following types setup (opens new window), we create :

  • src/api-interfaces/definitions.ts - this exports all the sub-folder definitions
export {default as kitties} from './kitties/definitions';
1
  • src/api-interfaces/kitties/definitions.ts - type definitions for the kitties module
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Packages

  • In the package.json file, make sure to add @polkadot/typegen as a development dependency and @polkadot/api as a regular dependency (ideally the same version). We also need ts-node as a development dependency to help us run the scripts.
  • We add scripts to run both types; generate:defs and metadata generate:meta generators (in that order, so metadata can use the types).

Here is a simplified version of package.json. Make sure in the scripts section the package name is correct and the directories are valid.

{
  "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"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Type generation

Now that preparation is completed, we are ready to generate types and metadata. Run the commands below:

# Yarn to install new dependencies
yarn

# Generate types
yarn generate:defs
1
2
3
4
5

In each modules folder (eg /kitties), there should now be a generated types.ts that defines all interfaces from this modules' definitions, also a file index.ts that exports them all.

# Generate metadata
yarn generate:meta
1
2

This command will generate the metadata and a new api-augment for the APIs. As we don't want to use the built-in API, we will need to replace them by adding an explicit override in our tsconfig.json. After the updates, the paths in the config will look like this (without the comments):

{
  "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"]
  }
}
1
2
3
4
5
6
7
8
9
10

# Usage

Now in the mapping function, we can show how the metadata and types actually decorate the API. The RPC endpoint will support the modules and methods we declared above. And to use custom rpc call, please see section Custom chain rpc calls

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);
}
1
2
3
4
5
6
7
8
9

If you wish to publish this project to our explorer, please include the generated files in src/api-interfaces.

# Custom chain rpc calls

To support customised chain RPC calls, we must manually inject RPC definitions for typesBundle, allowing per-spec configuration. You can define the typesBundle in the project.yml. And please remember only isHistoric type of calls are supported.

...
  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",
            }
          }
        }
      }
    }
  }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Last Updated: October 17, 2021 08:07