Auto generating state from indexer methods (#277)

* Generate default derived state for Mapping type variables

* Update IPLDBlock in storage methods instead of using a private class variable

* Default state flag for indexer storage methods

* Helper functions to update state objects

* Add checkpoint flag in Contract table and corresponding changes in existing watchers

* Update codegen docs

* Add examples to generated docs

* Turn default state off by default

* Make state parameter to indexer storage methods default to none

* Add method to get prev. state in indexer
This commit is contained in:
prathamesh0 2021-10-20 17:55:54 +05:30 committed by nabarun
parent 2aa0234da5
commit 4ddb8c4af6
28 changed files with 230 additions and 86 deletions

View File

@ -29,16 +29,16 @@
* `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided). * `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided).
* `mode`(alias: `m`): Code generation mode (default: `all`). * `mode`(alias: `m`): Code generation mode (default: `all`).
* `flatten`(alias: `f`): Flatten the input contract file (default: `true`). * `flatten`(alias: `f`): Flatten the input contract file (default: `true`).
* `kind` (alias: `k`): Kind of watcher (default; `active`). * `kind` (alias: `k`): Kind of watcher (default: `active`).
**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* as `input-file`, it is assumed that it points to an already flattened contract file.
Examples: Examples:
Generate code in both `eth_call` and `storage` mode, `active` kind. Generate code in `storage` mode, `lazy` kind.
```bash ```bash
yarn codegen --input-file ./test/examples/contracts/ERC20.sol --contract-name ERC20 --output-folder ../my-erc20-watcher --mode all --kind active yarn codegen --input-file ./test/examples/contracts/ERC721.sol --contract-name ERC721 --output-folder ../my-erc721-watcher --mode storage --kind lazy
``` ```
Generate code in `eth_call` mode using a contract provided by an URL. Generate code in `eth_call` mode using a contract provided by an URL.
@ -47,10 +47,10 @@
yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../my-erc721-watcher --mode eth_call yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../my-erc721-watcher --mode eth_call
``` ```
Generate code in `storage` mode, `lazy` kind. Generate code for `ERC721` in both `eth_call` and `storage` mode, `active` kind.
```bash ```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-file ../../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol --contract-name ERC721 --output-folder ../demo-erc721-watcher --mode all --kind active
``` ```
Generate code for `ERC20` contract in both `eth_call` and `storage` mode, `active` kind: Generate code for `ERC20` contract in both `eth_call` and `storage` mode, `active` kind:
@ -73,7 +73,7 @@
* Create the databases configured in `environments/local.toml`. * Create the databases configured in `environments/local.toml`.
* Update the derived state checkpoint settings in `environments/local.toml`. * Update the state checkpoint settings in `environments/local.toml`.
### Customize ### Customize
@ -81,9 +81,13 @@
* Edit the custom hook function `handleEvent` (triggered on an event) in `src/hooks.ts` to perform corresponding indexing using the `Indexer` object. * Edit the custom hook function `handleEvent` (triggered on an event) in `src/hooks.ts` to perform corresponding indexing using the `Indexer` object.
* Edit the custom hook function `postBlockHook` (triggered on a block) in `src/hooks.ts` to save `IPLDBlock`s using the `Indexer` object. * While using the indexer storage methods for indexing, pass the optional arg. `state` as `diff` or `checkpoint` if default state is desired to be generated using the state variables being indexed else pass `none`.
* Edit the custom hook function `initialCheckpointHook` (triggered on watch-contract) in `src/hooks.ts` to save an initial checkpoint `IPLDBlock` using the `Indexer` object. * 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 `createStateDiff` (triggered on a block) in `src/hooks.ts` to save the state in an `IPLDBlock` using the `Indexer` object. The default state (if exists) is updated.
* The existing example hooks in `src/hooks.ts` are for an `ERC20` contract. * The existing example hooks in `src/hooks.ts` are for an `ERC20` contract.
@ -112,7 +116,7 @@
* To watch a contract: * To watch a contract:
```bash ```bash
yarn watch:contract --address <contract-address> --kind <contract-kind> --starting-block [block-number] yarn watch:contract --address <contract-address> --kind <contract-kind> --checkpoint <true | false> --starting-block [block-number]
``` ```
* To fill a block range: * To fill a block range:

View File

@ -15,9 +15,10 @@ columns:
pgType: varchar pgType: varchar
tsType: string tsType: string
columnType: Column columnType: Column
columnOptions: - name: checkpoint
- option: length pgType: boolean
value: 8 tsType: boolean
columnType: Column
- name: startingBlock - name: startingBlock
pgType: integer pgType: integer
tsType: number tsType: number

View File

@ -32,8 +32,9 @@ export class Indexer {
* @param name Name of the query. * @param name Name of the query.
* @param params Parameters to the query. * @param params Parameters to the query.
* @param returnType Return type for the query. * @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): void { addQuery (mode: string, name: string, params: Array<Param>, returnType: string, stateVariableType?: string): void {
// Check if the query is already added. // Check if the query is already added.
if (this._queries.some(query => query.name === name)) { if (this._queries.some(query => query.name === name)) {
return; return;
@ -45,7 +46,8 @@ export class Indexer {
saveQueryName: '', saveQueryName: '',
params: _.cloneDeep(params), params: _.cloneDeep(params),
returnType, returnType,
mode mode,
stateVariableType
}; };
if (name.charAt(0) === '_') { if (name.charAt(0) === '_') {
@ -69,6 +71,10 @@ export class Indexer {
assert(tsReturnType); assert(tsReturnType);
queryObject.returnType = tsReturnType; queryObject.returnType = tsReturnType;
if (stateVariableType) {
queryObject.stateVariableType = stateVariableType;
}
this._queries.push(queryObject); this._queries.push(queryObject);
} }

View File

@ -292,8 +292,9 @@ export class Schema {
watchContract: { watchContract: {
type: 'Boolean!', type: 'Boolean!',
args: { args: {
contractAddress: 'String!', address: 'String!',
kind: 'String!', kind: 'String!',
checkpoint: 'Boolean!',
startingBlock: 'Int' startingBlock: 'Int'
} }
} }

View File

@ -3,7 +3,7 @@
port = 3008 port = 3008
kind = "{{watcherKind}}" kind = "{{watcherKind}}"
# Checkpointing derived state. # Checkpointing state.
checkpointing = true checkpointing = true
# Checkpoint interval in number of blocks. # Checkpoint interval in number of blocks.

View File

@ -309,11 +309,11 @@ export class Database {
return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events); return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events);
} }
async saveContract (address: string, kind: string, startingBlock: number): Promise<void> { async saveContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
await this._conn.transaction(async (tx) => { await this._conn.transaction(async (tx) => {
const repo = tx.getRepository(Contract); const repo = tx.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, startingBlock, kind); return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
}); });
} }

View File

@ -3,9 +3,8 @@
// //
import assert from 'assert'; import assert from 'assert';
import _ from 'lodash';
import { UNKNOWN_EVENT_NAME } from '@vulcanize/util'; import { UNKNOWN_EVENT_NAME, updateStateForMappingType, updateStateForElementaryType } from '@vulcanize/util';
import { Indexer, ResultEvent } from './indexer'; import { Indexer, ResultEvent } from './indexer';
import { BlockProgress } from './entity/BlockProgress'; import { BlockProgress } from './entity/BlockProgress';
@ -15,19 +14,23 @@ const ACCOUNTS = [
]; ];
/** /**
* Initial checkpoint hook function. * Hook function to create an initial checkpoint.
* @param indexer Indexer instance. * @param indexer Indexer instance.
* @param block Concerned block. * @param block Concerned block.
* @param contractAddress Address of the concerned contract. * @param contractAddress Address of the concerned contract.
*/ */
export async function initialCheckpointHook (indexer: Indexer, block: BlockProgress, contractAddress: string): Promise<void> { export async function createInitialCheckpoint (indexer: Indexer, block: BlockProgress, contractAddress: string): Promise<void> {
assert(indexer);
assert(block);
assert(contractAddress);
// Store the initial state values in an IPLDBlock. // Store the initial state values in an IPLDBlock.
const ipldBlockData: any = {}; let ipldBlockData: any = {};
// Setting the initial balances of accounts. // Setting the initial balances of accounts.
for (const account of ACCOUNTS) { for (const account of ACCOUNTS) {
const balance = await indexer._balances(block.blockHash, contractAddress, account); const balance = await indexer._balances(block.blockHash, contractAddress, account);
_.set(ipldBlockData, `state._balances[${account}]`, balance.value.toString()); ipldBlockData = updateStateForMappingType(ipldBlockData, '_balances', [account], balance.value.toString());
} }
const ipldBlock = await indexer.prepareIPLDBlock(block, contractAddress, ipldBlockData, 'checkpoint'); const ipldBlock = await indexer.prepareIPLDBlock(block, contractAddress, ipldBlockData, 'checkpoint');
@ -35,11 +38,14 @@ export async function initialCheckpointHook (indexer: Indexer, block: BlockProgr
} }
/** /**
* Post-block hook function. * Hook function to create and store state diffs.
* @param indexer Indexer instance that contains methods to fetch the contract varaiable values. * @param indexer Indexer instance that contains methods to fetch the contract varaiable values.
* @param blockHash Block hash of the concerned block. * @param blockHash Block hash of the concerned block.
*/ */
export async function postBlockHook (indexer: Indexer, blockHash: string): Promise<void> { export async function createStateDiff (indexer: Indexer, blockHash: string): Promise<void> {
assert(indexer);
assert(blockHash);
// Get events for current block and make an entry of updated values in IPLDBlock. // Get events for current block and make an entry of updated values in IPLDBlock.
const events = await indexer.getEventsByFilter(blockHash); const events = await indexer.getEventsByFilter(blockHash);
@ -58,7 +64,7 @@ export async function postBlockHook (indexer: Indexer, blockHash: string): Promi
const eventData = indexer.getResultEvent(event); const eventData = indexer.getResultEvent(event);
const ipldBlockData: any = {}; let ipldBlockData: any = {};
switch (event.eventName) { switch (event.eventName) {
case 'Transfer': { case 'Transfer': {
@ -73,8 +79,8 @@ export async function postBlockHook (indexer: Indexer, blockHash: string): Promi
// "0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc": "999999999999999999900" // "0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc": "999999999999999999900"
// } // }
// } // }
_.set(ipldBlockData, `state._balances[${from}]`, fromBalance.value.toString()); ipldBlockData = updateStateForMappingType(ipldBlockData, '_balances', [from], fromBalance.value.toString());
_.set(ipldBlockData, `state._balances[${to}]`, toBalance.value.toString()); ipldBlockData = updateStateForMappingType(ipldBlockData, '_balances', [to], toBalance.value.toString());
break; break;
} }
@ -90,7 +96,7 @@ export async function postBlockHook (indexer: Indexer, blockHash: string): Promi
// } // }
// } // }
// } // }
_.set(ipldBlockData, `state._allowances[${owner}][${spender}]`, allowance.value.toString()); ipldBlockData = updateStateForMappingType(ipldBlockData, '_allowances', [owner, spender], allowance.value.toString());
break; break;
} }
@ -123,10 +129,10 @@ export async function handleEvent (indexer: Indexer, eventData: ResultEvent): Pr
const { from, to } = eventData.event; const { from, to } = eventData.event;
// Update balance entry for sender in the database. // Update balance entry for sender in the database.
await indexer.balanceOf(eventData.block.hash, eventData.contract, from); await indexer._balances(eventData.block.hash, eventData.contract, from);
// Update balance entry for receiver in the database. // Update balance entry for receiver in the database.
await indexer.balanceOf(eventData.block.hash, eventData.contract, to); await indexer._balances(eventData.block.hash, eventData.contract, to);
break; break;
} }
@ -138,7 +144,7 @@ export async function handleEvent (indexer: Indexer, eventData: ResultEvent): Pr
const { owner, spender } = eventData.event; const { owner, spender } = eventData.event;
// Update allowance entry for (owner, spender) combination in the database. // Update allowance entry for (owner, spender) combination in the database.
await indexer.allowance(eventData.block.hash, eventData.contract, owner, spender); await indexer._allowances(eventData.block.hash, eventData.contract, owner, spender);
break; break;
} }

View File

@ -16,7 +16,7 @@ import { BaseProvider } from '@ethersproject/providers';
import * as codec from '@ipld/dag-json'; import * as codec from '@ipld/dag-json';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper'; import { StorageLayout } from '@vulcanize/solidity-mapper';
import { Indexer as BaseIndexer, ValueResult, UNKNOWN_EVENT_NAME, ServerConfig, Where, QueryOptions } from '@vulcanize/util'; import { Indexer as BaseIndexer, ValueResult, UNKNOWN_EVENT_NAME, ServerConfig, Where, QueryOptions, updateStateForElementaryType, updateStateForMappingType } from '@vulcanize/util';
import { Database } from './database'; import { Database } from './database';
import { Contract } from './entity/Contract'; import { Contract } from './entity/Contract';
@ -26,7 +26,7 @@ import { HookStatus } from './entity/HookStatus';
import { BlockProgress } from './entity/BlockProgress'; import { BlockProgress } from './entity/BlockProgress';
import { IPLDBlock } from './entity/IPLDBlock'; import { IPLDBlock } from './entity/IPLDBlock';
import artifacts from './artifacts/{{inputFileName}}.json'; import artifacts from './artifacts/{{inputFileName}}.json';
import { initialCheckpointHook, handleEvent, postBlockHook } from './hooks'; import { createInitialCheckpoint, handleEvent, createStateDiff } from './hooks';
const log = debug('vulcanize:indexer'); const log = debug('vulcanize:indexer');
@ -75,7 +75,7 @@ export class Indexer {
_db: Database _db: Database
_ethClient: EthClient _ethClient: EthClient
_ethProvider: BaseProvider _ethProvider: BaseProvider
_postgraphileClient: EthClient; _postgraphileClient: EthClient
_baseIndexer: BaseIndexer _baseIndexer: BaseIndexer
_serverConfig: ServerConfig _serverConfig: ServerConfig
@ -158,7 +158,14 @@ export class Indexer {
{{#each queries as | query |}} {{#each queries as | query |}}
async {{query.name}} (blockHash: string, contractAddress: string async {{query.name}} (blockHash: string, contractAddress: string
{{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}): Promise<ValueResult> { {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}
{{~#if query.stateVariableType~}}
, state = 'none'): Promise<ValueResult> {
assert(_.includes(['diff', 'checkpoint', 'none'], state));
{{else~}}
): Promise<ValueResult> {
{{/if}}
const entity = await this._db.{{query.getQueryName}}({ blockHash, contractAddress const entity = await this._db.{{query.getQueryName}}({ blockHash, contractAddress
{{~#each query.params}}, {{this.name~}} {{~/each}} }); {{~#each query.params}}, {{this.name~}} {{~/each}} });
if (entity) { if (entity) {
@ -203,6 +210,28 @@ export class Indexer {
await this._db.{{query.saveQueryName}}({ blockHash, contractAddress await this._db.{{query.saveQueryName}}({ blockHash, contractAddress
{{~#each query.params}}, {{this.name~}} {{/each}}, value: result.value, proof: JSONbig.stringify(result.proof) }); {{~#each query.params}}, {{this.name~}} {{/each}}, value: result.value, proof: JSONbig.stringify(result.proof) });
{{#if query.stateVariableType}}
{{#if (compare query.stateVariableType 'Mapping')}}
if (state !== 'none') {
const stateUpdate = updateStateForMappingType({}, '{{query.name}}', [
{{~#each query.params}}
{{~this.name}}.toString() {{~#unless @last}}, {{/unless~}}
{{/each~}}
], result.value.toString());
await this.storeIPLDData(blockHash, contractAddress, stateUpdate, state);
}
{{else if (compare query.stateVariableType 'ElementaryTypeName')}}
if (state !== 'none') {
const stateUpdate = updateStateForElementaryType({}, '{{query.name}}', result.value.toString());
await this.storeIPLDData(blockHash, contractAddress, stateUpdate, state);
}
{{else}}
assert(state === 'none', 'Type not supported for default state.');
{{/if}}
{{/if}}
return result; return result;
} }
@ -210,8 +239,8 @@ export class Indexer {
async processBlock (job: any): Promise<void> { async processBlock (job: any): Promise<void> {
const { data: { blockHash } } = job; const { data: { blockHash } } = job;
// Call custom post-block hook. // Call custom stateDiff hook.
await postBlockHook(this, blockHash); await createStateDiff(this, blockHash);
} }
async processCheckpoint (job: any): Promise<void> { async processCheckpoint (job: any): Promise<void> {
@ -225,11 +254,12 @@ export class Indexer {
// Get all the contracts. // Get all the contracts.
const contracts = await this._db.getContracts({}); const contracts = await this._db.getContracts({});
const contractAddresses = contracts.map(contract => contract.address);
// For each contractAddress, merge the diff till now to create a checkpoint. // For each contract, merge the diff till now to create a checkpoint.
for (const contractAddress of contractAddresses) { for (const contract of contracts) {
await this.createCheckpoint(contractAddress, currentBlockHash, checkpointInterval); if (contract.checkpoint) {
await this.createCheckpoint(contract.address, currentBlockHash, checkpointInterval);
}
} }
} }
@ -303,6 +333,15 @@ export class Indexer {
return ipldBlocks[0]; return ipldBlocks[0];
} }
async getPrevState (blockHash: string, contractAddress: string, kind?: string): Promise<any> {
const ipldBlock = await this.getPrevIPLDBlock(blockHash, contractAddress, kind);
if (ipldBlock) {
const data = codec.decode(Buffer.from(ipldBlock.data)) as any;
return data.state;
}
}
async getPrevIPLDBlock (blockHash: string, contractAddress: string, kind?: string): Promise<IPLDBlock | undefined> { async getPrevIPLDBlock (blockHash: string, contractAddress: string, kind?: string): Promise<IPLDBlock | undefined> {
const dbTx = await this._db.createTransactionRunner(); const dbTx = await this._db.createTransactionRunner();
let res; let res;
@ -336,11 +375,21 @@ export class Indexer {
return res; return res;
} }
async storeIPLDData (blockHash: string, contractAddress: string, data: any, kind: string): Promise<void> {
const block = await this.getBlockProgress(blockHash);
assert(block);
const ipldBlock = await this.prepareIPLDBlock(block, contractAddress, data, kind);
await this.saveOrUpdateIPLDBlock(ipldBlock);
}
async saveOrUpdateIPLDBlock (ipldBlock: IPLDBlock): Promise<IPLDBlock> { async saveOrUpdateIPLDBlock (ipldBlock: IPLDBlock): Promise<IPLDBlock> {
return this._db.saveOrUpdateIPLDBlock(ipldBlock); return this._db.saveOrUpdateIPLDBlock(ipldBlock);
} }
async prepareIPLDBlock (block: BlockProgress, contractAddress: string, data: any, kind: string):Promise<any> { async prepareIPLDBlock (block: BlockProgress, contractAddress: string, data: any, kind: string):Promise<any> {
assert(_.includes(['diff', 'checkpoint'], kind));
// Get an existing IPLDBlock for current block and contractAddress. // Get an existing IPLDBlock for current block and contractAddress.
const currentIPLDBlocks = await this.getIPLDBlocks(block, contractAddress, 'diff'); const currentIPLDBlocks = await this.getIPLDBlocks(block, contractAddress, 'diff');
// There can be only one IPLDBlock for a (block, contractAddress, 'diff') combination. // There can be only one IPLDBlock for a (block, contractAddress, 'diff') combination.
@ -364,7 +413,7 @@ export class Indexer {
// Setting the meta-data for an IPLDBlock (done only once per block). // Setting the meta-data for an IPLDBlock (done only once per block).
data.meta = { data.meta = {
id: contractAddress, id: contractAddress,
kind: kind || 'diff', kind,
parent: { parent: {
'/': parentIPLDBlock ? parentIPLDBlock.cid : null '/': parentIPLDBlock ? parentIPLDBlock.cid : null
}, },
@ -441,16 +490,21 @@ export class Indexer {
return { eventName, eventInfo }; return { eventName, eventInfo };
} }
async watchContract (address: string, kind: string, startingBlock: number): Promise<boolean> { async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<boolean> {
// Always use the checksum address (https://docs.ethers.io/v5/api/utils/address/#utils-getAddress). // Use the checksum address (https://docs.ethers.io/v5/api/utils/address/#utils-getAddress) if input to address is a contract address.
await this._db.saveContract(ethers.utils.getAddress(address), kind, startingBlock); // If a contract identifier is passed as address instead, no need to convert to checksum address.
// Customize: use the kind input to filter out non-contract-address input to address.
const formattedAddress = (kind === '__protocol__') ? address : ethers.utils.getAddress(address);
await this._db.saveContract(formattedAddress, kind, checkpoint, startingBlock);
if (checkpoint) {
// Getting the current block. // Getting the current block.
const currentBlock = await this._db.getLatestBlockProgress(); const currentBlock = await this._db.getLatestBlockProgress();
assert(currentBlock); assert(currentBlock);
// Call custom initial checkpoint hook. // Call custom initial checkpoint hook.
await initialCheckpointHook(this, currentBlock, address); await createInitialCheckpoint(this, currentBlock, address);
}
return true; return true;
} }

View File

@ -39,7 +39,7 @@
* Update the `upstream` config in the [config file](./environments/local.toml) and provide the `ipld-eth-server` GQL API and the `indexer-db` postgraphile endpoints. * Update the `upstream` config in the [config file](./environments/local.toml) and provide the `ipld-eth-server` GQL API and the `indexer-db` postgraphile endpoints.
* Update the [config](./environments/local.toml) with derived state checkpoint settings. * Update the [config](./environments/local.toml) with state checkpoint settings.
## Customize ## Customize
@ -47,9 +47,13 @@
* Edit the custom hook function `handleEvent` (triggered on an event) in [hooks.ts](./src/hooks.ts) to perform corresponding indexing using the `Indexer` object. * Edit the custom hook function `handleEvent` (triggered on an event) in [hooks.ts](./src/hooks.ts) to perform corresponding indexing using the `Indexer` object.
* Edit the custom hook function `postBlockHook` (triggered on a block) in [hooks.ts](./src/hooks.ts) to save `IPLDBlock`s using the `Indexer` object. * While using the indexer storage methods for indexing, pass the optional arg. `state` as `diff` or `checkpoint` if default state is desired to be generated using the state variables being indexed else pass `none`.
* Edit the custom hook function `initialCheckpointHook` (triggered on watch-contract) in [hooks.ts](./src/hooks.ts) to save an initial checkpoint `IPLDBlock` using the `Indexer` object. * 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 `createStateDiff` (triggered on a block) in [hooks.ts](./src/hooks.ts) to save the state in an `IPLDBlock` using the `Indexer` object. The default state (if exists) is updated.
* The existing example hooks in [hooks.ts](./src/hooks.ts) are for an `ERC20` contract. * The existing example hooks in [hooks.ts](./src/hooks.ts) are for an `ERC20` contract.
@ -74,7 +78,26 @@ GQL console: http://localhost:3008/graphql
* To watch a contract: * To watch a contract:
```bash ```bash
yarn watch:contract --address <contract-address> --kind {{contractName}} --starting-block [block-number] yarn watch:contract --address <contract-address> --kind <contract-kind> --checkpoint <true | false> --starting-block [block-number]
```
* `address`: Address or identifier of the contract to be watched.
* `kind`: Kind of the contract.
* `checkpoint`: Turn checkpointing on (`true` | `false`).
* `starting-block`: Starting block for the contract (default: `1`).
Examples:
Watch a contract with its address and checkpointing on:
```bash
yarn watch:contract --address 0x1F78641644feB8b64642e833cE4AFE93DD6e7833 --kind ERC20 --checkpoint true
```
Watch a contract with its identifier and checkpointing on:
```bash
yarn watch:contract --address MyProtocol --kind protocol --checkpoint true
``` ```
* To fill a block range: * To fill a block range:
@ -83,8 +106,14 @@ GQL console: http://localhost:3008/graphql
yarn fill --start-block <from-block> --end-block <to-block> yarn fill --start-block <from-block> --end-block <to-block>
``` ```
* `start-block`: Block number to start filling from.
* `end-block`: Block number till which to fill.
* To create a checkpoint for a contract: * To create a checkpoint for a contract:
```bash ```bash
yarn checkpoint --address <contract-address> --block-hash [block-hash] yarn checkpoint --address <contract-address> --block-hash [block-hash]
``` ```
* `address`: Address or identifier of the contract for which to create a checkpoint.
* `block-hash`: Hash of the block at which to create the checkpoint (default: current block hash).

View File

@ -34,9 +34,9 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch
}, },
Mutation: { Mutation: {
watchContract: (_: any, { contractAddress, kind, startingBlock = 1 }: { contractAddress: string, kind: string, startingBlock: number }): Promise<boolean> => { watchContract: (_: any, { address, kind, checkpoint, startingBlock = 1 }: { address: string, kind: string, checkpoint: boolean, startingBlock: number }): Promise<boolean> => {
log('watchContract', contractAddress, kind, startingBlock); log('watchContract', address, kind, checkpoint, startingBlock);
return indexer.watchContract(contractAddress, kind, startingBlock); return indexer.watchContract(address, kind, checkpoint, startingBlock);
} }
}, },

View File

@ -41,6 +41,12 @@ const main = async (): Promise<void> => {
demandOption: true, demandOption: true,
describe: 'Kind of contract' describe: 'Kind of contract'
}, },
checkpoint: {
type: 'boolean',
require: true,
demandOption: true,
describe: 'Turn checkpointing on'
},
startingBlock: { startingBlock: {
type: 'number', type: 'number',
default: 1, default: 1,
@ -79,7 +85,7 @@ const main = async (): Promise<void> => {
const ethProvider = getDefaultProvider(rpcProviderEndpoint); const ethProvider = getDefaultProvider(rpcProviderEndpoint);
const indexer = new Indexer(serverConfig, db, ethClient, postgraphileClient, ethProvider); const indexer = new Indexer(serverConfig, db, ethClient, postgraphileClient, ethProvider);
await indexer.watchContract(argv.address, argv.kind, argv.startingBlock); await indexer.watchContract(argv.address, argv.kind, argv.checkpoint, argv.startingBlock);
await db.close(); await db.close();
}; };

View File

@ -60,11 +60,13 @@ export class Visitor {
stateVariableDeclarationVisitor (node: any): void { stateVariableDeclarationVisitor (node: any): void {
// TODO Handle multiples variables in a single line. // TODO Handle multiples variables in a single line.
// TODO Handle array types. // TODO Handle array types.
const name: string = node.variables[0].name; const variable = node.variables[0];
const name: string = variable.name;
const stateVariableType: string = variable.typeName.type;
const params: Param[] = []; const params: Param[] = [];
let typeName = node.variables[0].typeName; let typeName = variable.typeName;
let numParams = 0; let numParams = 0;
// If the variable type is mapping, extract key as a param: // If the variable type is mapping, extract key as a param:
@ -79,7 +81,7 @@ export class Visitor {
this._schema.addQuery(name, params, returnType); this._schema.addQuery(name, params, returnType);
this._resolvers.addQuery(name, params, returnType); this._resolvers.addQuery(name, params, returnType);
this._indexer.addQuery(MODE_STORAGE, name, params, returnType); this._indexer.addQuery(MODE_STORAGE, name, params, returnType, stateVariableType);
this._entity.addQuery(name, params, returnType); this._entity.addQuery(name, params, returnType);
this._database.addQuery(name, params, returnType); this._database.addQuery(name, params, returnType);
this._client.addQuery(name, params, returnType); this._client.addQuery(name, params, returnType);

View File

@ -29,6 +29,11 @@ import { Indexer } from '../indexer';
demandOption: true, demandOption: true,
describe: 'Address of the deployed contract' describe: 'Address of the deployed contract'
}, },
checkpoint: {
type: 'boolean',
default: false,
describe: 'Turn checkpointing on'
},
startingBlock: { startingBlock: {
type: 'number', type: 'number',
default: 1, default: 1,
@ -55,7 +60,7 @@ import { Indexer } from '../indexer';
const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, jobQueue, mode); const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, jobQueue, mode);
await indexer.watchContract(argv.address, argv.startingBlock); await indexer.watchContract(argv.address, argv.checkpoint, argv.startingBlock);
await db.close(); await db.close();
await jobQueue.stop(); await jobQueue.stop();

View File

@ -114,10 +114,10 @@ export class Database {
return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events); return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events);
} }
async saveContract (queryRunner: QueryRunner, address: string, kind: string, startingBlock: number): Promise<Contract> { async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract); const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, startingBlock, kind); return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
} }
async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> { async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {

View File

@ -16,6 +16,9 @@ export class Contract {
@Column('varchar', { length: 8 }) @Column('varchar', { length: 8 })
kind!: string; kind!: string;
@Column('boolean')
checkpoint!: boolean;
@Column('integer') @Column('integer')
startingBlock!: number; startingBlock!: number;
} }

View File

@ -305,8 +305,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address); return this._baseIndexer.isWatchedContract(address);
} }
async watchContract (address: string, startingBlock: number): Promise<void> { async watchContract (address: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, CONTRACT_KIND, startingBlock); return this._baseIndexer.watchContract(address, CONTRACT_KIND, checkpoint, startingBlock);
} }
async saveEventEntity (dbEvent: Event): Promise<Event> { async saveEventEntity (dbEvent: Event): Promise<Event> {

View File

@ -34,9 +34,9 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch
}, },
Mutation: { Mutation: {
watchToken: async (_: any, { token, startingBlock = 1 }: { token: string, startingBlock: number }): Promise<boolean> => { watchToken: async (_: any, { token, checkpoint = false, startingBlock = 1 }: { token: string, checkpoint: boolean, startingBlock: number }): Promise<boolean> => {
log('watchToken', token, startingBlock); log('watchToken', token, checkpoint, startingBlock);
await indexer.watchContract(token, startingBlock); await indexer.watchContract(token, checkpoint, startingBlock);
return true; return true;
} }

View File

@ -158,6 +158,7 @@ type Mutation {
# Actively watch and index data for the token. # Actively watch and index data for the token.
watchToken( watchToken(
token: String! token: String!
checkpoint: Boolean
startingBlock: Int startingBlock: Int
): Boolean! ): Boolean!
} }

View File

@ -35,6 +35,11 @@ import { Indexer } from '../indexer';
demandOption: true, demandOption: true,
describe: 'Kind of contract (factory|pool|nfpm)' describe: 'Kind of contract (factory|pool|nfpm)'
}, },
checkpoint: {
type: 'boolean',
default: false,
describe: 'Turn checkpointing on'
},
startingBlock: { startingBlock: {
type: 'number', type: 'number',
default: 1, default: 1,
@ -62,7 +67,7 @@ import { Indexer } from '../indexer';
const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, jobQueue); const indexer = new Indexer(db, ethClient, postgraphileClient, ethProvider, jobQueue);
await indexer.init(); await indexer.init();
await indexer.watchContract(argv.address, argv.kind, argv.startingBlock); await indexer.watchContract(argv.address, argv.kind, argv.checkpoint, argv.startingBlock);
await db.close(); await db.close();
await jobQueue.stop(); await jobQueue.stop();

View File

@ -51,10 +51,10 @@ export class Database implements DatabaseInterface {
return this._baseDatabase.getContracts(repo); return this._baseDatabase.getContracts(repo);
} }
async saveContract (queryRunner: QueryRunner, address: string, kind: string, startingBlock: number): Promise<Contract> { async saveContract (queryRunner: QueryRunner, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<Contract> {
const repo = queryRunner.manager.getRepository(Contract); const repo = queryRunner.manager.getRepository(Contract);
return this._baseDatabase.saveContract(repo, address, startingBlock, kind); return this._baseDatabase.saveContract(repo, address, kind, checkpoint, startingBlock);
} }
async createTransactionRunner (): Promise<QueryRunner> { async createTransactionRunner (): Promise<QueryRunner> {

View File

@ -20,6 +20,9 @@ export class Contract {
@Column('varchar', { length: 8 }) @Column('varchar', { length: 8 })
kind!: string; kind!: string;
@Column('boolean')
checkpoint!: boolean;
@Column('integer') @Column('integer')
startingBlock!: number; startingBlock!: number;
} }

View File

@ -102,7 +102,7 @@ export class Indexer implements IndexerInterface {
switch (re.event.__typename) { switch (re.event.__typename) {
case 'PoolCreatedEvent': { case 'PoolCreatedEvent': {
const poolContract = ethers.utils.getAddress(re.event.pool); const poolContract = ethers.utils.getAddress(re.event.pool);
await this.watchContract(poolContract, KIND_POOL, dbEvent.block.blockNumber); await this.watchContract(poolContract, KIND_POOL, false, dbEvent.block.blockNumber);
} }
} }
} }
@ -353,8 +353,8 @@ export class Indexer implements IndexerInterface {
return this._baseIndexer.isWatchedContract(address); return this._baseIndexer.isWatchedContract(address);
} }
async watchContract (address: string, kind: string, startingBlock: number): Promise<void> { async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, startingBlock); return this._baseIndexer.watchContract(address, kind, checkpoint, startingBlock);
} }
cacheContract (contract: Contract): void { cacheContract (contract: Contract): void {

View File

@ -30,7 +30,7 @@ const deployFactoryContract = async (indexer: Indexer, signer: Signer): Promise<
assert(factory.address, 'Factory contract not deployed.'); assert(factory.address, 'Factory contract not deployed.');
// Watch factory contract. // Watch factory contract.
await indexer.watchContract(factory.address, 'factory', 100); await indexer.watchContract(factory.address, 'factory', false, 100);
return factory; return factory;
}; };
@ -45,7 +45,7 @@ const deployNFPMContract = async (indexer: Indexer, signer: Signer, factory: Con
assert(nfpm.address, 'NFPM contract not deployed.'); assert(nfpm.address, 'NFPM contract not deployed.');
// Watch NFPM contract. // Watch NFPM contract.
await indexer.watchContract(nfpm.address, 'nfpm', 100); await indexer.watchContract(nfpm.address, 'nfpm', false, 100);
}; };
const main = async () => { const main = async () => {

View File

@ -13,3 +13,4 @@ export * from './src/types';
export * from './src/indexer'; export * from './src/indexer';
export * from './src/job-runner'; export * from './src/job-runner';
export * from './src/graph-decimal'; export * from './src/graph-decimal';
export * from './src/ipldHelper';

View File

@ -538,13 +538,13 @@ export class Database {
.getMany(); .getMany();
} }
async saveContract (repo: Repository<ContractInterface>, address: string, startingBlock: number, kind?: string): Promise<ContractInterface> { async saveContract (repo: Repository<ContractInterface>, address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<ContractInterface> {
const contract = await repo const contract = await repo
.createQueryBuilder() .createQueryBuilder()
.where('address = :address', { address }) .where('address = :address', { address })
.getOne(); .getOne();
const entity = repo.create({ address, kind, startingBlock }); const entity = repo.create({ address, kind, checkpoint, startingBlock });
// If contract already present, overwrite fields. // If contract already present, overwrite fields.
if (contract) { if (contract) {

View File

@ -308,7 +308,7 @@ export class Indexer {
return this._watchedContracts[address]; return this._watchedContracts[address];
} }
async watchContract (address: string, kind: string, startingBlock: number): Promise<void> { async watchContract (address: string, kind: string, checkpoint: boolean, startingBlock: number): Promise<void> {
assert(this._db.saveContract); assert(this._db.saveContract);
const dbTx = await this._db.createTransactionRunner(); const dbTx = await this._db.createTransactionRunner();
@ -316,7 +316,7 @@ export class Indexer {
const contractAddress = ethers.utils.getAddress(address); const contractAddress = ethers.utils.getAddress(address);
try { try {
const contract = await this._db.saveContract(dbTx, contractAddress, kind, startingBlock); const contract = await this._db.saveContract(dbTx, contractAddress, kind, checkpoint, startingBlock);
this.cacheContract(contract); this.cacheContract(contract);
await dbTx.commitTransaction(); await dbTx.commitTransaction();

View File

@ -0,0 +1,16 @@
import _ from 'lodash';
export const updateStateForElementaryType = (initialObject: any, stateVariable: string, value: string): any => {
const object = _.cloneDeep(initialObject);
const path = ['state', stateVariable];
return _.set(object, path, value);
};
export const updateStateForMappingType = (initialObject: any, stateVariable: string, keys: string[], value: string): any => {
const object = _.cloneDeep(initialObject);
keys.unshift('state', stateVariable);
// Use _.setWith() with Object as customizer as _.set() treats numeric value in path as an index to an array.
return _.setWith(object, keys, value, Object);
};

View File

@ -48,6 +48,7 @@ export interface ContractInterface {
address: string; address: string;
startingBlock: number; startingBlock: number;
kind: string; kind: string;
checkpoint: boolean;
} }
export interface IndexerInterface { export interface IndexerInterface {
@ -101,5 +102,5 @@ export interface DatabaseInterface {
saveEventEntity (queryRunner: QueryRunner, entity: EventInterface): Promise<EventInterface>; saveEventEntity (queryRunner: QueryRunner, entity: EventInterface): Promise<EventInterface>;
removeEntities<Entity> (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions<Entity> | FindConditions<Entity>): Promise<void>; removeEntities<Entity> (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindManyOptions<Entity> | FindConditions<Entity>): Promise<void>;
getContracts?: () => Promise<ContractInterface[]> getContracts?: () => Promise<ContractInterface[]>
saveContract?: (queryRunner: QueryRunner, contractAddress: string, kind: string, startingBlock: number) => Promise<ContractInterface> saveContract?: (queryRunner: QueryRunner, contractAddress: string, kind: string, checkpoint: boolean, startingBlock: number) => Promise<ContractInterface>
} }