mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 20:36:19 +00:00
Generate schema from ABI when using subgraph path (#101)
This commit is contained in:
parent
a267058d51
commit
561c2c9066
@ -23,6 +23,7 @@
|
||||
```yaml
|
||||
# Example config.yaml
|
||||
# Contracts to watch (required).
|
||||
# Can pass empty array ([]) when using subgraphPath.
|
||||
contracts:
|
||||
# Contract name.
|
||||
- name: Example
|
||||
@ -34,8 +35,8 @@
|
||||
# Output folder path (logs output using `stdout` if not provided).
|
||||
outputFolder: ../test-watcher
|
||||
|
||||
# Code generation mode [eth_call | storage | all | none] (default: all).
|
||||
mode: all
|
||||
# Code generation mode [eth_call | storage | all | none] (default: none).
|
||||
mode: none
|
||||
|
||||
# Kind of watcher [lazy | active] (default: active).
|
||||
kind: active
|
||||
@ -47,6 +48,7 @@
|
||||
flatten: true
|
||||
|
||||
# Path to the subgraph build (optional).
|
||||
# Can set empty contracts array when using subgraphPath.
|
||||
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.
|
||||
@ -149,7 +151,7 @@
|
||||
```bash
|
||||
yarn checkpoint --address <contract-address> --block-hash [block-hash]
|
||||
```
|
||||
|
||||
|
||||
* To reset the watcher to a previous block number:
|
||||
|
||||
* Reset state:
|
||||
@ -175,7 +177,7 @@
|
||||
```bash
|
||||
yarn import-state --import-file <import-file-path>
|
||||
```
|
||||
|
||||
|
||||
* To inspect a CID:
|
||||
|
||||
```bash
|
||||
|
@ -3,16 +3,14 @@
|
||||
//
|
||||
|
||||
import solc from 'solc';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
/**
|
||||
* Compiles the given contract using solc and writes the resultant artifacts to a file.
|
||||
* @param outStream A writable output stream to write the artifacts file to.
|
||||
* Compiles the given contract using solc and returns resultant artifacts.
|
||||
* @param contractContent Contents of the contract file to be compiled.
|
||||
* @param contractFileName Input contract file name.
|
||||
* @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 = {
|
||||
language: 'Solidity',
|
||||
sources: {},
|
||||
@ -30,6 +28,5 @@ export function exportArtifacts (outStream: Writable, contractContent: string, c
|
||||
};
|
||||
|
||||
// Get artifacts for the required contract.
|
||||
const output = JSON.parse(solc.compile(JSON.stringify(input))).contracts[contractFileName][contractName];
|
||||
outStream.write(JSON.stringify(output, null, 2));
|
||||
return JSON.parse(solc.compile(JSON.stringify(input))).contracts[contractFileName][contractName];
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL, MODE_NONE, DEFAULT_PORT } from '
|
||||
import { Visitor } from './visitor';
|
||||
import { exportServer } from './server';
|
||||
import { exportConfig } from './config';
|
||||
import { exportArtifacts } from './artifacts';
|
||||
import { generateArtifacts } from './artifacts';
|
||||
import { exportPackage } from './package';
|
||||
import { exportTSConfig } from './tsconfig';
|
||||
import { exportReadme } from './readme';
|
||||
@ -35,7 +35,7 @@ import { exportCheckpoint } from './checkpoint';
|
||||
import { exportState } from './export-state';
|
||||
import { importState } from './import-state';
|
||||
import { exportInspectCID } from './inspect-cid';
|
||||
import { getContractKindList } from './utils/subgraph';
|
||||
import { getSubgraphConfig } from './utils/subgraph';
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
@ -50,25 +50,48 @@ const main = async (): Promise<void> => {
|
||||
const config = getConfig(path.resolve(argv['config-file']));
|
||||
|
||||
// Create an array of flattened contract strings.
|
||||
const contracts: any = [];
|
||||
const contracts: any[] = [];
|
||||
|
||||
for (const contract of config.contracts) {
|
||||
const inputFile = contract.path;
|
||||
assert(typeof inputFile === 'string', 'Contract input file path should be a string');
|
||||
const { path: inputFile, abiPath, name, kind } = contract;
|
||||
|
||||
let contractString;
|
||||
const contractData: any = {
|
||||
contractName: name,
|
||||
contractKind: kind
|
||||
};
|
||||
|
||||
if (inputFile.startsWith('http')) {
|
||||
// Assume flattened file in case of URL.
|
||||
const response = await fetch(inputFile);
|
||||
contractString = await response.text();
|
||||
} else {
|
||||
contractString = config.flatten
|
||||
? await flatten(path.resolve(inputFile))
|
||||
: fs.readFileSync(path.resolve(inputFile)).toString();
|
||||
if (abiPath) {
|
||||
const abiString = fs.readFileSync(path.resolve(abiPath)).toString();
|
||||
contractData.contractAbi = JSON.parse(abiString);
|
||||
}
|
||||
|
||||
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();
|
||||
@ -79,7 +102,6 @@ const main = async (): Promise<void> => {
|
||||
};
|
||||
|
||||
function parseAndVisit (visitor: Visitor, contracts: any[], mode: string) {
|
||||
const eventDefinitionVisitor = visitor.eventDefinitionVisitor.bind(visitor);
|
||||
let functionDefinitionVisitor;
|
||||
let stateVariableDeclarationVisitor;
|
||||
|
||||
@ -94,19 +116,21 @@ function parseAndVisit (visitor: Visitor, contracts: any[], mode: string) {
|
||||
}
|
||||
|
||||
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.parseEvents(contract.contractAbi);
|
||||
|
||||
visit(ast, {
|
||||
FunctionDefinition: functionDefinitionVisitor,
|
||||
StateVariableDeclaration: stateVariableDeclarationVisitor,
|
||||
EventDefinition: eventDefinitionVisitor
|
||||
});
|
||||
if (contract.contractString) {
|
||||
// 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'));
|
||||
|
||||
visit(ast, {
|
||||
StateVariableDeclaration: stateVariableDeclarationVisitor,
|
||||
FunctionDefinition: functionDefinitionVisitor
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,19 +157,12 @@ function generateWatcher (visitor: Visitor, contracts: any[], config: any) {
|
||||
let outStream: Writable;
|
||||
|
||||
// Export artifacts for the contracts.
|
||||
config.contracts.forEach((contract: any, index: number) => {
|
||||
const inputFileName = path.basename(contract.path, '.sol');
|
||||
|
||||
contracts.forEach((contract: any) => {
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${contract.name}.json`))
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${contract.contractName}.json`))
|
||||
: process.stdout;
|
||||
|
||||
exportArtifacts(
|
||||
outStream,
|
||||
contracts[index].contractString,
|
||||
`${inputFileName}.sol`,
|
||||
contract.name
|
||||
);
|
||||
outStream.write(JSON.stringify({ abi: contract.contractAbi, storageLayout: contract.contractStorageLayout }, null, 2));
|
||||
});
|
||||
|
||||
// Register the handlebar helpers to be used in the templates.
|
||||
@ -166,7 +183,7 @@ function generateWatcher (visitor: Visitor, contracts: any[], config: any) {
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/indexer.ts'))
|
||||
: process.stdout;
|
||||
visitor.exportIndexer(outStream, config.contracts);
|
||||
visitor.exportIndexer(outStream, contracts);
|
||||
|
||||
outStream = outputDir
|
||||
? 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');
|
||||
}
|
||||
|
||||
// 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.
|
||||
const contracts = inputConfig.contracts.map((contract: any) => {
|
||||
contract.path = contract.path.replace(/^~/, os.homedir());
|
||||
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 {
|
||||
contracts,
|
||||
outputFolder: inputConfig.outputFolder,
|
||||
@ -336,7 +360,8 @@ function getConfig (configFile: string): any {
|
||||
kind: inputConfig.kind || KIND_ACTIVE,
|
||||
port: inputConfig.port || DEFAULT_PORT,
|
||||
flatten,
|
||||
subgraphPath
|
||||
subgraphPath,
|
||||
subgraphConfig
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
import { GraphWatcher } from '@vulcanize/graph-node';
|
||||
|
||||
{{#each contracts as | contract |}}
|
||||
import {{contract.name}}Artifacts from './artifacts/{{contract.name}}.json';
|
||||
import {{contract.contractName}}Artifacts from './artifacts/{{contract.contractName}}.json';
|
||||
{{/each}}
|
||||
import { Database } from './database';
|
||||
import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks';
|
||||
@ -49,7 +49,7 @@ import { {{subgraphEntity.className}} } from './entity/{{subgraphEntity.classNam
|
||||
const log = debug('vulcanize:indexer');
|
||||
|
||||
{{#each contracts as | contract |}}
|
||||
const KIND_{{capitalize contract.name}} = '{{contract.kind}}';
|
||||
const KIND_{{capitalize contract.contractName}} = '{{contract.contractKind}}';
|
||||
{{/each}}
|
||||
|
||||
{{#each uniqueEvents as | event |}}
|
||||
@ -131,15 +131,22 @@ export class Indexer implements IndexerInterface {
|
||||
this._contractMap = new Map();
|
||||
|
||||
{{#each contracts as | contract |}}
|
||||
const { abi: {{contract.name}}ABI, storageLayout: {{contract.name}}StorageLayout } = {{contract.name}}Artifacts;
|
||||
assert({{contract.name}}ABI);
|
||||
assert({{contract.name}}StorageLayout);
|
||||
const {
|
||||
abi: {{contract.contractName}}ABI,
|
||||
{{#if contract.contractStorageLayout}}
|
||||
storageLayout: {{contract.contractName}}StorageLayout
|
||||
{{/if}}
|
||||
} = {{contract.contractName}}Artifacts;
|
||||
|
||||
{{/each}}
|
||||
{{#each contracts as | contract |}}
|
||||
this._abiMap.set(KIND_{{capitalize contract.name}}, {{contract.name}}ABI);
|
||||
this._storageLayoutMap.set(KIND_{{capitalize contract.name}}, {{contract.name}}StorageLayout);
|
||||
this._contractMap.set(KIND_{{capitalize contract.name}}, new ethers.utils.Interface({{contract.name}}ABI));
|
||||
assert({{contract.contractName}}ABI);
|
||||
this._abiMap.set(KIND_{{capitalize contract.contractName}}, {{contract.contractName}}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}}
|
||||
this._entityTypesMap = new Map();
|
||||
@ -434,8 +441,8 @@ export class Indexer implements IndexerInterface {
|
||||
|
||||
switch (kind) {
|
||||
{{#each contracts as | contract |}}
|
||||
case KIND_{{capitalize contract.name}}: {
|
||||
({ eventName, eventInfo } = this.parse{{contract.name}}Event(logDescription));
|
||||
case KIND_{{capitalize contract.contractName}}: {
|
||||
({ eventName, eventInfo } = this.parse{{contract.contractName}}Event(logDescription));
|
||||
|
||||
break;
|
||||
}
|
||||
@ -450,13 +457,13 @@ export class Indexer implements IndexerInterface {
|
||||
}
|
||||
|
||||
{{#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 eventInfo = {};
|
||||
|
||||
switch (logDescription.name) {
|
||||
{{#each ../events as | event |}}
|
||||
{{#if (compare contract.kind event.kind)}}
|
||||
{{#if (compare contract.contractKind event.kind)}}
|
||||
case {{capitalize event.name}}_EVENT: {
|
||||
eventName = logDescription.name;
|
||||
{{#if event.params}}
|
||||
|
@ -45,17 +45,11 @@ export function getFieldType (typeNode: any): { typeName: string, array: boolean
|
||||
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');
|
||||
|
||||
assert(fs.existsSync(subgraphConfigPath), `Subgraph config file not found at ${subgraphConfigPath}`);
|
||||
const subgraph = yaml.load(fs.readFileSync(subgraphConfigPath, 'utf8')) as any;
|
||||
|
||||
const contractKinds: string[] = subgraph.dataSources.map((dataSource: any) => {
|
||||
return dataSource.name;
|
||||
});
|
||||
|
||||
return contractKinds;
|
||||
return yaml.load(fs.readFileSync(subgraphConfigPath, 'utf8')) as any;
|
||||
}
|
||||
|
||||
function parseType (typeNode: any): any {
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import assert from 'assert';
|
||||
import { utils } from 'ethers';
|
||||
|
||||
import { Database } from './database';
|
||||
import { Entity } from './entity';
|
||||
@ -125,19 +126,22 @@ export class Visitor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor function for event definitions.
|
||||
* @param node ASTNode for an event definition.
|
||||
* Function to parse event definitions.
|
||||
* @param abi Contract ABI.
|
||||
*/
|
||||
eventDefinitionVisitor (node: any): void {
|
||||
const name = node.name;
|
||||
const params = node.parameters.map((item: any) => {
|
||||
return { name: item.name, type: item.typeName.name };
|
||||
parseEvents (abi: any): void {
|
||||
const contractInterface = new utils.Interface(abi);
|
||||
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user