Support artifacts generation for multiple contracts (#82)

This commit is contained in:
prathamesh0 2021-12-21 13:50:22 +05:30 committed by nabarun
parent 3638d56787
commit 5b12db541b
8 changed files with 112 additions and 71 deletions

View File

@ -21,11 +21,11 @@
* Run the following command to generate a watcher from a contract file:
```bash
yarn codegen --input-file <input-file-path> --contract-name <contract-name> --output-folder [output-folder] --mode [eth_call | storage | all] --flatten [true | false] --kind [lazy | active] --port [server-port] --subgraph-path [subgraph-build-path]
yarn codegen --input-files <input-file-paths> --contract-names <contract-names> --output-folder [output-folder] --mode [eth_call | storage | all] --flatten [true | false] --kind [lazy | active] --port [server-port] --subgraph-path [subgraph-build-path]
```
* `input-file`(alias: `i`): Input contract file path or an URL (required).
* `contract-name`(alias: `c`): Main contract name (required).
* `input-files`(alias: `i`): Input contract file path(s) or URL(s) (required).
* `contract-names`(alias: `c`): Contract name(s) (in order of `input-files`) (required).
* `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided).
* `mode`(alias: `m`): Code generation mode (default: `all`).
* `flatten`(alias: `f`): Flatten the input contract file (default: `true`).
@ -33,36 +33,42 @@
* `port` (alias: `p`): Server port (default: `3008`).
* `subgraph-path` (alias: `s`): Path to the subgraph build.
**Note**: When passed an *URL* as `input-file`, it is assumed that it points to an already flattened contract file.
**Note**: When passed an *URL* in `input-files`, it is assumed that it points to an already flattened contract file.
Examples:
Generate code in `storage` mode, `lazy` kind.
```bash
yarn codegen --input-file ./test/examples/contracts/ERC721.sol --contract-name ERC721 --output-folder ../my-erc721-watcher --mode storage --kind lazy
yarn codegen --input-files ./test/examples/contracts/ERC721.sol --contract-names ERC721 --output-folder ../my-erc721-watcher --mode storage --kind lazy
```
Generate code in `eth_call` mode using a contract provided by an URL.
```bash
yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../my-erc721-watcher --mode eth_call
yarn codegen --input-files https://git.io/Jupci --contract-names ERC721 --output-folder ../my-erc721-watcher --mode eth_call
```
Generate code for `ERC721` in both `eth_call` and `storage` mode, `active` kind.
```bash
yarn codegen --input-file ../../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol --contract-name ERC721 --output-folder ../demo-erc721-watcher --mode all --kind active
yarn codegen --input-files ../../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol --contract-names ERC721 --output-folder ../demo-erc721-watcher --mode all --kind active
```
Generate code for `ERC20` contract in both `eth_call` and `storage` mode, `active` kind:
```bash
yarn codegen --input-file ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --contract-name ERC20 --output-folder ../demo-erc20-watcher --mode all --kind active
yarn codegen --input-files ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --contract-names ERC20 --output-folder ../demo-erc20-watcher --mode all --kind active
```
This will create a folder called `demo-erc20-watcher` containing the generated code at the specified path. Follow the steps in [Run Generated Watcher](#run-generated-watcher) to setup and run the generated watcher.
Generate code for `Eden` contracts in `storage` mode, `active` kind:
```bash
yarn codegen --input-files ~/vulcanize/governance/contracts/EdenNetwork.sol ~/vulcanize/governance/contracts/MerkleDistributor.sol ~/vulcanize/governance/contracts/DistributorGovernance.sol --contract-names EdenNetwork MerkleDistributor DistributorGovernance --output-folder ../demo-eden-watcher --mode storage --kind active --subgraph-path ~/vulcanize/eden-data/packages/subgraph/build
```
## Run Generated Watcher
### Setup

View File

@ -7,6 +7,8 @@ import fetch from 'node-fetch';
import path from 'path';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import assert from 'assert';
import { Writable } from 'stream';
import { flatten } from '@poanet/solidity-flattener';
import { parse, visit } from '@solidity-parser/parser';
@ -35,17 +37,17 @@ import { exportInspectCID } from './inspect-cid';
const main = async (): Promise<void> => {
const argv = await yargs(hideBin(process.argv))
.option('input-file', {
.option('input-files', {
alias: 'i',
demandOption: true,
describe: 'Input contract file path or an url.',
type: 'string'
describe: 'Input contract file path(s) or url(s).',
type: 'array'
})
.option('contract-name', {
.option('contract-names', {
alias: 'c',
demandOption: true,
describe: 'Main contract name.',
type: 'string'
describe: 'Contract name(s).',
type: 'array'
})
.option('output-folder', {
alias: 'o',
@ -85,47 +87,57 @@ const main = async (): Promise<void> => {
})
.argv;
let data: string;
if (argv['input-file'].startsWith('http')) {
// Assume flattened file in case of URL.
const response = await fetch(argv['input-file']);
data = await response.text();
} else {
data = argv.flatten
? await flatten(path.resolve(argv['input-file']))
: fs.readFileSync(path.resolve(argv['input-file'])).toString();
// Create an array of flattened contract strings.
const contractStrings: string[] = [];
for (const inputFile of argv['input-files']) {
assert(typeof inputFile === 'string', 'Input file path should be a string');
if (inputFile.startsWith('http')) {
// Assume flattened file in case of URL.
const response = await fetch(inputFile);
const contractString = await response.text();
contractStrings.push(contractString);
} else {
contractStrings.push(argv.flatten
? await flatten(path.resolve(inputFile))
: fs.readFileSync(path.resolve(inputFile)).toString()
);
}
}
const visitor = new Visitor();
parseAndVisit(data, visitor, argv.mode);
parseAndVisit(contractStrings, visitor, argv.mode);
generateWatcher(data, visitor, argv);
generateWatcher(contractStrings, visitor, argv);
};
function parseAndVisit (data: string, visitor: Visitor, mode: string) {
// Get the abstract syntax tree for the flattened contract.
const ast = parse(data);
function parseAndVisit (contractStrings: string[], visitor: Visitor, mode: string) {
for (const contractString of contractStrings) {
// Get the abstract syntax tree for the flattened contract.
const ast = parse(contractString);
// Filter out library nodes.
ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library'));
// Filter out library nodes.
ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library'));
if ([MODE_ALL, MODE_ETH_CALL].some(value => value === mode)) {
visit(ast, {
FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor),
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
});
}
if ([MODE_ALL, MODE_ETH_CALL].includes(mode)) {
visit(ast, {
FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor),
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
});
}
if ([MODE_ALL, MODE_STORAGE].some(value => value === mode)) {
visit(ast, {
StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor),
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
});
if ([MODE_ALL, MODE_STORAGE].includes(mode)) {
visit(ast, {
StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor),
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
});
}
}
}
function generateWatcher (data: string, visitor: Visitor, argv: any) {
function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any) {
// Prepare directory structure for the watcher.
let outputDir = '';
if (argv['output-folder']) {
@ -145,13 +157,34 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
if (!fs.existsSync(resetCmdsFolder)) fs.mkdirSync(resetCmdsFolder, { recursive: true });
}
const inputFileName = path.basename(argv['input-file'], '.sol');
let outStream: Writable;
const contractNames = argv['contract-names'];
const inputFileNames: string[] = [];
// Export artifacts for the contracts.
argv['input-files'].forEach((inputFile: string, index: number) => {
const inputFileName = path.basename(inputFile, '.sol');
inputFileNames.push(inputFileName);
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${inputFileName}.json`))
: process.stdout;
exportArtifacts(
outStream,
contractStrings[index],
`${inputFileName}.sol`,
contractNames[index]
);
});
// Register the handlebar helpers to be used in the templates.
registerHandlebarHelpers();
visitor.visitSubgraph(argv['subgraph-path']);
let outStream = outputDir
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/schema.gql'))
: process.stdout;
const schemaContent = visitor.exportSchema(outStream);
@ -164,7 +197,7 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/indexer.ts'))
: process.stdout;
visitor.exportIndexer(outStream, inputFileName, argv['contract-name']);
visitor.exportIndexer(outStream, inputFileNames);
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/server.ts'))
@ -176,16 +209,6 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
: process.stdout;
exportConfig(argv.kind, argv.port, path.basename(outputDir), outStream, argv['subgraph-path']);
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${inputFileName}.json`))
: process.stdout;
exportArtifacts(
outStream,
data,
`${inputFileName}.sol`,
argv['contract-name']
);
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/database.ts'))
: process.stdout;
@ -242,6 +265,7 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
exportFill(outStream);
let rcOutStream, ignoreOutStream;
if (outputDir) {
rcOutStream = fs.createWriteStream(path.join(outputDir, '.eslintrc.json'));
ignoreOutStream = fs.createWriteStream(path.join(outputDir, '.eslintignore'));
@ -249,6 +273,7 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
rcOutStream = process.stdout;
ignoreOutStream = process.stdout;
}
exportLint(rcOutStream, ignoreOutStream);
outStream = outputDir
@ -257,6 +282,7 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
visitor.exportClient(outStream, schemaContent, path.join(outputDir, 'src/gql'));
let resetOutStream, resetJQOutStream, resetStateOutStream;
if (outputDir) {
resetOutStream = fs.createWriteStream(path.join(outputDir, 'src/cli/reset.ts'));
resetJQOutStream = fs.createWriteStream(path.join(outputDir, 'src/cli/reset-cmds/job-queue.ts'));
@ -266,6 +292,7 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
resetJQOutStream = process.stdout;
resetStateOutStream = process.stdout;
}
visitor.exportReset(resetOutStream, resetJQOutStream, resetStateOutStream);
outStream = outputDir

View File

@ -104,12 +104,11 @@ export class Indexer {
* @param outStream A writable output stream to write the indexer file to.
* @param inputFileName Input contract file name to be passed to the template.
*/
exportIndexer (outStream: Writable, inputFileName: string, contractName: string): void {
exportIndexer (outStream: Writable, inputFileNames: string[]): void {
const template = Handlebars.compile(this._templateString);
const obj = {
inputFileName,
contractName,
inputFileNames,
queries: this._queries,
constants: {
MODE_ETH_CALL,

View File

@ -36,7 +36,7 @@ export class Schema {
// TODO: Handle cases where returnType/params type is an array.
const tsReturnType = getTsForSol(returnType);
assert(tsReturnType);
assert(tsReturnType, `ts type for sol type ${returnType} for ${name} not found`);
const queryObject: { [key: string]: any; } = {};
queryObject[name] = {
@ -51,7 +51,7 @@ export class Schema {
if (params.length > 0) {
queryObject[name].args = params.reduce((acc, curr) => {
const tsCurrType = getTsForSol(curr.type);
assert(tsCurrType);
assert(tsCurrType, `ts type for sol type ${curr.type} for ${curr.name} not found`);
acc[curr.name] = `${getGqlForTs(tsCurrType)}!`;
return acc;
}, queryObject[name].args);
@ -81,7 +81,7 @@ export class Schema {
if (params.length > 0) {
typeObject.fields = params.reduce((acc, curr) => {
const tsCurrType = getTsForSol(curr.type);
assert(tsCurrType);
assert(tsCurrType, `ts type for sol type ${curr.type} for ${curr.name} not found`);
acc[curr.name] = `${getGqlForTs(tsCurrType)}!`;
return acc;
}, typeObject.fields);

View File

@ -26,7 +26,11 @@ import { SyncStatus } from './entity/SyncStatus';
import { HookStatus } from './entity/HookStatus';
import { BlockProgress } from './entity/BlockProgress';
import { IPLDBlock } from './entity/IPLDBlock';
{{#each inputFileNames as | inputFileName |}}
import artifacts from './artifacts/{{inputFileName}}.json';
{{/each}}
import { createInitialCheckpoint, handleEvent, createStateDiff, createStateCheckpoint } from './hooks';
import { IPFSClient } from './ipfs';

View File

@ -11,10 +11,15 @@ const _gqlToTs: Map<string, string> = new Map();
// Solidity to Typescript type-mapping.
_solToTs.set('string', 'string');
_solToTs.set('uint8', 'number');
_solToTs.set('uint16', 'number');
_solToTs.set('uint64', 'bigint');
_solToTs.set('uint128', 'bigint');
_solToTs.set('uint256', 'bigint');
_solToTs.set('address', 'string');
_solToTs.set('bool', 'boolean');
_solToTs.set('bytes', 'string');
_solToTs.set('bytes4', 'string');
_solToTs.set('bytes32', 'string');
// Typescript to Graphql type-mapping.
_tsToGql.set('string', 'String');

View File

@ -140,10 +140,10 @@ export class Visitor {
/**
* Writes the indexer file generated from a template to a stream.
* @param outStream A writable output stream to write the indexer file to.
* @param inputFileName Input contract file name to be passed to the template.
* @param inputFileName Input contract file names to be passed to the template.
*/
exportIndexer (outStream: Writable, inputFileName: string, contractName: string): void {
this._indexer.exportIndexer(outStream, inputFileName, contractName);
exportIndexer (outStream: Writable, inputFileNames: string[]): void {
this._indexer.exportIndexer(outStream, inputFileNames);
}
/**

View File

@ -2032,7 +2032,7 @@
"@poanet/solidity-flattener@https://github.com/vulcanize/solidity-flattener.git":
version "3.0.6"
resolved "https://github.com/vulcanize/solidity-flattener.git#9c8a22b9c43515be306646a177a43636fd95aaae"
resolved "https://github.com/vulcanize/solidity-flattener.git#144ef6cda8823f4a5e48cb4f615be87a32e2dcbc"
dependencies:
bunyan "^1.8.12"
decomment "^0.9.1"
@ -2431,9 +2431,9 @@
"@types/node" "*"
"@types/glob@*":
version "7.1.4"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672"
integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
dependencies:
"@types/minimatch" "*"
"@types/node" "*"
@ -5508,9 +5508,9 @@ decode-uri-component@^0.2.0:
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
decomment@^0.9.1:
version "0.9.4"
resolved "https://registry.yarnpkg.com/decomment/-/decomment-0.9.4.tgz#fa40335bd90e3826d5c1984276e390525ff856d5"
integrity sha512-8eNlhyI5cSU4UbBlrtagWpR03dqXcE5IR9zpe7PnO6UzReXDskucsD8usgrzUmQ6qJ3N82aws/p/mu/jqbURWw==
version "0.9.5"
resolved "https://registry.yarnpkg.com/decomment/-/decomment-0.9.5.tgz#61753c80b8949620eb6bc3f8246cc0e2720ceac1"
integrity sha512-h0TZ8t6Dp49duwyDHo3iw67mnh9/UpFiSSiOb5gDK1sqoXzrfX/SQxIUQd2R2QEiSnqib0KF2fnKnGfAhAs6lg==
dependencies:
esprima "4.0.1"