mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 20:36:19 +00:00
Parse events for multiple contracts in the generated code (#95)
* Parse events for multiple contracts in the generated code * Use contract wise artifacts in the generated indexer methods * Update codegen docs to use config file to generate a watcher * Add watcher generation config to eden-watcher
This commit is contained in:
parent
e5faba8e68
commit
e883463aa6
@ -6,68 +6,69 @@
|
||||
|
||||
* Install required packages:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
* Build files:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
* Run the following command to generate a watcher from a contract file:
|
||||
* Create a `.yaml` config file in the following format for generating a watcher:
|
||||
|
||||
```bash
|
||||
yarn codegen --input-files <input-file-paths> --contract-names <contract-names> --output-folder [output-folder] --mode [eth_call | storage | all | none] --flatten [true | false] --kind [lazy | active] --port [server-port] --subgraph-path [subgraph-build-path]
|
||||
```yaml
|
||||
# Example config.yaml
|
||||
# Contracts to watch (required).
|
||||
contracts:
|
||||
# Contract name.
|
||||
- name: Example
|
||||
# Contract file path or an url.
|
||||
path: ../graph-node/test/contracts/Example.sol
|
||||
# Contract kind (should match that in {subgraphPath}/subgraph.yaml if subgraphPath provided)
|
||||
kind: Example1
|
||||
|
||||
# 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
|
||||
|
||||
# Kind of watcher [lazy | active] (default: active).
|
||||
kind: active
|
||||
|
||||
# Watcher server port (default: 3008).
|
||||
port: 3008
|
||||
|
||||
# Flatten the input contract file(s) [true | false] (default: true).
|
||||
flatten: true
|
||||
|
||||
# Path to the subgraph build (optional).
|
||||
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.
|
||||
```
|
||||
|
||||
* `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`).
|
||||
* `kind` (alias: `k`): Kind of watcher (default: `active`).
|
||||
* `port` (alias: `p`): Server port (default: `3008`).
|
||||
* `subgraph-path` (alias: `s`): Path to the subgraph build.
|
||||
|
||||
**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.
|
||||
* Run the following command to generate a watcher from contract(s):
|
||||
|
||||
```bash
|
||||
yarn codegen --input-files ./test/examples/contracts/ERC721.sol --contract-names ERC721 --output-folder ../my-erc721-watcher --mode storage --kind lazy
|
||||
yarn codegen --config-file <config-file-path>
|
||||
```
|
||||
|
||||
Generate code in `eth_call` mode using a contract provided by an URL.
|
||||
* `config-file`(alias: `c`): Watcher generation config file path (yaml) (required).
|
||||
|
||||
```bash
|
||||
yarn codegen --input-files https://git.io/Jupci --contract-names ERC721 --output-folder ../my-erc721-watcher --mode eth_call
|
||||
```
|
||||
Example:
|
||||
|
||||
Generate code for `ERC721` in both `eth_call` and `storage` mode, `active` kind.
|
||||
* Generate code using a config file `config.yaml`:
|
||||
|
||||
```bash
|
||||
yarn codegen --input-files ../../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol --contract-names ERC721 --output-folder ../demo-erc721-watcher --mode all --kind active
|
||||
```
|
||||
```bash
|
||||
yarn codegen --config-file ./config.yaml
|
||||
```
|
||||
|
||||
Generate code for `ERC20` contract in both `eth_call` and `storage` mode, `active` kind:
|
||||
|
||||
```bash
|
||||
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 `none` 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 none --kind active --subgraph-path ~/vulcanize/eden-data/packages/subgraph/build
|
||||
```
|
||||
This will create a folder containing the generated code at the path provided in config. Follow the steps in [Run Generated Watcher](#run-generated-watcher) to setup and run the generated watcher.
|
||||
|
||||
## Run Generated Watcher
|
||||
|
||||
@ -103,14 +104,12 @@
|
||||
|
||||
* Generating state:
|
||||
|
||||
* Edit the custom hook function `createInitialCheckpoint` (triggered on watch-contract, checkpoint: `true`) in `src/hooks.ts` to save an initial checkpoint `IPLDBlock` using the `Indexer` object.
|
||||
* Edit the custom hook function `createInitialState` (triggered if the watcher passes the start block, checkpoint: `true`) in `src/hooks.ts` to save an initial state `IPLDBlock` using the `Indexer` object.
|
||||
|
||||
* Edit the custom hook function `createStateDiff` (triggered on a block) in `src/hooks.ts` to save the state in a `diff` `IPLDBlock` using the `Indexer` object. The default state (if exists) is updated.
|
||||
|
||||
* Edit the custom hook function `createStateCheckpoint` (triggered just before default and CLI checkpoint) in `src/hooks.ts` to save the state in a `checkpoint` `IPLDBlock` using the `Indexer` object.
|
||||
|
||||
* The existing example hooks in `src/hooks.ts` are for an `ERC20` contract.
|
||||
|
||||
### Run
|
||||
|
||||
* Run lint:
|
||||
|
@ -9,12 +9,14 @@ import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import assert from 'assert';
|
||||
import { Writable } from 'stream';
|
||||
import yaml from 'js-yaml';
|
||||
import os from 'os';
|
||||
|
||||
import { flatten } from '@poanet/solidity-flattener';
|
||||
import { parse, visit } from '@solidity-parser/parser';
|
||||
import { KIND_ACTIVE, KIND_LAZY } from '@vulcanize/util';
|
||||
|
||||
import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL, MODE_NONE } from './utils/constants';
|
||||
import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL, MODE_NONE, DEFAULT_PORT } from './utils/constants';
|
||||
import { Visitor } from './visitor';
|
||||
import { exportServer } from './server';
|
||||
import { exportConfig } from './config';
|
||||
@ -33,86 +35,50 @@ import { exportCheckpoint } from './checkpoint';
|
||||
import { exportState } from './export-state';
|
||||
import { importState } from './import-state';
|
||||
import { exportInspectCID } from './inspect-cid';
|
||||
import { getContractKindList } from './utils/subgraph';
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.option('input-files', {
|
||||
alias: 'i',
|
||||
demandOption: true,
|
||||
describe: 'Input contract file path(s) or url(s).',
|
||||
type: 'array'
|
||||
})
|
||||
.option('contract-names', {
|
||||
.option('config-file', {
|
||||
alias: 'c',
|
||||
demandOption: true,
|
||||
describe: 'Contract name(s).',
|
||||
type: 'array'
|
||||
})
|
||||
.option('output-folder', {
|
||||
alias: 'o',
|
||||
describe: 'Output folder path.',
|
||||
type: 'string'
|
||||
})
|
||||
.option('mode', {
|
||||
alias: 'm',
|
||||
describe: 'Code generation mode.',
|
||||
type: 'string',
|
||||
default: MODE_ALL,
|
||||
choices: [MODE_ETH_CALL, MODE_STORAGE, MODE_ALL, MODE_NONE]
|
||||
})
|
||||
.option('kind', {
|
||||
alias: 'k',
|
||||
describe: 'Watcher kind.',
|
||||
type: 'string',
|
||||
default: KIND_ACTIVE,
|
||||
choices: [KIND_ACTIVE, KIND_LAZY]
|
||||
})
|
||||
.option('port', {
|
||||
alias: 'p',
|
||||
describe: 'Server port.',
|
||||
type: 'number',
|
||||
default: 3008
|
||||
})
|
||||
.option('flatten', {
|
||||
alias: 'f',
|
||||
describe: 'Flatten the input contract file.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
})
|
||||
.option('subgraph-path', {
|
||||
alias: 's',
|
||||
describe: 'Path to the subgraph build.',
|
||||
describe: 'Watcher generation config file path (yaml)',
|
||||
type: 'string'
|
||||
})
|
||||
.argv;
|
||||
|
||||
// Create an array of flattened contract strings.
|
||||
const contractStrings: string[] = [];
|
||||
const config = getConfig(path.resolve(argv['config-file']));
|
||||
|
||||
for (const inputFile of argv['input-files']) {
|
||||
assert(typeof inputFile === 'string', 'Input file path should be a string');
|
||||
// Create an array of flattened contract strings.
|
||||
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');
|
||||
|
||||
let contractString;
|
||||
|
||||
if (inputFile.startsWith('http')) {
|
||||
// Assume flattened file in case of URL.
|
||||
const response = await fetch(inputFile);
|
||||
const contractString = await response.text();
|
||||
contractStrings.push(contractString);
|
||||
contractString = await response.text();
|
||||
} else {
|
||||
contractStrings.push(argv.flatten
|
||||
contractString = config.flatten
|
||||
? await flatten(path.resolve(inputFile))
|
||||
: fs.readFileSync(path.resolve(inputFile)).toString()
|
||||
);
|
||||
: fs.readFileSync(path.resolve(inputFile)).toString();
|
||||
}
|
||||
|
||||
contracts.push({ contractString, contractName: contract.name, contractKind: contract.kind });
|
||||
}
|
||||
|
||||
const visitor = new Visitor();
|
||||
|
||||
parseAndVisit(contractStrings, visitor, argv.mode);
|
||||
parseAndVisit(visitor, contracts, config.mode);
|
||||
|
||||
generateWatcher(contractStrings, visitor, argv);
|
||||
generateWatcher(visitor, contracts, config);
|
||||
};
|
||||
|
||||
function parseAndVisit (contractStrings: string[], visitor: Visitor, mode: string) {
|
||||
function parseAndVisit (visitor: Visitor, contracts: any[], mode: string) {
|
||||
const eventDefinitionVisitor = visitor.eventDefinitionVisitor.bind(visitor);
|
||||
let functionDefinitionVisitor;
|
||||
let stateVariableDeclarationVisitor;
|
||||
@ -127,13 +93,15 @@ function parseAndVisit (contractStrings: string[], visitor: Visitor, mode: strin
|
||||
stateVariableDeclarationVisitor = visitor.stateVariableDeclarationVisitor.bind(visitor);
|
||||
}
|
||||
|
||||
for (const contractString of contractStrings) {
|
||||
for (const contract of contracts) {
|
||||
// Get the abstract syntax tree for the flattened contract.
|
||||
const ast = parse(contractString);
|
||||
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);
|
||||
|
||||
visit(ast, {
|
||||
FunctionDefinition: functionDefinitionVisitor,
|
||||
StateVariableDeclaration: stateVariableDeclarationVisitor,
|
||||
@ -142,11 +110,11 @@ function parseAndVisit (contractStrings: string[], visitor: Visitor, mode: strin
|
||||
}
|
||||
}
|
||||
|
||||
function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any) {
|
||||
function generateWatcher (visitor: Visitor, contracts: any[], config: any) {
|
||||
// Prepare directory structure for the watcher.
|
||||
let outputDir = '';
|
||||
if (argv['output-folder']) {
|
||||
outputDir = path.resolve(argv['output-folder']);
|
||||
if (config.outputFolder) {
|
||||
outputDir = path.resolve(config.outputFolder);
|
||||
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const environmentsFolder = path.join(outputDir, 'environments');
|
||||
@ -164,30 +132,26 @@ function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any
|
||||
|
||||
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);
|
||||
config.contracts.forEach((contract: any, index: number) => {
|
||||
const inputFileName = path.basename(contract.path, '.sol');
|
||||
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${inputFileName}.json`))
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${contract.name}.json`))
|
||||
: process.stdout;
|
||||
|
||||
exportArtifacts(
|
||||
outStream,
|
||||
contractStrings[index],
|
||||
contracts[index].contractString,
|
||||
`${inputFileName}.sol`,
|
||||
contractNames[index]
|
||||
contract.name
|
||||
);
|
||||
});
|
||||
|
||||
// Register the handlebar helpers to be used in the templates.
|
||||
registerHandlebarHelpers();
|
||||
|
||||
visitor.visitSubgraph(argv['subgraph-path']);
|
||||
visitor.visitSubgraph(config.subgraphPath);
|
||||
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/schema.gql'))
|
||||
@ -202,7 +166,7 @@ function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/indexer.ts'))
|
||||
: process.stdout;
|
||||
visitor.exportIndexer(outStream, inputFileNames);
|
||||
visitor.exportIndexer(outStream, config.contracts);
|
||||
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/server.ts'))
|
||||
@ -212,7 +176,7 @@ function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'environments/local.toml'))
|
||||
: process.stdout;
|
||||
exportConfig(argv.kind, argv.port, path.basename(outputDir), outStream, argv['subgraph-path']);
|
||||
exportConfig(config.kind, config.port, path.basename(outputDir), outStream, config.subgraphPath);
|
||||
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/database.ts'))
|
||||
@ -237,7 +201,7 @@ function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'README.md'))
|
||||
: process.stdout;
|
||||
exportReadme(path.basename(outputDir), argv['contract-name'], outStream);
|
||||
exportReadme(path.basename(outputDir), config.port, outStream);
|
||||
|
||||
outStream = outputDir
|
||||
? fs.createWriteStream(path.join(outputDir, 'src/events.ts'))
|
||||
@ -321,6 +285,61 @@ function generateWatcher (contractStrings: string[], visitor: Visitor, argv: any
|
||||
exportInspectCID(outStream);
|
||||
}
|
||||
|
||||
function getConfig (configFile: string): any {
|
||||
assert(fs.existsSync(configFile), `Config file not found at ${configFile}`);
|
||||
|
||||
// Read config.
|
||||
const inputConfig = yaml.load(fs.readFileSync(configFile, 'utf8')) as any;
|
||||
|
||||
// Run validations on config fields.
|
||||
if (inputConfig.mode) {
|
||||
assert([MODE_ETH_CALL, MODE_STORAGE, MODE_ALL, MODE_NONE].includes(inputConfig.mode), 'Invalid code generation mode');
|
||||
}
|
||||
|
||||
if (inputConfig.kind) {
|
||||
assert([KIND_ACTIVE, KIND_LAZY].includes(inputConfig.kind), 'Invalid watcher kind');
|
||||
}
|
||||
|
||||
if (inputConfig.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.
|
||||
const contracts = inputConfig.contracts.map((contract: any) => {
|
||||
contract.path = contract.path.replace(/^~/, os.homedir());
|
||||
return contract;
|
||||
});
|
||||
|
||||
return {
|
||||
contracts,
|
||||
outputFolder: inputConfig.outputFolder,
|
||||
mode: inputConfig.mode || MODE_ALL,
|
||||
kind: inputConfig.kind || KIND_ACTIVE,
|
||||
port: inputConfig.port || DEFAULT_PORT,
|
||||
flatten,
|
||||
subgraphPath
|
||||
};
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
@ -37,7 +37,7 @@ export class Indexer {
|
||||
* @param returnType Return type for the query.
|
||||
* @param stateVariableTypeName Type of the state variable in case of state variable query.
|
||||
*/
|
||||
addQuery (mode: string, name: string, params: Array<Param>, returnType: string, stateVariableType?: string): void {
|
||||
addQuery (contract: string, mode: string, name: string, params: Array<Param>, returnType: string, stateVariableType?: string): void {
|
||||
// Check if the query is already added.
|
||||
if (this._queries.some(query => query.name === name)) {
|
||||
return;
|
||||
@ -50,7 +50,8 @@ export class Indexer {
|
||||
params: _.cloneDeep(params),
|
||||
returnType,
|
||||
mode,
|
||||
stateVariableType
|
||||
stateVariableType,
|
||||
contract
|
||||
};
|
||||
|
||||
if (name.charAt(0) === '_') {
|
||||
@ -81,15 +82,16 @@ export class Indexer {
|
||||
this._queries.push(queryObject);
|
||||
}
|
||||
|
||||
addEvent (name: string, params: Array<Param>): void {
|
||||
addEvent (name: string, params: Array<Param>, contractKind: string): void {
|
||||
// Check if the event is already added.
|
||||
if (this._events.some(event => event.name === name)) {
|
||||
if (this._events.some(event => event.name === name && event.kind === contractKind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventObject = {
|
||||
name,
|
||||
params: _.cloneDeep(params)
|
||||
params: _.cloneDeep(params),
|
||||
kind: contractKind
|
||||
};
|
||||
|
||||
eventObject.params = eventObject.params.map((param) => {
|
||||
@ -169,20 +171,23 @@ export class Indexer {
|
||||
/**
|
||||
* 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 contracts Input contracts to be passed to the template.
|
||||
*/
|
||||
exportIndexer (outStream: Writable, inputFileNames: string[]): void {
|
||||
exportIndexer (outStream: Writable, contracts: any[]): void {
|
||||
const template = Handlebars.compile(this._templateString);
|
||||
|
||||
const eventNames = this._events.map((event: any) => event.name);
|
||||
|
||||
const obj = {
|
||||
inputFileNames,
|
||||
contracts,
|
||||
queries: this._queries,
|
||||
subgraphEntities: this._subgraphEntities,
|
||||
constants: {
|
||||
MODE_ETH_CALL,
|
||||
MODE_STORAGE
|
||||
},
|
||||
events: this._events
|
||||
events: this._events,
|
||||
uniqueEvents: new Set(eventNames)
|
||||
};
|
||||
|
||||
const indexer = template(obj);
|
||||
|
@ -12,15 +12,15 @@ const TEMPLATE_FILE = './templates/readme-template.handlebars';
|
||||
/**
|
||||
* Writes the README.md file generated from a template to a stream.
|
||||
* @param folderName Watcher folder name to be passed to the template.
|
||||
* @param contractName Input contract name given as title of the README.
|
||||
* @param port Watcher server port.
|
||||
* @param outStream A writable output stream to write the README.md file to.
|
||||
*/
|
||||
export function exportReadme (folderName: string, contractName: string, outStream: Writable): void {
|
||||
export function exportReadme (folderName: string, port: number, outStream: Writable): void {
|
||||
const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString();
|
||||
const template = Handlebars.compile(templateString);
|
||||
const readmeString = template({
|
||||
folderName,
|
||||
contractName
|
||||
port
|
||||
});
|
||||
outStream.write(readmeString);
|
||||
}
|
||||
|
@ -30,8 +30,8 @@ import {
|
||||
} from '@vulcanize/util';
|
||||
import { GraphWatcher } from '@vulcanize/graph-node';
|
||||
|
||||
{{#each inputFileNames as | inputFileName |}}
|
||||
import artifacts from './artifacts/{{inputFileName}}.json';
|
||||
{{#each contracts as | contract |}}
|
||||
import {{contract.name}}Artifacts from './artifacts/{{contract.name}}.json';
|
||||
{{/each}}
|
||||
import { Database } from './database';
|
||||
import { createInitialState, handleEvent, createStateDiff, createStateCheckpoint } from './hooks';
|
||||
@ -48,8 +48,12 @@ import { {{subgraphEntity.className}} } from './entity/{{subgraphEntity.classNam
|
||||
|
||||
const log = debug('vulcanize:indexer');
|
||||
|
||||
{{#each events as | event |}}
|
||||
const {{capitalize event.name}}_EVENT = '{{event.name}}';
|
||||
{{#each contracts as | contract |}}
|
||||
const KIND_{{capitalize contract.name}} = '{{contract.kind}}';
|
||||
{{/each}}
|
||||
|
||||
{{#each uniqueEvents as | event |}}
|
||||
const {{capitalize event}}_EVENT = '{{event}}';
|
||||
{{/each}}
|
||||
|
||||
export type ResultEvent = {
|
||||
@ -99,9 +103,9 @@ export class Indexer implements IndexerInterface {
|
||||
_serverConfig: ServerConfig
|
||||
_graphWatcher: GraphWatcher;
|
||||
|
||||
_abi: JsonFragment[]
|
||||
_storageLayout: StorageLayout
|
||||
_contract: ethers.utils.Interface
|
||||
_abiMap: Map<string, JsonFragment[]>
|
||||
_storageLayoutMap: Map<string, StorageLayout>
|
||||
_contractMap: Map<string, ethers.utils.Interface>
|
||||
|
||||
_ipfsClient: IPFSClient
|
||||
|
||||
@ -122,16 +126,22 @@ export class Indexer implements IndexerInterface {
|
||||
this._baseIndexer = new BaseIndexer(this._serverConfig, this._db, this._ethClient, this._postgraphileClient, this._ethProvider, jobQueue, this._ipfsClient);
|
||||
this._graphWatcher = graphWatcher;
|
||||
|
||||
const { abi, storageLayout } = artifacts;
|
||||
this._abiMap = new Map();
|
||||
this._storageLayoutMap = new Map();
|
||||
this._contractMap = new Map();
|
||||
|
||||
assert(abi);
|
||||
assert(storageLayout);
|
||||
{{#each contracts as | contract |}}
|
||||
const { abi: {{contract.name}}ABI, storageLayout: {{contract.name}}StorageLayout } = {{contract.name}}Artifacts;
|
||||
assert({{contract.name}}ABI);
|
||||
assert({{contract.name}}StorageLayout);
|
||||
|
||||
this._abi = abi;
|
||||
this._storageLayout = storageLayout;
|
||||
|
||||
this._contract = new ethers.utils.Interface(this._abi);
|
||||
{{/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));
|
||||
|
||||
{{/each}}
|
||||
this._entityTypesMap = new Map();
|
||||
this._populateEntityTypesMap();
|
||||
|
||||
@ -224,7 +234,10 @@ export class Indexer implements IndexerInterface {
|
||||
const blockNumber = ethers.BigNumber.from(number).toNumber();
|
||||
|
||||
{{#if (compare query.mode @root.constants.MODE_ETH_CALL)}}
|
||||
const contract = new ethers.Contract(contractAddress, this._abi, this._ethProvider);
|
||||
const abi = this._abiMap.get(KIND_{{capitalize query.contract}});
|
||||
assert(abi);
|
||||
|
||||
const contract = new ethers.Contract(contractAddress, abi, this._ethProvider);
|
||||
{{#if (compare query.returnType 'bigint')}}
|
||||
let value = await contract.{{query.name}}(
|
||||
{{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockHash });
|
||||
@ -239,8 +252,11 @@ export class Indexer implements IndexerInterface {
|
||||
{{/if}}
|
||||
|
||||
{{~#if (compare query.mode @root.constants.MODE_STORAGE)}}
|
||||
const storageLayout = this._storageLayoutMap.get(KIND_{{capitalize query.contract}});
|
||||
assert(storageLayout);
|
||||
|
||||
const result = await this._baseIndexer.getStorageValue(
|
||||
this._storageLayout,
|
||||
storageLayout,
|
||||
blockHash,
|
||||
contractAddress,
|
||||
'{{query.name}}'{{#if query.params.length}},{{/if}}
|
||||
@ -413,10 +429,37 @@ export class Indexer implements IndexerInterface {
|
||||
let eventInfo = {};
|
||||
|
||||
const { topics, data } = logObj;
|
||||
const logDescription = this._contract.parseLog({ data, topics });
|
||||
|
||||
const contract = this._contractMap.get(kind);
|
||||
assert(contract);
|
||||
|
||||
const logDescription = contract.parseLog({ data, topics });
|
||||
|
||||
switch (kind) {
|
||||
{{#each contracts as | contract |}}
|
||||
case KIND_{{capitalize contract.name}}: {
|
||||
({ eventName, eventInfo } = this.parse{{contract.name}}Event(logDescription));
|
||||
|
||||
break;
|
||||
}
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
return {
|
||||
eventName,
|
||||
eventInfo,
|
||||
eventSignature: logDescription.signature
|
||||
};
|
||||
}
|
||||
|
||||
{{#each contracts as | contract |}}
|
||||
parse{{contract.name}}Event (logDescription: ethers.utils.LogDescription): { eventName: string, eventInfo: any } {
|
||||
let eventName = UNKNOWN_EVENT_NAME;
|
||||
let eventInfo = {};
|
||||
|
||||
switch (logDescription.name) {
|
||||
{{#each events as | event |}}
|
||||
{{#each ../events as | event |}}
|
||||
{{#if (compare contract.kind event.kind)}}
|
||||
case {{capitalize event.name}}_EVENT: {
|
||||
eventName = logDescription.name;
|
||||
{{#if event.params}}
|
||||
@ -435,16 +478,17 @@ export class Indexer implements IndexerInterface {
|
||||
|
||||
break;
|
||||
}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
}
|
||||
|
||||
return {
|
||||
eventName,
|
||||
eventInfo,
|
||||
eventSignature: logDescription.signature
|
||||
eventInfo
|
||||
};
|
||||
}
|
||||
|
||||
{{/each}}
|
||||
async getHookStatus (): Promise<HookStatus | undefined> {
|
||||
return this._db.getHookStatus();
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# {{contractName}} Watcher
|
||||
# {{folderName}}
|
||||
|
||||
## Setup
|
||||
|
||||
@ -59,14 +59,12 @@
|
||||
|
||||
* Generating state:
|
||||
|
||||
* Edit the custom hook function `createInitialCheckpoint` (triggered on watch-contract, checkpoint: `true`) in [hooks.ts](./src/hooks.ts) to save an initial checkpoint `IPLDBlock` using the `Indexer` object.
|
||||
* Edit the custom hook function `createInitialState` (triggered if the watcher passes the start block, checkpoint: `true`) in [hooks.ts](./src/hooks.ts) to save an initial state `IPLDBlock` using the `Indexer` object.
|
||||
|
||||
* Edit the custom hook function `createStateDiff` (triggered on a block) in [hooks.ts](./src/hooks.ts) to save the state in a `diff` `IPLDBlock` using the `Indexer` object. The default state (if exists) is updated.
|
||||
|
||||
* Edit the custom hook function `createStateCheckpoint` (triggered just before default and CLI checkpoint) in [hooks.ts](./src/hooks.ts) to save the state in a `checkpoint` `IPLDBlock` using the `Indexer` object.
|
||||
|
||||
* The existing example hooks in [hooks.ts](./src/hooks.ts) are for an `ERC20` contract.
|
||||
|
||||
## Run
|
||||
|
||||
* Run the watcher:
|
||||
@ -75,7 +73,7 @@
|
||||
yarn server
|
||||
```
|
||||
|
||||
GQL console: http://localhost:3008/graphql
|
||||
GQL console: http://localhost:{{port}}/graphql
|
||||
|
||||
* If the watcher is an `active` watcher:
|
||||
|
||||
|
@ -6,3 +6,5 @@ export const MODE_ETH_CALL = 'eth_call';
|
||||
export const MODE_STORAGE = 'storage';
|
||||
export const MODE_ALL = 'all';
|
||||
export const MODE_NONE = 'none';
|
||||
|
||||
export const DEFAULT_PORT = 3008;
|
||||
|
@ -1,13 +1,14 @@
|
||||
import path from 'path';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
import { loadFilesSync } from '@graphql-tools/load-files';
|
||||
|
||||
export function parseSubgraphSchema (subgraphPath: string): any {
|
||||
const subgraphSchemaPath = path.join(path.resolve(subgraphPath), '/schema.graphql');
|
||||
|
||||
assert(fs.existsSync(subgraphSchemaPath));
|
||||
assert(fs.existsSync(subgraphSchemaPath), `Schema file not found at ${subgraphSchemaPath}`);
|
||||
const typesArray = loadFilesSync(subgraphSchemaPath);
|
||||
|
||||
// Get a subgraph-schema DocumentNode with existing types.
|
||||
@ -44,6 +45,19 @@ export function getFieldType (typeNode: any): { typeName: string, array: boolean
|
||||
return { typeName: typeNode.name.value, array: false, nullable: true };
|
||||
}
|
||||
|
||||
export function getContractKindList (subgraphPath: string): string[] {
|
||||
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;
|
||||
}
|
||||
|
||||
function parseType (typeNode: any): any {
|
||||
// Check if 'NamedType' is reached.
|
||||
if (typeNode.kind !== 'NamedType') {
|
||||
|
@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
import { Writable } from 'stream';
|
||||
import assert from 'assert';
|
||||
|
||||
import { Database } from './database';
|
||||
import { Entity } from './entity';
|
||||
@ -26,6 +27,8 @@ export class Visitor {
|
||||
_reset: Reset;
|
||||
_types: Types;
|
||||
|
||||
_contract?: { name: string, kind: string };
|
||||
|
||||
constructor () {
|
||||
this._schema = new Schema();
|
||||
this._resolvers = new Resolvers();
|
||||
@ -37,6 +40,13 @@ export class Visitor {
|
||||
this._types = new Types();
|
||||
}
|
||||
|
||||
setContract (name: string, kind: string): void {
|
||||
this._contract = {
|
||||
name,
|
||||
kind
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor function for function definitions.
|
||||
* @param node ASTNode for a function definition.
|
||||
@ -61,11 +71,13 @@ export class Visitor {
|
||||
|
||||
this._schema.addQuery(name, params, returnType);
|
||||
this._resolvers.addQuery(name, params, returnType);
|
||||
this._indexer.addQuery(MODE_ETH_CALL, name, params, returnType);
|
||||
this._entity.addQuery(name, params, returnType);
|
||||
this._database.addQuery(name, params, returnType);
|
||||
this._client.addQuery(name, params, returnType);
|
||||
this._reset.addQuery(name);
|
||||
|
||||
assert(this._contract);
|
||||
this._indexer.addQuery(this._contract.name, MODE_ETH_CALL, name, params, returnType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,11 +115,13 @@ export class Visitor {
|
||||
|
||||
this._schema.addQuery(name, params, returnType);
|
||||
this._resolvers.addQuery(name, params, returnType);
|
||||
this._indexer.addQuery(MODE_STORAGE, name, params, returnType, stateVariableType);
|
||||
this._entity.addQuery(name, params, returnType);
|
||||
this._database.addQuery(name, params, returnType);
|
||||
this._client.addQuery(name, params, returnType);
|
||||
this._reset.addQuery(name);
|
||||
|
||||
assert(this._contract);
|
||||
this._indexer.addQuery(this._contract.name, MODE_STORAGE, name, params, returnType, stateVariableType);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,7 +135,9 @@ export class Visitor {
|
||||
});
|
||||
|
||||
this._schema.addEventType(name, params);
|
||||
this._indexer.addEvent(name, params);
|
||||
|
||||
assert(this._contract);
|
||||
this._indexer.addEvent(name, params, this._contract.kind);
|
||||
}
|
||||
|
||||
visitSubgraph (subgraphPath?: string): void {
|
||||
@ -160,10 +176,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 names to be passed to the template.
|
||||
* @param contracts Input contracts to be passed to the template.
|
||||
*/
|
||||
exportIndexer (outStream: Writable, inputFileNames: string[]): void {
|
||||
this._indexer.exportIndexer(outStream, inputFileNames);
|
||||
exportIndexer (outStream: Writable, contracts: any[]): void {
|
||||
this._indexer.exportIndexer(outStream, contracts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
43
packages/eden-watcher/codegen.yaml
Normal file
43
packages/eden-watcher/codegen.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
# Config to generate eden-watcher using codegen.
|
||||
# Contracts to watch (required).
|
||||
contracts:
|
||||
# Contract name.
|
||||
- name: EdenNetwork
|
||||
# Contract file path or an url.
|
||||
path: ~/eden/governance/contracts/EdenNetwork.sol
|
||||
# Contract kind (should match that in {subgraphPath}/subgraph.yaml if subgraphPath provided)
|
||||
kind: EdenNetwork
|
||||
|
||||
# Contract name.
|
||||
- name: MerkleDistributor
|
||||
# Contract file path or an url.
|
||||
path: ~/eden/governance/contracts/MerkleDistributor.sol
|
||||
# Contract kind (should match that in {subgraphPath}/subgraph.yaml if subgraphPath provided)
|
||||
kind: EdenNetworkDistribution
|
||||
|
||||
# Contract name.
|
||||
- name: DistributorGovernance
|
||||
# Contract file path or an url.
|
||||
path: ~/eden/governance/contracts/DistributorGovernance.sol
|
||||
# Contract kind (should match that in {subgraphPath}/subgraph.yaml if subgraphPath provided)
|
||||
kind: EdenNetworkGovernance
|
||||
|
||||
# Output folder path (logs output using `stdout` if not provided).
|
||||
outputFolder: ../demo-eden-watcher
|
||||
|
||||
# Code generation mode [eth_call | storage | all | none] (default: all).
|
||||
mode: none
|
||||
|
||||
# Kind of watcher [lazy | active] (default: active).
|
||||
kind: active
|
||||
|
||||
# Watcher server port (default: 3008).
|
||||
port: 3012
|
||||
|
||||
# Flatten the input contract file(s) [true | false] (default: true).
|
||||
flatten: true
|
||||
|
||||
# Path to the subgraph build (optional).
|
||||
subgraphPath: ~/eden/eden-data/packages/subgraph/build
|
||||
|
||||
# NOTE: When passed an *URL* as contract path, it is assumed that it points to an already flattened contract file.
|
Loading…
Reference in New Issue
Block a user