Generate schema from ABI when using subgraph path (#101)

This commit is contained in:
nikugogoi 2021-12-28 15:56:28 +05:30 committed by nabarun
parent a267058d51
commit 561c2c9066
6 changed files with 128 additions and 99 deletions

View File

@ -23,6 +23,7 @@
```yaml ```yaml
# Example config.yaml # Example config.yaml
# Contracts to watch (required). # Contracts to watch (required).
# Can pass empty array ([]) when using subgraphPath.
contracts: contracts:
# Contract name. # Contract name.
- name: Example - name: Example
@ -34,8 +35,8 @@
# Output folder path (logs output using `stdout` if not provided). # Output folder path (logs output using `stdout` if not provided).
outputFolder: ../test-watcher outputFolder: ../test-watcher
# Code generation mode [eth_call | storage | all | none] (default: all). # Code generation mode [eth_call | storage | all | none] (default: none).
mode: all mode: none
# Kind of watcher [lazy | active] (default: active). # Kind of watcher [lazy | active] (default: active).
kind: active kind: active
@ -47,6 +48,7 @@
flatten: true flatten: true
# Path to the subgraph build (optional). # Path to the subgraph build (optional).
# Can set empty contracts array when using subgraphPath.
subgraphPath: ../graph-node/test/subgraph/example1/build subgraphPath: ../graph-node/test/subgraph/example1/build
# NOTE: When passed an *URL* as contract path, it is assumed that it points to an already flattened contract file. # NOTE: When passed an *URL* as contract path, it is assumed that it points to an already flattened contract file.
@ -149,7 +151,7 @@
```bash ```bash
yarn checkpoint --address <contract-address> --block-hash [block-hash] yarn checkpoint --address <contract-address> --block-hash [block-hash]
``` ```
* To reset the watcher to a previous block number: * To reset the watcher to a previous block number:
* Reset state: * Reset state:
@ -175,7 +177,7 @@
```bash ```bash
yarn import-state --import-file <import-file-path> yarn import-state --import-file <import-file-path>
``` ```
* To inspect a CID: * To inspect a CID:
```bash ```bash

View File

@ -3,16 +3,14 @@
// //
import solc from 'solc'; import solc from 'solc';
import { Writable } from 'stream';
/** /**
* Compiles the given contract using solc and writes the resultant artifacts to a file. * Compiles the given contract using solc and returns resultant artifacts.
* @param outStream A writable output stream to write the artifacts file to.
* @param contractContent Contents of the contract file to be compiled. * @param contractContent Contents of the contract file to be compiled.
* @param contractFileName Input contract file name. * @param contractFileName Input contract file name.
* @param contractName Name of the main contract in the contract file. * @param contractName Name of the main contract in the contract file.
*/ */
export function exportArtifacts (outStream: Writable, contractContent: string, contractFileName: string, contractName: string): void { export function generateArtifacts (contractContent: string, contractFileName: string, contractName: string): { abi: any[], storageLayout: any } {
const input: any = { const input: any = {
language: 'Solidity', language: 'Solidity',
sources: {}, sources: {},
@ -30,6 +28,5 @@ export function exportArtifacts (outStream: Writable, contractContent: string, c
}; };
// Get artifacts for the required contract. // Get artifacts for the required contract.
const output = JSON.parse(solc.compile(JSON.stringify(input))).contracts[contractFileName][contractName]; return JSON.parse(solc.compile(JSON.stringify(input))).contracts[contractFileName][contractName];
outStream.write(JSON.stringify(output, null, 2));
} }

View File

@ -20,7 +20,7 @@ import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL, MODE_NONE, DEFAULT_PORT } from '
import { Visitor } from './visitor'; import { Visitor } from './visitor';
import { exportServer } from './server'; import { exportServer } from './server';
import { exportConfig } from './config'; import { exportConfig } from './config';
import { exportArtifacts } from './artifacts'; import { generateArtifacts } from './artifacts';
import { exportPackage } from './package'; import { exportPackage } from './package';
import { exportTSConfig } from './tsconfig'; import { exportTSConfig } from './tsconfig';
import { exportReadme } from './readme'; import { exportReadme } from './readme';
@ -35,7 +35,7 @@ import { exportCheckpoint } from './checkpoint';
import { exportState } from './export-state'; import { exportState } from './export-state';
import { importState } from './import-state'; import { importState } from './import-state';
import { exportInspectCID } from './inspect-cid'; import { exportInspectCID } from './inspect-cid';
import { getContractKindList } from './utils/subgraph'; import { getSubgraphConfig } from './utils/subgraph';
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
const argv = await yargs(hideBin(process.argv)) const argv = await yargs(hideBin(process.argv))
@ -50,25 +50,48 @@ const main = async (): Promise<void> => {
const config = getConfig(path.resolve(argv['config-file'])); const config = getConfig(path.resolve(argv['config-file']));
// Create an array of flattened contract strings. // Create an array of flattened contract strings.
const contracts: any = []; const contracts: any[] = [];
for (const contract of config.contracts) { for (const contract of config.contracts) {
const inputFile = contract.path; const { path: inputFile, abiPath, name, kind } = contract;
assert(typeof inputFile === 'string', 'Contract input file path should be a string');
let contractString; const contractData: any = {
contractName: name,
contractKind: kind
};
if (inputFile.startsWith('http')) { if (abiPath) {
// Assume flattened file in case of URL. const abiString = fs.readFileSync(path.resolve(abiPath)).toString();
const response = await fetch(inputFile); contractData.contractAbi = JSON.parse(abiString);
contractString = await response.text();
} else {
contractString = config.flatten
? await flatten(path.resolve(inputFile))
: fs.readFileSync(path.resolve(inputFile)).toString();
} }
contracts.push({ contractString, contractName: contract.name, contractKind: contract.kind }); if (inputFile) {
assert(typeof inputFile === 'string', 'Contract input file path should be a string');
if (inputFile.startsWith('http')) {
// Assume flattened file in case of URL.
const response = await fetch(inputFile);
contractData.contractString = await response.text();
} else {
contractData.contractString = config.flatten
? await flatten(path.resolve(inputFile))
: fs.readFileSync(path.resolve(inputFile)).toString();
}
// Generate artifacts from contract.
const inputFileName = path.basename(inputFile, '.sol');
const { abi, storageLayout } = generateArtifacts(
contractData.contractString,
`${inputFileName}.sol`,
contractData.contractName
);
contractData.contractAbi = abi;
contractData.contractStorageLayout = storageLayout;
}
contracts.push(contractData);
} }
const visitor = new Visitor(); const visitor = new Visitor();
@ -79,7 +102,6 @@ const main = async (): Promise<void> => {
}; };
function parseAndVisit (visitor: Visitor, contracts: any[], mode: string) { function parseAndVisit (visitor: Visitor, contracts: any[], mode: string) {
const eventDefinitionVisitor = visitor.eventDefinitionVisitor.bind(visitor);
let functionDefinitionVisitor; let functionDefinitionVisitor;
let stateVariableDeclarationVisitor; let stateVariableDeclarationVisitor;
@ -94,19 +116,21 @@ function parseAndVisit (visitor: Visitor, contracts: any[], mode: string) {
} }
for (const contract of contracts) { for (const contract of contracts) {
// Get the abstract syntax tree for the flattened contract.
const ast = parse(contract.contractString);
// Filter out library nodes.
ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library'));
visitor.setContract(contract.contractName, contract.contractKind); visitor.setContract(contract.contractName, contract.contractKind);
visitor.parseEvents(contract.contractAbi);
visit(ast, { if (contract.contractString) {
FunctionDefinition: functionDefinitionVisitor, // Get the abstract syntax tree for the flattened contract.
StateVariableDeclaration: stateVariableDeclarationVisitor, const ast = parse(contract.contractString);
EventDefinition: eventDefinitionVisitor
}); // Filter out library nodes.
ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library'));
visit(ast, {
StateVariableDeclaration: stateVariableDeclarationVisitor,
FunctionDefinition: functionDefinitionVisitor
});
}
} }
} }
@ -133,19 +157,12 @@ function generateWatcher (visitor: Visitor, contracts: any[], config: any) {
let outStream: Writable; let outStream: Writable;
// Export artifacts for the contracts. // Export artifacts for the contracts.
config.contracts.forEach((contract: any, index: number) => { contracts.forEach((contract: any) => {
const inputFileName = path.basename(contract.path, '.sol');
outStream = outputDir outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${contract.name}.json`)) ? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${contract.contractName}.json`))
: process.stdout; : process.stdout;
exportArtifacts( outStream.write(JSON.stringify({ abi: contract.contractAbi, storageLayout: contract.contractStorageLayout }, null, 2));
outStream,
contracts[index].contractString,
`${inputFileName}.sol`,
contract.name
);
}); });
// Register the handlebar helpers to be used in the templates. // Register the handlebar helpers to be used in the templates.
@ -166,7 +183,7 @@ function generateWatcher (visitor: Visitor, contracts: any[], config: any) {
outStream = outputDir outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/indexer.ts')) ? fs.createWriteStream(path.join(outputDir, 'src/indexer.ts'))
: process.stdout; : process.stdout;
visitor.exportIndexer(outStream, config.contracts); visitor.exportIndexer(outStream, contracts);
outStream = outputDir outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/server.ts')) ? fs.createWriteStream(path.join(outputDir, 'src/server.ts'))
@ -304,31 +321,38 @@ function getConfig (configFile: string): any {
assert(typeof inputConfig.port === 'number', 'Invalid watcher server port'); assert(typeof inputConfig.port === 'number', 'Invalid watcher server port');
} }
// Check that every input contract kind is present in the subgraph config.
let subgraphPath;
if (inputConfig.subgraphPath) {
// Resolve path.
subgraphPath = inputConfig.subgraphPath.replace(/^~/, os.homedir());
const subgraphKinds: string[] = getContractKindList(subgraphPath);
const inputKinds: string[] = inputConfig.contracts.map((contract: any) => contract.kind);
assert(
inputKinds.every((inputKind: string) => subgraphKinds.includes(inputKind)),
'Input contract kind not available in the subgraph.'
);
}
const inputFlatten = inputConfig.flatten;
const flatten = (inputFlatten === undefined || inputFlatten === null) ? true : inputFlatten;
// Resolve paths. // Resolve paths.
const contracts = inputConfig.contracts.map((contract: any) => { const contracts = inputConfig.contracts.map((contract: any) => {
contract.path = contract.path.replace(/^~/, os.homedir()); contract.path = contract.path.replace(/^~/, os.homedir());
return contract; return contract;
}); });
let subgraphPath: any;
let subgraphConfig;
if (inputConfig.subgraphPath) {
// Resolve path.
subgraphPath = inputConfig.subgraphPath.replace(/^~/, os.homedir());
subgraphConfig = getSubgraphConfig(subgraphPath);
// Add contracts missing for dataSources in subgraph config.
subgraphConfig.dataSources.forEach((dataSource: any) => {
if (!contracts.some((contract: any) => contract.kind === dataSource.name)) {
const abi = dataSource.mapping.abis.find((abi: any) => abi.name === dataSource.source.abi);
const abiPath = path.resolve(subgraphPath, abi.file);
contracts.push({
name: dataSource.name,
kind: dataSource.name,
abiPath
});
}
});
}
const inputFlatten = inputConfig.flatten;
const flatten = (inputFlatten === undefined || inputFlatten === null) ? true : inputFlatten;
return { return {
contracts, contracts,
outputFolder: inputConfig.outputFolder, outputFolder: inputConfig.outputFolder,
@ -336,7 +360,8 @@ function getConfig (configFile: string): any {
kind: inputConfig.kind || KIND_ACTIVE, kind: inputConfig.kind || KIND_ACTIVE,
port: inputConfig.port || DEFAULT_PORT, port: inputConfig.port || DEFAULT_PORT,
flatten, flatten,
subgraphPath subgraphPath,
subgraphConfig
}; };
} }

View File

@ -31,7 +31,7 @@ import {
import { GraphWatcher } from '@vulcanize/graph-node'; import { GraphWatcher } from '@vulcanize/graph-node';
{{#each contracts as | contract |}} {{#each contracts as | contract |}}
import {{contract.name}}Artifacts from './artifacts/{{contract.name}}.json'; import {{contract.contractName}}Artifacts from './artifacts/{{contract.contractName}}.json';
{{/each}} {{/each}}
import { Database } from './database'; import { Database } from './database';
import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks'; import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks';
@ -49,7 +49,7 @@ import { {{subgraphEntity.className}} } from './entity/{{subgraphEntity.classNam
const log = debug('vulcanize:indexer'); const log = debug('vulcanize:indexer');
{{#each contracts as | contract |}} {{#each contracts as | contract |}}
const KIND_{{capitalize contract.name}} = '{{contract.kind}}'; const KIND_{{capitalize contract.contractName}} = '{{contract.contractKind}}';
{{/each}} {{/each}}
{{#each uniqueEvents as | event |}} {{#each uniqueEvents as | event |}}
@ -131,15 +131,22 @@ export class Indexer implements IndexerInterface {
this._contractMap = new Map(); this._contractMap = new Map();
{{#each contracts as | contract |}} {{#each contracts as | contract |}}
const { abi: {{contract.name}}ABI, storageLayout: {{contract.name}}StorageLayout } = {{contract.name}}Artifacts; const {
assert({{contract.name}}ABI); abi: {{contract.contractName}}ABI,
assert({{contract.name}}StorageLayout); {{#if contract.contractStorageLayout}}
storageLayout: {{contract.contractName}}StorageLayout
{{/if}}
} = {{contract.contractName}}Artifacts;
{{/each}} {{/each}}
{{#each contracts as | contract |}} {{#each contracts as | contract |}}
this._abiMap.set(KIND_{{capitalize contract.name}}, {{contract.name}}ABI); assert({{contract.contractName}}ABI);
this._storageLayoutMap.set(KIND_{{capitalize contract.name}}, {{contract.name}}StorageLayout); this._abiMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}ABI);
this._contractMap.set(KIND_{{capitalize contract.name}}, new ethers.utils.Interface({{contract.name}}ABI)); {{#if contract.contractStorageLayout}}
assert({{contract.contractName}}StorageLayout);
this._storageLayoutMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}StorageLayout);
{{/if}}
this._contractMap.set(KIND_{{capitalize contract.contractName}}, new ethers.utils.Interface({{contract.contractName}}ABI));
{{/each}} {{/each}}
this._entityTypesMap = new Map(); this._entityTypesMap = new Map();
@ -434,8 +441,8 @@ export class Indexer implements IndexerInterface {
switch (kind) { switch (kind) {
{{#each contracts as | contract |}} {{#each contracts as | contract |}}
case KIND_{{capitalize contract.name}}: { case KIND_{{capitalize contract.contractName}}: {
({ eventName, eventInfo } = this.parse{{contract.name}}Event(logDescription)); ({ eventName, eventInfo } = this.parse{{contract.contractName}}Event(logDescription));
break; break;
} }
@ -450,13 +457,13 @@ export class Indexer implements IndexerInterface {
} }
{{#each contracts as | contract |}} {{#each contracts as | contract |}}
parse{{contract.name}}Event (logDescription: ethers.utils.LogDescription): { eventName: string, eventInfo: any } { parse{{contract.contractName}}Event (logDescription: ethers.utils.LogDescription): { eventName: string, eventInfo: any } {
let eventName = UNKNOWN_EVENT_NAME; let eventName = UNKNOWN_EVENT_NAME;
let eventInfo = {}; let eventInfo = {};
switch (logDescription.name) { switch (logDescription.name) {
{{#each ../events as | event |}} {{#each ../events as | event |}}
{{#if (compare contract.kind event.kind)}} {{#if (compare contract.contractKind event.kind)}}
case {{capitalize event.name}}_EVENT: { case {{capitalize event.name}}_EVENT: {
eventName = logDescription.name; eventName = logDescription.name;
{{#if event.params}} {{#if event.params}}

View File

@ -45,17 +45,11 @@ export function getFieldType (typeNode: any): { typeName: string, array: boolean
return { typeName: typeNode.name.value, array: false, nullable: true }; return { typeName: typeNode.name.value, array: false, nullable: true };
} }
export function getContractKindList (subgraphPath: string): string[] { export function getSubgraphConfig (subgraphPath: string): any {
const subgraphConfigPath = path.join(path.resolve(subgraphPath), '/subgraph.yaml'); const subgraphConfigPath = path.join(path.resolve(subgraphPath), '/subgraph.yaml');
assert(fs.existsSync(subgraphConfigPath), `Subgraph config file not found at ${subgraphConfigPath}`); assert(fs.existsSync(subgraphConfigPath), `Subgraph config file not found at ${subgraphConfigPath}`);
const subgraph = yaml.load(fs.readFileSync(subgraphConfigPath, 'utf8')) as any; return yaml.load(fs.readFileSync(subgraphConfigPath, 'utf8')) as any;
const contractKinds: string[] = subgraph.dataSources.map((dataSource: any) => {
return dataSource.name;
});
return contractKinds;
} }
function parseType (typeNode: any): any { function parseType (typeNode: any): any {

View File

@ -4,6 +4,7 @@
import { Writable } from 'stream'; import { Writable } from 'stream';
import assert from 'assert'; import assert from 'assert';
import { utils } from 'ethers';
import { Database } from './database'; import { Database } from './database';
import { Entity } from './entity'; import { Entity } from './entity';
@ -125,19 +126,22 @@ export class Visitor {
} }
/** /**
* Visitor function for event definitions. * Function to parse event definitions.
* @param node ASTNode for an event definition. * @param abi Contract ABI.
*/ */
eventDefinitionVisitor (node: any): void { parseEvents (abi: any): void {
const name = node.name; const contractInterface = new utils.Interface(abi);
const params = node.parameters.map((item: any) => {
return { name: item.name, type: item.typeName.name }; Object.values(contractInterface.events).forEach(event => {
const params = event.inputs.map(input => {
return { name: input.name, type: input.type };
});
this._schema.addEventType(event.name, params);
assert(this._contract);
this._indexer.addEvent(event.name, params, this._contract.kind);
}); });
this._schema.addEventType(name, params);
assert(this._contract);
this._indexer.addEvent(name, params, this._contract.kind);
} }
visitSubgraph (subgraphPath?: string): void { visitSubgraph (subgraphPath?: string): void {