Мапинг

... 2022-8-15 About 3 min

# Мапинг

Мапинг функциите дефинират как данните от веригата се трансформират в оптимизираните GraphQL обекти, които по-рано сме дефинирали във файла schema.graphql.

  • Мапингите се дефинират в директорията src/mappings и се експортират като функция
  • Мапингите също се експортират src/index.ts
  • Мапинг файловете са препратки в project.yaml под манипулаторите за мапинг.

Има три класа функции за мапинг; Манипулатори на блокове, Манипулатори на събития и Манипулатори на изпълнение.

# Манипулатор на блокове

Можете да използвате манипулатори на блокове, за да улавяте информация всеки път, когато нов блок е прикачен към веригата на Substrate, напр. номер на блока. За да се постигне това, дефиниран BlockHandler ще бъде заявен веднъж за всеки блок.

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

SubstrateBlock (opens new window) е разширен тип интерфейс на signedBlock (opens new window), но също така включва specVersion и timestamp.

# Манипулатор на събития

Можете да използвате манипулатори на събития за улавяне на информация, когато определени събития са включени в нов блок. Събитията, които са част от времето за изпълнение на 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();
1
2
3
4
5
6
7
8
9

SubstrateEvent (opens new window) е разширен тип интерфейс на EventRecord (opens 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();
}
1
2
3
4
5

SubstrateExtrinsic (opens new window) разширява GenericExtrinsic (opens new window). Прилага му се id (блокът, към който принадлежи този външен елемент) и предоставя външен елемент, който разширява събитията между този блок. Освен това, той записва статуса на успех на този външен елемент.

# Състояния на заявка

Нашата цел е да покрием всички източници на данни за потребители за манипулатори на мапинг (повече от трите типа събития на интерфейса по-горе). Ето защо ние разкрихме някои от @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

Вижте пример за използване на този API в нашия пример за използване на validator-threshold (opens new window).

# RPC повиквания

Ние поддържаме някои API RPC методи, които са отдалечени повиквания, които позволяват на функцията за мапинг да взаимодейства с действителния нод, заявка и подаване. Основната предпоставка на SubQuery е, че е детерминистична и следователно, за да поддържаме резултатите последователни, позволяваме само исторически RPC повиквания.

Документите в JSON-RPC (opens new window) предоставят някои методи, които приема BlockHash като входен параметър (например at?: BlockHash), който вече е разрешен. Ние също така променихме тези методи, за да приемем хеш на текущия индексиращ блок по подразбиране.

// Да приемем, че в момента индексираме блок с този хеш номер
const blockhash = `0x844047c4cf1719ba6d54891e92c071a41e3dfe789d064871148e9d41ef086f6a`;

// Оригиналният метод има избираем вход в блок хеш
const b1 = await api.rpc.chain.getBlock(blockhash);

// Ще използва текущия блок по подразбиране, по този начин:
const b2 = await api.rpc.chain.getBlock();
1
2
3
4
5
6
7
8

# Модули и библиотеки

За да подобрим възможностите на SubQuery за обработка на данни, ние разрешихме някои от вградените модули на NodeJS за изпълнение на функции за мапинг в sandbox, и позволихме на потребителите да изпълняват библиотеки на трети страни.

Моля, имайте предвид, че това е експериментална функция и може да срещнете грешки или проблеми, които могат да повлияят негативно на функциите ви за мапинг. Моля, докладвайте всички грешки, които откриете, като създадете казус в GitHub (opens new window).

# Вградени модули

Понастоящем позволяваме следните модули на NodeJS: assert, buffer, crypto, util и path.

Вместо да импортирате целия модул, препоръчваме да импортирате само необходимия(те) метод(и), от който се нуждаете. Някои методи в тези модули може да имат зависимости, които не се поддържат и няма да се импортират.

импортирайте  {hashMessage} от "ethers/lib/utils"; //Добър начин
импортирайте  {utils} от "ethers" //Лош начин

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

# Библиотеки на трети страни

Поради ограниченията на виртуалната машина в нашия sandbox, в момента поддържаме само библиотеки на трети страни, написани на CommonJS.

Ние поддържаме хибридна библиотека като @polkadot/* която използва ESM по подразбиране. Въпреки това, ако други библиотеки зависят от модули във формат ESM виртуалната машина НЯМА да компилира и да върне грешка.

# Персонализирани вериги за Substrate

SubQuery може да се използва във всяка верига, базирана на Substrate, не само Polkadot или Kusama.

Можете да използвате персонализирана верига, базирана на Substrate, и ние предоставяме инструменти за автоматично импортиране на типове, интерфейси и допълнителни методи, използвайки @polkadot/typegen (opens new window).

В следващите раздели използваме нашия пример kitty (opens new window), за да обясним процеса на интеграция.

# Подготовка

Създайте нова директория api-interfaces под папката src на проекта, за да съхранявате всички необходими и генерирани файлове. Също създаваме директория api-interfaces/kitties тъй като искаме да добавим декорация в API от модула kitties.

# Метаданни

Нуждаем се от метаданни, за да генерираме действителните крайни точки на API. В примера с котките, ние използваме крайна точка от локална тестова мрежа и тя предоставя допълнителни типове. Следвайте стъпките в настройката на метаданните на PolkadotJS metadata setup (opens new window) за да извлечете метаданните на нода от неговата HTTP крайна точка.

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

или от неговата websocket крайна точка с помощта на 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

След това копирайте и поставете резултата в JSON файл. В нашия пример kitty (opens new window), създадохме api-interface/kitty.json.

# Дефиниции на типове

Предполагаме, че потребителят познава специфичните типове и RPC поддръжка от веригата, и това е дефинирано в Манифеста.

Следвайки видовете сетъпи (opens new window), създаваме :

  • src/api-interfaces/definitions.ts - това експортира всички дефиниции на подпапки
export { default as kitties } from './kitties/definitions';
1
  • src/api-interfaces/kitties/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'
        }
    }
}
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

# Пакети

  • Във файла package.json, не забравяйте да добавите @polkadot/typegen като зависимост за разработка и @polkadot/api като обикновена зависимост (в идеалния случай същата версия). Нуждаем се и от ts-node като зависимост за разработка, за да ни помогне да стартираме скриптовете.
  • Добавяме скриптове за изпълнение на двата типа; generate:defs и метадата generate:meta генератори (в този ред, така че метаданните могат да използват типовете).

Ето опростена версия на package.json. Уверете се, че в секцията scripts името на пакета е правилно и директориите са валидни.

{
  "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

# Генериране на тип

След като подготовката е завършена, ние сме готови да генерираме типове и метаданни. Изпълнете командите по-долу:

# Yarn to install new dependencies
yarn

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

Във всяка папка с модули (напр. /kitties), вече трябва да има генериран types.ts който дефинира всички интерфейси от дефинициите на тези модули, също и файл index.ts който ги експортира всички.

# Generate metadata
yarn generate:meta
1
2

Тази команда ще генерира метаданните и нов api-augment за 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"]
    }
}
1
2
3
4
5
6
7
8
9
10

# Използване

Сега във функцията за преобразуване можем да покажем как метаданните и типовете всъщност декорират 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);
}
1
2
3
4
5
6
7
8
9

Ако искате да публикувате този проект в нашия експлорър, моля, включете генерираните файлове в src/api-interfaces.

# Rpc повиквания в персонализирана верига

За да поддържаме персонализирани верижни RPC преобразувания, трябва ръчно да вкараме RPC дефиниции за typesBundle, позволявайки конфигурация по спецификация. Можете да дефинирате typesBundle в project.yml. И моля, не забравяйте, че се поддържат само повиквания тип 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",
            }
          }
        }
      }
    }
  }

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 update: August 15, 2022 23:43