Basic GQL API to get traces, save to db. (#76)

This commit is contained in:
Ashwin Phatak 2021-06-18 17:04:02 +05:30 committed by GitHub
parent 8507bc8b9c
commit 6a33c704b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 147 additions and 26 deletions

View File

@ -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"

View File

@ -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",

View File

@ -17,6 +17,7 @@ export interface Config {
upstream: { upstream: {
gqlEndpoint: string; gqlEndpoint: string;
gqlSubscriptionEndpoint: string; gqlSubscriptionEndpoint: string;
traceProviderEndpoint: string;
cache: CacheConfig cache: CacheConfig
} }
} }

View File

@ -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);
}
} }

View 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;
}

View File

@ -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
};
}
} }

View File

@ -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);
}
}
}; };
}; };

View File

@ -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!
} }
# #

View File

@ -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);

View File

@ -0,0 +1 @@
export * from './src/tracing';

View File

@ -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));
})(); })();

View File

@ -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));
})(); })();

View File

@ -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 }]);
}; };
}