Skip to content

Commit

Permalink
Abi codegen (#1532)
Browse files Browse the repository at this point in the history
* draft

* fix abi name, add support for transaction

* Update packages/cli/src/template/abi-interface.ts.ejs

Co-authored-by: Scott Twiname <[email protected]>

* fix

* fix dependencies

---------

Co-authored-by: Scott Twiname <[email protected]>
  • Loading branch information
jiqiang90 and stwiname authored Feb 28, 2023
1 parent abd627f commit c27d5e2
Show file tree
Hide file tree
Showing 4 changed files with 390 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@subql/common-terra": "latest",
"@subql/utils": "workspace:*",
"@subql/validator": "workspace:*",
"@typechain/ethers-v5": "10.2.0",
"@types/ejs": "^3.1.0",
"@types/inquirer": "^8.2.0",
"algosdk": "^1.19.0",
Expand All @@ -37,6 +38,7 @@
"simple-git": "^3.12.0",
"ts-loader": "^9.2.6",
"tslib": "^2.3.1",
"typechain": "8.1.1",
"webpack": "^5.68.0",
"webpack-merge": "^5.8.0",
"websocket": "^1.0.34",
Expand Down
127 changes: 127 additions & 0 deletions packages/cli/src/controller/codegen-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
isRuntimeDs as isRuntimeEthereumDs,
RuntimeDatasourceTemplate as EthereumDsTemplate,
CustomDatasourceTemplate as EthereumCustomDsTemplate,
RuntimeDataSourceV0_3_0 as EthereumDs,
CustomDatasourceV0_3_0 as EthereumCustomDs,
} from '@subql/common-ethereum';
import {
isCustomDs as isCustomNearDs,
Expand All @@ -33,6 +35,7 @@ import {
isCustomDs as isCustomSubstrateDs,
RuntimeDatasourceTemplate as SubstrateDsTemplate,
CustomDatasourceTemplate as SubstrateCustomDsTemplate,
CustomDatasourceV0_2_0 as SubstrateCustomDatasource,
} from '@subql/common-substrate';
import {
isCustomTerraDs,
Expand All @@ -53,6 +56,7 @@ import {
import ejs from 'ejs';
import {upperFirst, uniq} from 'lodash';
import rimraf from 'rimraf';
import {runTypeChain, glob, parseContractPath} from 'typechain';

type TemplateKind =
| SubstrateDsTemplate
Expand All @@ -67,14 +71,22 @@ type TemplateKind =
| NearCustomDsTemplate
| TerraDsTemplate
| TerraCustomDsTemplate;

type DatasourceKind = SubstrateCustomDatasource | EthereumDs | EthereumCustomDs;

const MODEL_TEMPLATE_PATH = path.resolve(__dirname, '../template/model.ts.ejs');
const MODELS_INDEX_TEMPLATE_PATH = path.resolve(__dirname, '../template/models-index.ts.ejs');
const TYPES_INDEX_TEMPLATE_PATH = path.resolve(__dirname, '../template/types-index.ts.ejs');
const INTERFACE_TEMPLATE_PATH = path.resolve(__dirname, '../template/interface.ts.ejs');
const ABI_INTERFACE_TEMPLATE_PATH = path.resolve(__dirname, '../template/abi-interface.ts.ejs');
const ENUM_TEMPLATE_PATH = path.resolve(__dirname, '../template/enum.ts.ejs');
const DYNAMIC_DATASOURCE_TEMPLATE_PATH = path.resolve(__dirname, '../template/datasource-templates.ts.ejs');
const TYPE_ROOT_DIR = 'src/types';
const MODEL_ROOT_DIR = 'src/types/models';
const ABI_INTERFACES_ROOT_DIR = 'src/types/abi-interfaces';
const CONTRACTS_DIR = 'src/types/contracts'; //generated
const TYPECHAIN_TARGET = 'ethers-v5';

const exportTypes = {
models: false,
interfaces: false,
Expand Down Expand Up @@ -155,6 +167,118 @@ export async function generateEnums(projectPath: string, schema: string): Promis
}
}

interface abiRenderProps {
name: string;
events: string[];
functions: {typeName: string; functionName: string}[];
}
interface abiInterface {
name: string;
type: 'event' | 'function';
inputs: {
internalType: string;
name: string;
type: string;
}[];
}
export async function generateAbis(datasources: DatasourceKind[], projectPath: string): Promise<void> {
const sortedAssets = new Map<string, string>();
datasources.map((d) => {
if (!d?.assets || !isRuntimeEthereumDs(d) || !isCustomEthereumDs(d) || !isCustomSubstrateDs(d)) {
return;
}
Object.entries(d.assets).map(([name, value]) => {
const filePath = path.join(projectPath, value.file);
if (!fs.existsSync(filePath)) {
throw new Error(`Error: Asset ${name}, file ${value.file} does not exist`);
}
// We use actual abi file name instead on name provided in assets
// This is aligning with files in './ethers-contracts'
sortedAssets.set(parseContractPath(filePath).name, value.file);
});
});
if (sortedAssets.size !== 0) {
await prepareDirPath(path.join(projectPath, ABI_INTERFACES_ROOT_DIR), true);
try {
const allFiles = glob(projectPath, [...sortedAssets.values()]);
// Typechain generate interfaces under CONTRACTS_DIR
await runTypeChain({
cwd: projectPath,
filesToProcess: allFiles,
allFiles,
outDir: CONTRACTS_DIR,
target: TYPECHAIN_TARGET,
});
// Iterate here as we have to make sure type chain generated successful,
// also avoid duplicate generate same abi interfaces
const renderAbiJobs = processAbis(sortedAssets, projectPath);
await Promise.all(
renderAbiJobs.map((renderProps) => {
console.log(`* Abi Interface ${renderProps.name} generated`);
return renderTemplate(
ABI_INTERFACE_TEMPLATE_PATH,
path.join(projectPath, ABI_INTERFACES_ROOT_DIR, `${renderProps.name}.ts`),
{
props: {abi: renderProps},
helper: {upperFirst},
}
);
})
);
} catch (e) {
throw new Error(`When render abi interface having problems.`);
}
}
}

function processAbis(sortedAssets: Map<string, string>, projectPath: string): abiRenderProps[] {
const renderInterfaceJobs: abiRenderProps[] = [];
sortedAssets.forEach((value, key) => {
const renderProps: abiRenderProps = {name: key, events: [], functions: []};
const readAbi = loadFromJsonOrYaml(path.join(projectPath, value)) as abiInterface[];
// We need to use for loop instead of map, due to events/function name could be duplicate,
// because they have different input, and following ether typegen rules, name also changed
// we need to find duplicates, and update its name rather than just unify them.
const duplicateEventNames = readAbi
.filter((abiObject) => abiObject.type === 'event')
.map((obj) => obj.name)
.filter((name, index, arr) => arr.indexOf(name) !== index);
const duplicateFunctionNames = readAbi
.filter((abiObject) => abiObject.type === 'function')
.map((obj) => obj.name)
.filter((name, index, arr) => arr.indexOf(name) !== index);
readAbi.map((abiObject) => {
if (abiObject.type === 'function') {
let typeName = abiObject.name;
let functionName = abiObject.name;
if (duplicateFunctionNames.includes(abiObject.name)) {
functionName = `${abiObject.name}(${abiObject.inputs.map((obj) => obj.type.toLowerCase()).join(',')})`;
typeName = joinInputAbiName(abiObject);
}
renderProps.functions.push({typeName, functionName});
}
if (abiObject.type === 'event') {
let name = abiObject.name;
if (duplicateEventNames.includes(abiObject.name)) {
name = joinInputAbiName(abiObject);
}
renderProps.events.push(name);
}
});
// avoid empty json
if (!!renderProps.events || !!renderProps.functions) {
renderInterfaceJobs.push(renderProps);
}
});
return renderInterfaceJobs;
}

function joinInputAbiName(abiObject: abiInterface) {
// example: "TextChanged_bytes32_string_string_string_Event", Event name/Function type name will be joined in ejs
const inputToSnake: string = abiObject.inputs.map((obj) => obj.type.toLowerCase()).join('_');
return `${abiObject.name}_${inputToSnake}_`;
}

export function processFields(
type: 'entity' | 'jsonField',
className: string,
Expand Down Expand Up @@ -240,12 +364,15 @@ export async function codegen(projectPath: string, fileName?: string): Promise<v
const plainManifest = loadFromJsonOrYaml(getManifestPath(projectPath, fileName)) as {
specVersion: string;
templates?: TemplateKind[];
dataSources: DatasourceKind[];
};
if (plainManifest.templates && plainManifest.templates.length !== 0) {
await generateDatasourceTemplates(projectPath, plainManifest.specVersion, plainManifest.templates);
}

const schemaPath = getSchemaPath(projectPath, fileName);

await generateAbis(plainManifest.dataSources, projectPath);
await generateJsonInterfaces(projectPath, schemaPath);
await generateModels(projectPath, schemaPath);
await generateEnums(projectPath, schemaPath);
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/template/abi-interface.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0

// Auto-generated , DO NOT EDIT
import {EthereumLog, EthereumTransaction} from "@subql/types-ethereum";

import {
<% props.abi.events.forEach(function(event){ %>
<%= event %>Event,
<% });
%>
<%=props.abi.name%>
} from '../contracts/<%=props.abi.name%>'

<% props.abi.events.forEach(function(event){ %>
export type <%=helper.upperFirst(event) %>Log = EthereumLog<<%=event%>Event["args"]>
<% }); %>
<% props.abi.functions.forEach(function(_function){ %>
export type <%=helper.upperFirst(_function.typeName) %>Transaction = EthereumTransaction<Parameters<<%=props.abi.name%>['functions']['<%=_function.functionName%>']>>
<% }); %>


Loading

0 comments on commit c27d5e2

Please sign in to comment.