Skip to content

Commit

Permalink
Merge pull request #300 from subquery/release-20240606-abi
Browse files Browse the repository at this point in the history
20240606 abi validation
  • Loading branch information
yoozo authored Jun 7, 2024
2 parents 52b9317 + 731aca8 commit bf2d3d9
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 7 deletions.
3 changes: 3 additions & 0 deletions packages/common-ethereum/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Improve ABI validation

## [3.7.0] - 2024-06-05
### Changed
- Add default value in model class to follow ES2022 rule
Expand Down
113 changes: 108 additions & 5 deletions packages/common-ethereum/src/codegen/codegen-controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
import fs from 'fs';
import path from 'path';
import {promisify} from 'util';
import {EthereumDatasourceKind, EthereumHandlerKind} from '@subql/types-ethereum';
import {EthereumDatasourceKind, EthereumHandlerKind, SubqlRuntimeDatasource} from '@subql/types-ethereum';
import ejs from 'ejs';
import {upperFirst} from 'lodash';
import rimraf from 'rimraf';
import {AbiInterface, getAbiNames, joinInputAbiName, prepareAbiJob, prepareSortedAssets} from './codegen-controller';
import {
AbiInterface,
generateAbis,
getAbiNames,
joinInputAbiName,
prepareAbiJob,
prepareSortedAssets,
} from './codegen-controller';

describe('Codegen spec', () => {
const PROJECT_PATH = path.join(__dirname, '../../test/abiTest');
Expand Down Expand Up @@ -102,9 +109,7 @@ describe('Codegen spec', () => {
abi: 'erc20',
address: '',
},
assets: {
erc20: {file: './abis/erc20.json'},
} as unknown as Map<string, {file: string}>,
assets: new Map([['erc20', {file: './abis/erc20.json'}]]),
mapping: {
file: '',
handlers: [
Expand Down Expand Up @@ -248,4 +253,102 @@ describe('Codegen spec', () => {
'registerOperatorWithCoordinator(bytes,(uint256,uint256),string,(uint8,address,(uint256,uint256))[],(bytes,bytes32,uint256))'
);
});

it('validate Abi.json path field', async () => {
const ds: SubqlRuntimeDatasource = {
kind: EthereumDatasourceKind.Runtime,
startBlock: 1,
options: {
abi: 'erc20',
address: '',
},
assets: new Map([['erc20', {file: './abis/xxx.json'}]]),
mapping: {
file: '',
handlers: [
{
handler: 'handleTransaction',
kind: EthereumHandlerKind.Call,
filter: {
function: 'transfer()',
},
},
],
},
};

await expect(generateAbis([ds], PROJECT_PATH, undefined, undefined, undefined)).rejects.toThrow(
/Asset: "erc20" not found in project/
);
});

it('validate Abi.json Function Not Exist', async () => {
const ds: SubqlRuntimeDatasource = {
kind: EthereumDatasourceKind.Runtime,
startBlock: 1,
options: {
abi: 'erc20',
address: '',
},
assets: new Map([['erc20', {file: './abis/erc20.json'}]]),
mapping: {
file: '',
handlers: [
{
handler: 'handleTransaction',
kind: EthereumHandlerKind.Call,
filter: {
function: 'approve(address a,uint256 b)',
},
},
{
handler: 'handleTransaction',
kind: EthereumHandlerKind.Call,
filter: {
function: 'approve222(address a,uint256 b)',
},
},
],
},
};

await expect(generateAbis([ds], PROJECT_PATH, undefined, undefined, undefined)).rejects.toThrow(
/Function: "approve222\(address a,uint256 b\)" not found in erc20 contract interface/
);
});

it('validate Abi.json Topic Not Exist', async () => {
const ds: SubqlRuntimeDatasource = {
kind: EthereumDatasourceKind.Runtime,
startBlock: 1,
options: {
abi: 'erc20',
address: '',
},
assets: new Map([['erc20', {file: './abis/erc20.json'}]]),
mapping: {
file: '',
handlers: [
{
handler: 'handleTransaction',
kind: EthereumHandlerKind.Event,
filter: {
topics: ['Transfer(address a,address b,uint256 c)'],
},
},
{
handler: 'handleTransaction',
kind: EthereumHandlerKind.Event,
filter: {
topics: ['Transfer(address a,address b,uint256 c)', 'NotExist(address a)'],
},
},
],
},
};

await expect(generateAbis([ds], PROJECT_PATH, undefined, undefined, undefined)).rejects.toThrow(
/Topic: "NotExist\(address a\)" not found in erc20 contract interface/
);
});
});
73 changes: 71 additions & 2 deletions packages/common-ethereum/src/codegen/codegen-controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright 2020-2024 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import assert from 'assert';
import fs from 'fs';
import path from 'path';
import {Interface, EventFragment, FunctionFragment} from '@ethersproject/abi';
import {FileReference} from '@subql/types-core';
import {SubqlRuntimeDatasource} from '@subql/types-ethereum';
import {EthereumHandlerKind, SubqlRuntimeDatasource} from '@subql/types-ethereum';
import {Data} from 'ejs';
import {runTypeChain, glob, parseContractPath} from 'typechain';
import {isCustomDs, isRuntimeDs} from '../project';
Expand Down Expand Up @@ -40,6 +42,65 @@ function validateCustomDsDs(d: {kind: string}): boolean {
return CUSTOM_EVM_HANDLERS.includes(d.kind);
}

function validateAbi(datasources: SubqlRuntimeDatasource[], projectPath: string) {
const issues: string[] = [];
for (const datasource of datasources) {
const abiName = datasource.options.abi;
const topicIssues: string[] = [];
const funcIssues: string[] = [];
const abi = datasource.assets.get(abiName);
let data = '';
try {
data = fs.readFileSync(path.join(projectPath, abi.file), 'utf8');
} catch (e) {
issues.push(`Asset: "${abiName}" not found in project`);
continue;
}
let abiObj = JSON.parse(data);
if (!Array.isArray(abiObj) && abiObj.abi) {
abiObj = (abiObj as {abi: string[]}).abi;
}

const iface = new Interface(abiObj);
const abiFunctions = Object.values(iface.functions).map((func) => func.format());
const abiEvents = Object.values(iface.events).map((event) => event.format());

for (const mappingHandler of datasource.mapping.handlers) {
if (!mappingHandler?.filter) continue;

if (mappingHandler.kind === EthereumHandlerKind.Event) {
const notMatch = mappingHandler.filter.topics.find(
(topic) => !abiEvents.includes(EventFragment.fromString(topic).format())
);

if (notMatch) topicIssues.push(notMatch);
}

if (mappingHandler.kind === EthereumHandlerKind.Call) {
const functionFormat = FunctionFragment.fromString(mappingHandler.filter.function).format();
if (!abiFunctions.includes(functionFormat)) funcIssues.push(mappingHandler.filter.function);
}
}

if (topicIssues.length) {
issues.push(
`Topic: "${topicIssues.join(
', '
)}" not found in ${abiName} contract interface, supported topics: ${abiEvents.join(', ')}`
);
}
if (funcIssues.length) {
issues.push(
`Function: "${funcIssues.join(
', '
)}" not found in ${abiName} contract interface, supported functions: ${abiFunctions.join(', ')}`
);
}
}

assert(issues.length === 0, issues.join('\n'));
}

export function joinInputAbiName(abiObject: AbiInterface): string {
// example: "TextChanged_bytes32_string_string_string_Event", Event name/Function type name will be joined in ejs
const inputToSnake = abiObject.inputs.map((obj) => obj.type.replace(/\[\]/g, '_arr').toLowerCase()).join('_');
Expand Down Expand Up @@ -89,7 +150,7 @@ export function prepareSortedAssets(
}
} else {
Object.entries(d.assets).map(([name, value]) => {
addAsset(name, value as any);
addAsset(name, value as FileReference);
});
}
});
Expand Down Expand Up @@ -172,6 +233,14 @@ export async function generateAbis(
upperFirst: (input?: string) => string,
renderTemplate: (templatePath: string, outputPath: string, templateData: Data) => Promise<void>
): Promise<void> {
// @subql/cli package calls this function with datasources as an array of objects
datasources = datasources.map((d) => ({
...d,
assets: d.assets instanceof Map ? d.assets : new Map(Object.entries(d.assets)),
})) as SubqlRuntimeDatasource[];

validateAbi(datasources, projectPath);

const sortedAssets = prepareSortedAssets(datasources, projectPath);

if (Object.keys(sortedAssets).length === 0) {
Expand Down

0 comments on commit bf2d3d9

Please sign in to comment.