From 18ccc8fdbb622053ada9a1f8c89472bf2023e85c Mon Sep 17 00:00:00 2001 From: JQQQ Date: Sun, 19 Jan 2025 14:58:52 +1300 Subject: [PATCH] update starknet dictionary logic --- .../v1/starknetDictionaryV1.spec.ts | 208 ++++++++++++++++++ .../dictionary/v1/starknetDictionaryV1.ts | 80 +++---- packages/node/src/starknet/utils.starknet.ts | 25 ++- 3 files changed, 258 insertions(+), 55 deletions(-) create mode 100644 packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.spec.ts diff --git a/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.spec.ts b/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.spec.ts new file mode 100644 index 0000000..d6dd130 --- /dev/null +++ b/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.spec.ts @@ -0,0 +1,208 @@ +// Copyright 2020-2024 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import { + StarknetDatasourceKind, + StarknetHandlerKind, + StarknetRuntimeDatasource, +} from '@subql/types-starknet'; +import { StarknetProjectDsTemplate } from '../../../configure/SubqueryProject'; +import { buildDictionaryV1QueryEntries } from './starknetDictionaryV1'; + +const mockTempDs: StarknetProjectDsTemplate[] = [ + { + name: 'ZkLend', + kind: StarknetDatasourceKind.Runtime, + assets: new Map(), + options: { + // Must be a key of assets + abi: 'zkLend', + // # this is the contract address for zkLend market https://starkscan.co/contract/0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05 + address: + '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + }, + mapping: { + file: '', + handlers: [ + { + kind: StarknetHandlerKind.Call, + handler: 'handleTransaction', + filter: { + to: '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + type: 'INVOKE', + /** + * The function can either be the function fragment or signature + * function: 'withdraw' + * function: '0x015511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77' + */ + function: 'withdraw', + }, + }, + { + kind: StarknetHandlerKind.Event, + handler: 'handleLog', + filter: { + /** + * Follows standard log filters for Starknet + * zkLend address: "0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05" + */ + topics: [ + 'Deposit', //0x9149d2123147c5f43d258257fef0b7b969db78269369ebcf5ebb9eef8592f2 + ], + }, + }, + ], + }, + }, +]; + +describe('buildDictionaryV1QueryEntries', () => { + describe('Log filters', () => { + it('Build filter for logs', () => { + const ds: StarknetRuntimeDatasource = { + kind: StarknetDatasourceKind.Runtime, + assets: new Map(), + options: { + // Must be a key of assets + abi: 'zkLend', + // # this is the contract address for zkLend market https://starkscan.co/contract/0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05 + address: + '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + }, + mapping: { + file: '', + handlers: [ + { + kind: StarknetHandlerKind.Event, + handler: 'handleLog', + filter: { + /** + * Follows standard log filters for Starknet + * zkLend address: "0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05" + */ + topics: [ + 'Deposit', //0x9149d2123147c5f43d258257fef0b7b969db78269369ebcf5ebb9eef8592f2 + ], + }, + }, + ], + }, + }; + + const result = buildDictionaryV1QueryEntries([ds]); + + expect(result).toEqual([ + { + conditions: [ + { + field: 'address', + matcher: 'equalTo', + value: + '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + }, + { + field: 'topics', + matcher: 'contains', + value: [ + '0x9149d2123147c5f43d258257fef0b7b969db78269369ebcf5ebb9eef8592f2', + ], + }, + ], + entity: 'logs', + }, + ]); + }); + }); + describe('Transaction filters', () => { + it('Build a filter for contract type', () => { + const ds: StarknetRuntimeDatasource = { + kind: StarknetDatasourceKind.Runtime, + assets: new Map(), + startBlock: 1, + mapping: { + file: '', + handlers: [ + { + handler: 'handleTransaction', + kind: StarknetHandlerKind.Call, + filter: { + type: 'L1_HANDLER', + }, + }, + ], + }, + }; + + const result = buildDictionaryV1QueryEntries([ds]); + + expect(result).toEqual([ + [ + { + conditions: [ + { + field: 'type', + matcher: 'equalTo', + value: 'L1_HANDLER', + }, + ], + entity: 'calls', + }, + ], + ]); + }); + + it('Build a filter with include ds option and contract address', () => { + const ds: StarknetRuntimeDatasource = { + kind: StarknetDatasourceKind.Runtime, + assets: new Map(), + options: { + // Must be a key of assets + abi: 'zkLend', + // # this is the contract address for zkLend market https://starkscan.co/contract/0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05 + address: + '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + }, + mapping: { + file: '', + handlers: [ + { + kind: StarknetHandlerKind.Call, + handler: 'handleTransaction', + filter: { + to: '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + // type: "INVOKE", + /** + * The function can either be the function fragment or signature + * function: 'withdraw' + * function: '0x015511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77' + */ + function: 'withdraw', + }, + }, + ], + }, + }; + + const result = buildDictionaryV1QueryEntries([ds]); + expect(result).toEqual([ + { + conditions: [ + { + field: 'to', + matcher: 'equalTo', + value: + '0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05', + }, + { + field: 'func', + matcher: 'equalTo', + value: + '0x15511cc3694f64379908437d6d64458dc76d02482052bfb8a5b33a72c054c77', + }, + ], + entity: 'calls', + }, + ]); + }); + }); +}); diff --git a/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.ts b/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.ts index 48e0391..e056d8f 100644 --- a/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.ts +++ b/packages/node/src/indexer/dictionary/v1/starknetDictionaryV1.ts @@ -1,7 +1,6 @@ // Copyright 2020-2024 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: GPL-3.0 -import { NOT_NULL_FILTER } from '@subql/common-starknet'; import { NodeConfig, DictionaryV1, getLogger } from '@subql/node-core'; import { DictionaryQueryCondition, @@ -13,21 +12,16 @@ import { StarknetTransactionFilter, SubqlDatasource, } from '@subql/types-starknet'; -import JSON5 from 'json5'; import { sortBy, uniqBy } from 'lodash'; -import fetch from 'node-fetch'; +import { num } from 'starknet'; import { StarknetProjectDs, StarknetProjectDsTemplate, SubqueryProject, } from '../../../configure/SubqueryProject'; -import { encodeSelectorToHex } from '../../../starknet/utils.starknet'; +import { encodeSelectorToHex, hexEq } from '../../../starknet/utils.starknet'; import { yargsOptions } from '../../../yargs'; import { groupedDataSources, validAddresses } from '../utils'; - -const CHAIN_ALIASES_URL = - 'https://raw.githubusercontent.com/subquery/templates/main/chainAliases.json5'; - const logger = getLogger('dictionary-v1'); // Adds the addresses to the query conditions if valid @@ -69,31 +63,19 @@ function eventFilterToQueryEntry( ): DictionaryV1QueryEntry { const conditions: DictionaryQueryCondition[] = []; applyAddresses(conditions, addresses); + // No null not needed, can use [] instead if (filter?.topics) { - for (let i = 0; i < Math.min(filter.topics.length, 4); i++) { - const topic = filter.topics[i]; - if (!topic) { - continue; - } - const field = `topics${i}`; - - if (topic === NOT_NULL_FILTER) { - conditions.push({ - field, - value: false, - matcher: 'isNull', - }); - } else { - conditions.push({ - field, - value: encodeSelectorToHex(topic), - matcher: 'equalTo', - }); - } - } + const hexTopics: string[] = filter.topics + .filter((topic) => topic !== null && topic !== undefined) + .map((topic) => (num.isHex(topic) ? topic : encodeSelectorToHex(topic))); + conditions.push({ + field: 'topics', + value: hexTopics, + matcher: 'contains', + }); } return { - entity: 'evmLogs', + entity: 'logs', conditions, }; } @@ -110,14 +92,12 @@ function callFilterToQueryEntry( condition.field = 'to'; } } - if (!filter) { return { - entity: 'evmTransactions', + entity: 'calls', conditions, }; } - if (filter.from) { conditions.push({ field: 'from', @@ -125,6 +105,14 @@ function callFilterToQueryEntry( matcher: 'equalTo', }); } + if (filter.type) { + conditions.push({ + field: 'type', + value: filter.type, + matcher: 'equalTo', + }); + } + const optionsAddresses = conditions.find((c) => c.field === 'to'); if (!optionsAddresses) { if (filter.to) { @@ -145,7 +133,6 @@ function callFilterToQueryEntry( `TransactionFilter 'to' conflict with 'address' in data source options`, ); } - if (filter.function === null || filter.function === '0x') { conditions.push({ field: 'func', @@ -155,12 +142,14 @@ function callFilterToQueryEntry( } else if (filter.function) { conditions.push({ field: 'func', - value: filter.function, + value: num.isHex(filter.function) + ? filter.function + : encodeSelectorToHex(filter.function), matcher: 'equalTo', }); } return { - entity: 'evmTransactions', + entity: 'calls', conditions, }; } @@ -221,9 +210,8 @@ export class StarknetDictionaryV1 extends DictionaryV1 { project: SubqueryProject, nodeConfig: NodeConfig, dictionaryUrl: string, - chainId?: string, ) { - super(dictionaryUrl, chainId ?? project.network.chainId, nodeConfig); + super(dictionaryUrl, project.network.chainId, nodeConfig); } static async create( @@ -231,31 +219,15 @@ export class StarknetDictionaryV1 extends DictionaryV1 { nodeConfig: NodeConfig, dictionaryUrl: string, ): Promise { - /*Some dictionarys for EVM are built with other SDKs as they are chains with an EVM runtime - * we maintain a list of aliases so we can map the evmChainId to the genesis hash of the other SDKs - * e.g moonbeam is built with Substrate SDK but can be used as an EVM dictionary - */ - const chainAliases = await this.getEvmChainId(); - const chainAlias = chainAliases[project.network.chainId]; - const dictionary = new StarknetDictionaryV1( project, nodeConfig, dictionaryUrl, - chainAlias, ); await dictionary.init(); return dictionary; } - private static async getEvmChainId(): Promise> { - const response = await fetch(CHAIN_ALIASES_URL); - - const raw = await response.text(); - // We use JSON5 here because the file has comments in it - return JSON5.parse(raw); - } - buildDictionaryQueryEntries( // Add name to datasource as templates have this set dataSources: (StarknetProjectDs | StarknetProjectDsTemplate)[], diff --git a/packages/node/src/starknet/utils.starknet.ts b/packages/node/src/starknet/utils.starknet.ts index 0c5f3f1..a86ef4e 100644 --- a/packages/node/src/starknet/utils.starknet.ts +++ b/packages/node/src/starknet/utils.starknet.ts @@ -10,7 +10,6 @@ import { StarknetContractCall, StarknetLog, StarknetLogRaw, - StarknetResult, StarknetTransaction, } from '@subql/types-starknet'; import { omit } from 'lodash'; @@ -85,6 +84,30 @@ export function formatLog( return formattedLog as unknown as StarknetLog; } +/*** + * @param tx + * @param block + * @param txIndex + * Explanation for from, to, selector, calldata with different tx type + * When apply filter please refer to the following: + * + * 1. L1_HANDLER + * from is the Contract Address (contract been called) + * entryPointSelector (method) + * within decodedCalls, to is same as from, selector is the entryPointSelector + * 2. DEPLOY_ACCOUNT + * from is the contract_address, also is the new account address + * 3. DECLARE + * from is the sender_address + * 4. DEPLOY + * from is the sender_address + * 5. INVOKE V1 and V3 + * from is the sender_address + * within decodedCalls, to is the contract been called, selector is the method + * 6. INVOKE V0 + * from is the Contract Address (contract been called) + * entryPointSelector is the method been called + */ export function formatTransaction( tx: Record, block: StarknetBlock,