mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 20:36:19 +00:00
Basic GQL API to get traces, save to db. (#76)
This commit is contained in:
parent
8507bc8b9c
commit
6a33c704b8
@ -24,6 +24,7 @@
|
|||||||
[upstream]
|
[upstream]
|
||||||
gqlEndpoint = "http://127.0.0.1:8083/graphql"
|
gqlEndpoint = "http://127.0.0.1:8083/graphql"
|
||||||
gqlSubscriptionEndpoint = "http://127.0.0.1:5000/graphql"
|
gqlSubscriptionEndpoint = "http://127.0.0.1:5000/graphql"
|
||||||
|
traceProviderEndpoint = "http://127.0.0.1:8545"
|
||||||
|
|
||||||
[upstream.cache]
|
[upstream.cache]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "DEBUG=vulcanize:* nodemon src/server.ts -f environments/local.toml",
|
"server": "DEBUG=vulcanize:* nodemon src/server.ts -f environments/local.toml",
|
||||||
"server:mock": "MOCK=1 nodemon src/server.ts -f environments/local.toml",
|
|
||||||
"test": "mocha -r ts-node/register src/**/*.spec.ts",
|
"test": "mocha -r ts-node/register src/**/*.spec.ts",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
@ -26,6 +25,7 @@
|
|||||||
"@vulcanize/cache": "^0.1.0",
|
"@vulcanize/cache": "^0.1.0",
|
||||||
"@vulcanize/ipld-eth-client": "^0.1.0",
|
"@vulcanize/ipld-eth-client": "^0.1.0",
|
||||||
"@vulcanize/solidity-mapper": "^0.1.0",
|
"@vulcanize/solidity-mapper": "^0.1.0",
|
||||||
|
"@vulcanize/tracing-client": "^0.1.0",
|
||||||
"apollo-server-express": "^2.25.0",
|
"apollo-server-express": "^2.25.0",
|
||||||
"apollo-type-bigint": "^0.1.3",
|
"apollo-type-bigint": "^0.1.3",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
|
@ -17,6 +17,7 @@ export interface Config {
|
|||||||
upstream: {
|
upstream: {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
gqlSubscriptionEndpoint: string;
|
gqlSubscriptionEndpoint: string;
|
||||||
|
traceProviderEndpoint: string;
|
||||||
cache: CacheConfig
|
cache: CacheConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { Connection, ConnectionOptions, createConnection } from 'typeorm';
|
import { Connection, ConnectionOptions, createConnection, DeepPartial } from 'typeorm';
|
||||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||||
|
|
||||||
import { Address } from './entity/Address';
|
import { Address } from './entity/Address';
|
||||||
|
import { Trace } from './entity/Trace';
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
_config: ConnectionOptions
|
_config: ConnectionOptions
|
||||||
@ -50,4 +51,17 @@ export class Database {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTrace (txHash: string): Promise<Trace | undefined> {
|
||||||
|
return this._conn.getRepository(Trace)
|
||||||
|
.createQueryBuilder('trace')
|
||||||
|
.where('tx_hash = :txHash', { txHash })
|
||||||
|
.getOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTrace ({ txHash, blockNumber, blockHash, trace }: DeepPartial<Trace>): Promise<Trace> {
|
||||||
|
const repo = this._conn.getRepository(Trace);
|
||||||
|
const entity = repo.create({ txHash, blockNumber, blockHash, trace });
|
||||||
|
return repo.save(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
20
packages/address-watcher/src/entity/Trace.ts
Normal file
20
packages/address-watcher/src/entity/Trace.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['txHash'], { unique: true })
|
||||||
|
export class Trace {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Column('varchar', { length: 66 })
|
||||||
|
txHash!: string;
|
||||||
|
|
||||||
|
@Column('numeric')
|
||||||
|
blockNumber!: number;
|
||||||
|
|
||||||
|
@Column('varchar', { length: 66 })
|
||||||
|
blockHash!: string;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
trace!: string;
|
||||||
|
}
|
@ -1,30 +1,34 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
// import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { PubSub } from 'apollo-server-express';
|
import { PubSub } from 'apollo-server-express';
|
||||||
|
|
||||||
import { EthClient } from '@vulcanize/ipld-eth-client';
|
import { EthClient } from '@vulcanize/ipld-eth-client';
|
||||||
import { GetStorageAt } from '@vulcanize/solidity-mapper';
|
import { GetStorageAt } from '@vulcanize/solidity-mapper';
|
||||||
|
import { TracingClient } from '@vulcanize/tracing-client';
|
||||||
|
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
|
|
||||||
// const log = debug('vulcanize:indexer');
|
const log = debug('vulcanize:indexer');
|
||||||
|
|
||||||
export class Indexer {
|
export class Indexer {
|
||||||
_db: Database
|
_db: Database
|
||||||
_ethClient: EthClient
|
_ethClient: EthClient
|
||||||
_pubsub: PubSub
|
_pubsub: PubSub
|
||||||
_getStorageAt: GetStorageAt
|
_getStorageAt: GetStorageAt
|
||||||
|
_tracingClient: TracingClient
|
||||||
|
|
||||||
constructor (db: Database, ethClient: EthClient, pubsub: PubSub) {
|
constructor (db: Database, ethClient: EthClient, pubsub: PubSub, tracingClient: TracingClient) {
|
||||||
assert(db);
|
assert(db);
|
||||||
assert(ethClient);
|
assert(ethClient);
|
||||||
assert(pubsub);
|
assert(pubsub);
|
||||||
|
assert(tracingClient);
|
||||||
|
|
||||||
this._db = db;
|
this._db = db;
|
||||||
this._ethClient = ethClient;
|
this._ethClient = ethClient;
|
||||||
this._pubsub = pubsub;
|
this._pubsub = pubsub;
|
||||||
this._getStorageAt = this._ethClient.getStorageAt.bind(this._ethClient);
|
this._getStorageAt = this._ethClient.getStorageAt.bind(this._ethClient);
|
||||||
|
this._tracingClient = tracingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEventIterator (): AsyncIterator<any> {
|
getEventIterator (): AsyncIterator<any> {
|
||||||
@ -43,4 +47,30 @@ export class Indexer {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async traceTx (txHash: string): Promise<any> {
|
||||||
|
let entity = await this._db.getTrace(txHash);
|
||||||
|
if (entity) {
|
||||||
|
log('traceTx: db hit');
|
||||||
|
} else {
|
||||||
|
log('traceTx: db miss, fetching from tracing API server');
|
||||||
|
|
||||||
|
const tx = await this._tracingClient.getTx(txHash);
|
||||||
|
const trace = await this._tracingClient.getTxTrace(txHash, 'callTraceWithAddresses', '15s');
|
||||||
|
|
||||||
|
entity = await this._db.saveTrace({
|
||||||
|
txHash,
|
||||||
|
blockNumber: tx.blockNumber,
|
||||||
|
blockHash: tx.blockHash,
|
||||||
|
trace: JSON.stringify(trace)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
txHash,
|
||||||
|
blockNumber: entity.blockNumber,
|
||||||
|
blockHash: entity.blockHash,
|
||||||
|
trace: entity.trace
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,11 @@ export const createResolvers = async (indexer: Indexer): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Query: {}
|
Query: {
|
||||||
|
traceTx: async (_: any, { txHash }: { txHash: string }): Promise<any> => {
|
||||||
|
log('traceTx', txHash);
|
||||||
|
return indexer.traceTx(txHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,17 @@ import { gql } from '@apollo/client/core';
|
|||||||
export default gql`
|
export default gql`
|
||||||
# Types
|
# Types
|
||||||
|
|
||||||
# Watched event, include additional context over and above the event data.
|
type TxTrace {
|
||||||
type WatchedEvent {
|
|
||||||
blockHash: String!
|
|
||||||
txHash: String!
|
txHash: String!
|
||||||
|
blockNumber: Int!
|
||||||
|
blockHash: String!
|
||||||
|
trace: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Watched address event, include additional context over and above the event data.
|
||||||
|
type WatchedAddressEvent {
|
||||||
address: String!
|
address: String!
|
||||||
|
txTrace: TxTrace!
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -16,10 +22,23 @@ type WatchedEvent {
|
|||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|
||||||
queryAppearances(
|
#
|
||||||
|
# Developer API methods
|
||||||
|
#
|
||||||
|
|
||||||
|
appearances(
|
||||||
address: String!
|
address: String!
|
||||||
|
fromBlockNumber: Int!
|
||||||
|
toBlockNumber: Int!
|
||||||
|
): [TxTrace!]
|
||||||
|
|
||||||
|
#
|
||||||
|
# Low level utility methods
|
||||||
|
#
|
||||||
|
|
||||||
|
traceTx(
|
||||||
txHash: String!
|
txHash: String!
|
||||||
): [String!]
|
): TxTrace
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -28,7 +47,7 @@ type Query {
|
|||||||
type Subscription {
|
type Subscription {
|
||||||
|
|
||||||
# Watch for token events (at head of chain).
|
# Watch for token events (at head of chain).
|
||||||
onAddressEvent: WatchedEvent!
|
onAddressEvent: WatchedAddressEvent!
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -10,6 +10,7 @@ import { createServer } from 'http';
|
|||||||
|
|
||||||
import { getCache } from '@vulcanize/cache';
|
import { getCache } from '@vulcanize/cache';
|
||||||
import { EthClient } from '@vulcanize/ipld-eth-client';
|
import { EthClient } from '@vulcanize/ipld-eth-client';
|
||||||
|
import { TracingClient } from '@vulcanize/tracing-client';
|
||||||
|
|
||||||
import typeDefs from './schema';
|
import typeDefs from './schema';
|
||||||
|
|
||||||
@ -44,18 +45,21 @@ export const main = async (): Promise<any> => {
|
|||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
assert(upstream, 'Missing upstream config');
|
assert(upstream, 'Missing upstream config');
|
||||||
const { gqlEndpoint, gqlSubscriptionEndpoint, cache: cacheConfig } = upstream;
|
const { gqlEndpoint, gqlSubscriptionEndpoint, traceProviderEndpoint, cache: cacheConfig } = upstream;
|
||||||
assert(gqlEndpoint, 'Missing upstream gqlEndpoint');
|
assert(gqlEndpoint, 'Missing upstream gqlEndpoint');
|
||||||
assert(gqlSubscriptionEndpoint, 'Missing upstream gqlSubscriptionEndpoint');
|
assert(gqlSubscriptionEndpoint, 'Missing upstream gqlSubscriptionEndpoint');
|
||||||
|
assert(traceProviderEndpoint, 'Missing upstream traceProviderEndpoint');
|
||||||
|
|
||||||
const cache = await getCache(cacheConfig);
|
const cache = await getCache(cacheConfig);
|
||||||
|
|
||||||
const ethClient = new EthClient({ gqlEndpoint, gqlSubscriptionEndpoint, cache });
|
const ethClient = new EthClient({ gqlEndpoint, gqlSubscriptionEndpoint, cache });
|
||||||
|
|
||||||
|
const tracingClient = new TracingClient(traceProviderEndpoint);
|
||||||
|
|
||||||
// Note: In-memory pubsub works fine for now, as each watcher is a single process anyway.
|
// Note: In-memory pubsub works fine for now, as each watcher is a single process anyway.
|
||||||
// Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries
|
// Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries
|
||||||
const pubsub = new PubSub();
|
const pubsub = new PubSub();
|
||||||
const indexer = new Indexer(db, ethClient, pubsub);
|
const indexer = new Indexer(db, ethClient, pubsub, tracingClient);
|
||||||
|
|
||||||
const resolvers = await createResolvers(indexer);
|
const resolvers = await createResolvers(indexer);
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './src/tracing';
|
@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
|
||||||
import { getCallTrace } from '../tracing';
|
import { TracingClient } from '../tracing';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const argv = await yargs.parserConfiguration({
|
const argv = await yargs.parserConfiguration({
|
||||||
@ -45,7 +45,8 @@ import { getCallTrace } from '../tracing';
|
|||||||
|
|
||||||
const txData = JSON.parse(fs.readFileSync(argv.txFile).toString("utf-8"));
|
const txData = JSON.parse(fs.readFileSync(argv.txFile).toString("utf-8"));
|
||||||
|
|
||||||
const result = await getCallTrace(argv.providerUrl, argv.block, txData, tracer);
|
const tracingClient = new TracingClient(argv.providerUrl);
|
||||||
|
const result = await tracingClient.getCallTrace(argv.block, txData, tracer);
|
||||||
|
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
})();
|
})();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
|
||||||
import { getTxTrace } from '../tracing';
|
import { TracingClient } from '../tracing';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const argv = await yargs.parserConfiguration({
|
const argv = await yargs.parserConfiguration({
|
||||||
@ -42,7 +42,8 @@ import { getTxTrace } from '../tracing';
|
|||||||
tracer = fs.readFileSync(tracerFile).toString("utf-8");
|
tracer = fs.readFileSync(tracerFile).toString("utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getTxTrace(argv.providerUrl, argv.txHash, tracer, argv.timeout);
|
const tracingClient = new TracingClient(argv.providerUrl);
|
||||||
|
const result = await tracingClient.getTxTrace(argv.txHash, tracer, argv.timeout);
|
||||||
|
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
})();
|
})();
|
||||||
|
@ -1,11 +1,35 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
export const getTxTrace = async (providerUrl: string, txHash: string, tracer: string | undefined, timeout: string | undefined): Promise<any> => {
|
const callTracerWithAddresses = fs.readFileSync(path.join(__dirname, 'tracers', 'call_address_tracer.js')).toString("utf-8");
|
||||||
const provider = new ethers.providers.JsonRpcProvider(providerUrl);
|
|
||||||
return provider.send('debug_traceTransaction', [txHash, { tracer, timeout }]);
|
export class TracingClient {
|
||||||
|
|
||||||
|
_providerUrl: string;
|
||||||
|
_provider: ethers.providers.JsonRpcProvider;
|
||||||
|
|
||||||
|
constructor(providerUrl: string) {
|
||||||
|
assert(providerUrl);
|
||||||
|
|
||||||
|
this._providerUrl = providerUrl;
|
||||||
|
this._provider = new ethers.providers.JsonRpcProvider(providerUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTx (txHash: string): Promise<ethers.providers.TransactionResponse> {
|
||||||
|
return this._provider.getTransaction(txHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxTrace (txHash: string, tracer: string | undefined, timeout: string | undefined): Promise<any> {
|
||||||
|
if (tracer === 'callTraceWithAddresses') {
|
||||||
|
tracer = callTracerWithAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._provider.send('debug_traceTransaction', [txHash, { tracer, timeout }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCallTrace = async (providerUrl: string, block: string, txData: any, tracer: string | undefined): Promise<any> => {
|
async getCallTrace (block: string, txData: any, tracer: string | undefined): Promise<any> {
|
||||||
const provider = new ethers.providers.JsonRpcProvider(providerUrl);
|
return this._provider.send('debug_traceCall', [ txData, block, { tracer }]);
|
||||||
return provider.send('debug_traceCall', [ txData, block, { tracer }]);
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user