mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-07-27 10:42:06 +00:00
Factory PoolCreated event handler (#120)
* Setup handler for PoolCreated event. * Create Pool entity. * Subscribe to uni-watcher for watching events. * Refactor code to create GraphQLClient in ipld-eth-client. Co-authored-by: nikugogoi <95nikass@gmail.com> Co-authored-by: nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
parent
9c60895352
commit
aec9281fb8
1
packages/erc20-watcher/index.ts
Normal file
1
packages/erc20-watcher/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './src/client';
|
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "ERC20 Watcher",
|
"description": "ERC20 Watcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
"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",
|
"server:mock": "MOCK=1 nodemon src/server.ts -f environments/local.toml",
|
||||||
|
29
packages/erc20-watcher/src/client.ts
Normal file
29
packages/erc20-watcher/src/client.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { gql } from 'apollo-server-express';
|
||||||
|
import { GraphQLClient } from '@vulcanize/ipld-eth-client';
|
||||||
|
|
||||||
|
import { querySymbol } from './queries';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
gqlEndpoint: string;
|
||||||
|
gqlSubscriptionEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
_config: Config;
|
||||||
|
_client: GraphQLClient;
|
||||||
|
|
||||||
|
constructor (config: Config) {
|
||||||
|
this._config = config;
|
||||||
|
|
||||||
|
this._client = new GraphQLClient(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSymbol (blockHash: string | undefined, token: string): Promise<any> {
|
||||||
|
const { symbol } = await this._client.query(
|
||||||
|
gql(querySymbol),
|
||||||
|
{ blockHash, token }
|
||||||
|
);
|
||||||
|
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
export * from './src/eth-client';
|
export * from './src/eth-client';
|
||||||
export * from './src/utils';
|
export * from './src/utils';
|
||||||
|
export * from './src/graphql-client';
|
||||||
|
@ -1,18 +1,10 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import debug from 'debug';
|
|
||||||
import fetch from 'cross-fetch';
|
|
||||||
import { SubscriptionClient } from 'subscriptions-transport-ws';
|
|
||||||
import ws from 'ws';
|
|
||||||
|
|
||||||
import { ApolloClient, NormalizedCacheObject, split, HttpLink, InMemoryCache } from '@apollo/client/core';
|
|
||||||
import { getMainDefinition } from '@apollo/client/utilities';
|
|
||||||
import { WebSocketLink } from '@apollo/client/link/ws';
|
|
||||||
import { Cache } from '@vulcanize/cache';
|
import { Cache } from '@vulcanize/cache';
|
||||||
|
|
||||||
import ethQueries from './eth-queries';
|
import ethQueries from './eth-queries';
|
||||||
import { padKey } from './utils';
|
import { padKey } from './utils';
|
||||||
|
import { GraphQLClient } from './graphql-client';
|
||||||
const log = debug('vulcanize:eth-client');
|
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
@ -28,7 +20,7 @@ interface Vars {
|
|||||||
|
|
||||||
export class EthClient {
|
export class EthClient {
|
||||||
_config: Config;
|
_config: Config;
|
||||||
_client: ApolloClient<NormalizedCacheObject>;
|
_graphqlClient: GraphQLClient;
|
||||||
_cache: Cache | undefined;
|
_cache: Cache | undefined;
|
||||||
|
|
||||||
constructor (config: Config) {
|
constructor (config: Config) {
|
||||||
@ -39,45 +31,7 @@ export class EthClient {
|
|||||||
assert(gqlEndpoint, 'Missing gql endpoint');
|
assert(gqlEndpoint, 'Missing gql endpoint');
|
||||||
assert(gqlSubscriptionEndpoint, 'Missing gql subscription endpoint');
|
assert(gqlSubscriptionEndpoint, 'Missing gql subscription endpoint');
|
||||||
|
|
||||||
// https://www.apollographql.com/docs/react/data/subscriptions/
|
this._graphqlClient = new GraphQLClient({ gqlEndpoint, gqlSubscriptionEndpoint });
|
||||||
const subscriptionClient = new SubscriptionClient(gqlSubscriptionEndpoint, {
|
|
||||||
reconnect: true,
|
|
||||||
connectionCallback: (error: Error[]) => {
|
|
||||||
if (error) {
|
|
||||||
log('Subscription client connection error', error[0].message);
|
|
||||||
} else {
|
|
||||||
log('Subscription client connected successfully');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, ws);
|
|
||||||
|
|
||||||
subscriptionClient.onError(error => {
|
|
||||||
log('Subscription client error', error.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
const httpLink = new HttpLink({
|
|
||||||
uri: gqlEndpoint,
|
|
||||||
fetch
|
|
||||||
});
|
|
||||||
|
|
||||||
const wsLink = new WebSocketLink(subscriptionClient);
|
|
||||||
|
|
||||||
const splitLink = split(
|
|
||||||
({ query }) => {
|
|
||||||
const definition = getMainDefinition(query);
|
|
||||||
return (
|
|
||||||
definition.kind === 'OperationDefinition' &&
|
|
||||||
definition.operation === 'subscription'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
wsLink,
|
|
||||||
httpLink
|
|
||||||
);
|
|
||||||
|
|
||||||
this._client = new ApolloClient({
|
|
||||||
link: splitLink,
|
|
||||||
cache: new InMemoryCache()
|
|
||||||
});
|
|
||||||
|
|
||||||
this._cache = cache;
|
this._cache = cache;
|
||||||
}
|
}
|
||||||
@ -107,9 +61,7 @@ export class EthClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getBlockWithTransactions (blockNumber: string): Promise<any> {
|
async getBlockWithTransactions (blockNumber: string): Promise<any> {
|
||||||
const { data: result } = await this._client.query({ query: ethQueries.getBlockWithTransactions, variables: { blockNumber } });
|
return this._graphqlClient.query(ethQueries.getBlockWithTransactions, { blockNumber });
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogs (vars: Vars): Promise<any> {
|
async getLogs (vars: Vars): Promise<any> {
|
||||||
@ -120,27 +72,11 @@ export class EthClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async watchLogs (onNext: (value: any) => void): Promise<ZenObservable.Subscription> {
|
async watchLogs (onNext: (value: any) => void): Promise<ZenObservable.Subscription> {
|
||||||
const observable = await this._client.subscribe({
|
return this._graphqlClient.subscribe(ethQueries.subscribeLogs, onNext);
|
||||||
query: ethQueries.subscribeLogs
|
|
||||||
});
|
|
||||||
|
|
||||||
return observable.subscribe({
|
|
||||||
next (data) {
|
|
||||||
onNext(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async watchTransactions (onNext: (value: any) => void): Promise<ZenObservable.Subscription> {
|
async watchTransactions (onNext: (value: any) => void): Promise<ZenObservable.Subscription> {
|
||||||
const observable = await this._client.subscribe({
|
return this._graphqlClient.subscribe(ethQueries.subscribeTransactions, onNext);
|
||||||
query: ethQueries.subscribeTransactions
|
|
||||||
});
|
|
||||||
|
|
||||||
return observable.subscribe({
|
|
||||||
next (data) {
|
|
||||||
onNext(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getCachedOrFetch (queryName: keyof typeof ethQueries, vars: Vars): Promise<any> {
|
async _getCachedOrFetch (queryName: keyof typeof ethQueries, vars: Vars): Promise<any> {
|
||||||
@ -158,7 +94,7 @@ export class EthClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Result not cached or cache disabled, need to perform an upstream GQL query.
|
// Result not cached or cache disabled, need to perform an upstream GQL query.
|
||||||
const { data: result } = await this._client.query({ query: ethQueries[queryName], variables: vars });
|
const result = await this._graphqlClient.query(ethQueries[queryName], vars);
|
||||||
|
|
||||||
// Cache the result and return it, if cache is enabled.
|
// Cache the result and return it, if cache is enabled.
|
||||||
if (this._cache) {
|
if (this._cache) {
|
||||||
|
86
packages/ipld-eth-client/src/graphql-client.ts
Normal file
86
packages/ipld-eth-client/src/graphql-client.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import debug from 'debug';
|
||||||
|
import fetch from 'cross-fetch';
|
||||||
|
import { SubscriptionClient } from 'subscriptions-transport-ws';
|
||||||
|
import ws from 'ws';
|
||||||
|
|
||||||
|
import { ApolloClient, NormalizedCacheObject, split, HttpLink, InMemoryCache, DocumentNode, TypedDocumentNode } from '@apollo/client/core';
|
||||||
|
import { getMainDefinition } from '@apollo/client/utilities';
|
||||||
|
import { WebSocketLink } from '@apollo/client/link/ws';
|
||||||
|
|
||||||
|
const log = debug('vulcanize:client');
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
gqlEndpoint: string;
|
||||||
|
gqlSubscriptionEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphQLClient {
|
||||||
|
_config: Config;
|
||||||
|
_client: ApolloClient<NormalizedCacheObject>;
|
||||||
|
|
||||||
|
constructor (config: Config) {
|
||||||
|
this._config = config;
|
||||||
|
|
||||||
|
const { gqlEndpoint, gqlSubscriptionEndpoint } = config;
|
||||||
|
|
||||||
|
assert(gqlEndpoint, 'Missing gql endpoint');
|
||||||
|
assert(gqlSubscriptionEndpoint, 'Missing gql subscription endpoint');
|
||||||
|
|
||||||
|
// https://www.apollographql.com/docs/react/data/subscriptions/
|
||||||
|
const subscriptionClient = new SubscriptionClient(gqlSubscriptionEndpoint, {
|
||||||
|
reconnect: true,
|
||||||
|
connectionCallback: (error: Error[]) => {
|
||||||
|
if (error) {
|
||||||
|
log('Subscription client connection error', error[0].message);
|
||||||
|
} else {
|
||||||
|
log('Subscription client connected successfully');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, ws);
|
||||||
|
|
||||||
|
subscriptionClient.onError(error => {
|
||||||
|
log('Subscription client error', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpLink = new HttpLink({
|
||||||
|
uri: gqlEndpoint,
|
||||||
|
fetch
|
||||||
|
});
|
||||||
|
|
||||||
|
const wsLink = new WebSocketLink(subscriptionClient);
|
||||||
|
|
||||||
|
const splitLink = split(
|
||||||
|
({ query }) => {
|
||||||
|
const definition = getMainDefinition(query);
|
||||||
|
return (
|
||||||
|
definition.kind === 'OperationDefinition' &&
|
||||||
|
definition.operation === 'subscription'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
wsLink,
|
||||||
|
httpLink
|
||||||
|
);
|
||||||
|
|
||||||
|
this._client = new ApolloClient({
|
||||||
|
link: splitLink,
|
||||||
|
cache: new InMemoryCache()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribe (query: DocumentNode, onNext: (value: any) => void): Promise<ZenObservable.Subscription> {
|
||||||
|
const observable = await this._client.subscribe({ query });
|
||||||
|
|
||||||
|
return observable.subscribe({
|
||||||
|
next (data) {
|
||||||
|
onNext(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async query (query: DocumentNode | TypedDocumentNode, variables: { [key: string]: any }): Promise<any> {
|
||||||
|
const { data: result } = await this._client.query({ query, variables });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
[server]
|
[server]
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 3003
|
port = 3004
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
type = "postgres"
|
type = "postgres"
|
||||||
@ -22,10 +22,18 @@
|
|||||||
subscribersDir = "src/subscriber"
|
subscribersDir = "src/subscriber"
|
||||||
|
|
||||||
[upstream]
|
[upstream]
|
||||||
gqlEndpoint = "http://127.0.0.1:8083/graphql"
|
gqlEndpoint = "http://127.0.0.1:8082/graphql"
|
||||||
gqlSubscriptionEndpoint = "http://127.0.0.1:5000/graphql"
|
gqlSubscriptionEndpoint = "http://127.0.0.1:5000/graphql"
|
||||||
|
|
||||||
[upstream.cache]
|
[upstream.cache]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
enabled = false
|
enabled = false
|
||||||
deleteOnStart = false
|
deleteOnStart = false
|
||||||
|
|
||||||
|
[upstream.uniWatcher]
|
||||||
|
gqlEndpoint = "http://127.0.0.1:3003/graphql"
|
||||||
|
gqlSubscriptionEndpoint = "http://127.0.0.1:3003/graphql"
|
||||||
|
|
||||||
|
[upstream.tokenWatcher]
|
||||||
|
gqlEndpoint = "http://127.0.0.1:3001/graphql"
|
||||||
|
gqlSubscriptionEndpoint = "http://127.0.0.1:3001/graphql"
|
||||||
|
@ -6,9 +6,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vulcanize/cache": "^0.1.0",
|
"@vulcanize/cache": "^0.1.0",
|
||||||
|
"@vulcanize/erc20-watcher": "^0.1.0",
|
||||||
"@vulcanize/ipld-eth-client": "^0.1.0",
|
"@vulcanize/ipld-eth-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",
|
||||||
|
"typeorm": "^0.2.32"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "DEBUG=vulcanize:* nodemon src/server.ts -f environments/local.toml",
|
"server": "DEBUG=vulcanize:* nodemon src/server.ts -f environments/local.toml",
|
||||||
|
@ -18,7 +18,15 @@ export interface Config {
|
|||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
gqlSubscriptionEndpoint: string;
|
gqlSubscriptionEndpoint: string;
|
||||||
traceProviderEndpoint: string;
|
traceProviderEndpoint: string;
|
||||||
cache: CacheConfig
|
cache: CacheConfig;
|
||||||
|
uniWatcher: {
|
||||||
|
gqlEndpoint: string;
|
||||||
|
gqlSubscriptionEndpoint: string;
|
||||||
|
};
|
||||||
|
tokenWatcher: {
|
||||||
|
gqlEndpoint: string;
|
||||||
|
gqlSubscriptionEndpoint: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
jobQueue: {
|
jobQueue: {
|
||||||
dbConnectionString: string;
|
dbConnectionString: string;
|
||||||
|
@ -2,7 +2,10 @@ import assert from 'assert';
|
|||||||
import { Connection, ConnectionOptions, createConnection, DeepPartial } from 'typeorm';
|
import { Connection, ConnectionOptions, createConnection, DeepPartial } from 'typeorm';
|
||||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||||
|
|
||||||
|
import { Factory } from './entity/Factory';
|
||||||
|
import { Pool } from './entity/Pool';
|
||||||
import { Event } from './entity/Event';
|
import { Event } from './entity/Event';
|
||||||
|
import { Token } from './entity/Token';
|
||||||
import { EventSyncProgress } from './entity/EventProgress';
|
import { EventSyncProgress } from './entity/EventProgress';
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
@ -27,6 +30,67 @@ export class Database {
|
|||||||
return this._conn.close();
|
return this._conn.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadFactory ({ id, blockNumber }: DeepPartial<Factory>): Promise<Factory> {
|
||||||
|
return this._conn.transaction(async (tx) => {
|
||||||
|
const repo = tx.getRepository(Factory);
|
||||||
|
|
||||||
|
let entity = await repo.createQueryBuilder('factory')
|
||||||
|
.where('id = :id AND block_number <= :blockNumber', {
|
||||||
|
id,
|
||||||
|
blockNumber
|
||||||
|
})
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
entity = repo.create({ blockNumber, id });
|
||||||
|
entity = await repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPool ({ id, blockNumber }: DeepPartial<Pool>): Promise<Pool> {
|
||||||
|
return this._conn.transaction(async (tx) => {
|
||||||
|
const repo = tx.getRepository(Pool);
|
||||||
|
|
||||||
|
let entity = await repo.createQueryBuilder('pool')
|
||||||
|
.where('id = :id AND block_number <= :blockNumber', {
|
||||||
|
id,
|
||||||
|
blockNumber
|
||||||
|
})
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
entity = repo.create({ blockNumber, id });
|
||||||
|
entity = await repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadToken ({ id, blockNumber }: DeepPartial<Token>, getValues: () => Promise<DeepPartial<Token>>): Promise<Token> {
|
||||||
|
return this._conn.transaction(async (tx) => {
|
||||||
|
const repo = tx.getRepository(Token);
|
||||||
|
|
||||||
|
let entity = await repo.createQueryBuilder('token')
|
||||||
|
.where('id = :id AND block_number <= :blockNumber', {
|
||||||
|
id,
|
||||||
|
blockNumber
|
||||||
|
})
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
const tokenValues = await getValues();
|
||||||
|
entity = repo.create({ blockNumber, id, ...tokenValues });
|
||||||
|
entity = await repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if events have already been synced for the (block, token) combination.
|
// Returns true if events have already been synced for the (block, token) combination.
|
||||||
async didSyncEvents ({ blockHash, token }: { blockHash: string, token: string }): Promise<boolean> {
|
async didSyncEvents ({ blockHash, token }: { blockHash: string, token: string }): Promise<boolean> {
|
||||||
const numRows = await this._conn.getRepository(EventSyncProgress)
|
const numRows = await this._conn.getRepository(EventSyncProgress)
|
||||||
|
14
packages/uni-info-watcher/src/entity/Factory.ts
Normal file
14
packages/uni-info-watcher/src/entity/Factory.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['blockNumber', 'id'], { unique: true })
|
||||||
|
export class Factory {
|
||||||
|
@PrimaryColumn('varchar', { length: 42 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column('numeric')
|
||||||
|
blockNumber!: number;
|
||||||
|
|
||||||
|
@Column('numeric', { default: 0 })
|
||||||
|
poolCount!: number;
|
||||||
|
}
|
11
packages/uni-info-watcher/src/entity/Pool.ts
Normal file
11
packages/uni-info-watcher/src/entity/Pool.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['blockNumber', 'id'])
|
||||||
|
export class Pool {
|
||||||
|
@PrimaryColumn('varchar', { length: 42 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column('numeric')
|
||||||
|
blockNumber!: number;
|
||||||
|
}
|
14
packages/uni-info-watcher/src/entity/Token.ts
Normal file
14
packages/uni-info-watcher/src/entity/Token.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['blockNumber', 'id'])
|
||||||
|
export class Token {
|
||||||
|
@PrimaryColumn('varchar', { length: 42 })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column('numeric')
|
||||||
|
blockNumber!: number;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
symbol!: string;
|
||||||
|
}
|
@ -1,59 +1,94 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import _ from 'lodash';
|
import { Client as UniClient } from '@vulcanize/uni-watcher';
|
||||||
|
import { Client as ERC20Client } from '@vulcanize/erc20-watcher';
|
||||||
|
|
||||||
import { EthClient } from '@vulcanize/ipld-eth-client';
|
import { Database } from './database';
|
||||||
|
|
||||||
import { Indexer } from './indexer';
|
|
||||||
|
|
||||||
const log = debug('vulcanize:events');
|
const log = debug('vulcanize:events');
|
||||||
|
|
||||||
|
interface PoolCreatedEvent {
|
||||||
|
token0: string;
|
||||||
|
token1: string;
|
||||||
|
fee: bigint;
|
||||||
|
tickSpacing: bigint;
|
||||||
|
pool: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultEvent {
|
||||||
|
proof: {
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
event: {
|
||||||
|
__typename: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class EventWatcher {
|
export class EventWatcher {
|
||||||
_ethClient: EthClient
|
_db: Database
|
||||||
_indexer: Indexer
|
_subscription?: ZenObservable.Subscription
|
||||||
_subscription: ZenObservable.Subscription | undefined
|
_uniClient: UniClient
|
||||||
|
_erc20Client: ERC20Client
|
||||||
|
|
||||||
constructor (ethClient: EthClient, indexer: Indexer) {
|
constructor (db: Database, uniClient: UniClient, erc20Client: ERC20Client) {
|
||||||
assert(ethClient);
|
assert(db);
|
||||||
assert(indexer);
|
|
||||||
|
|
||||||
this._ethClient = ethClient;
|
this._db = db;
|
||||||
this._indexer = indexer;
|
this._uniClient = uniClient;
|
||||||
|
this._erc20Client = erc20Client;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start (): Promise<void> {
|
async start (): Promise<void> {
|
||||||
assert(!this._subscription, 'subscription already started');
|
assert(!this._subscription, 'subscription already started');
|
||||||
|
log('Started watching upstream events...');
|
||||||
log('Started watching upstream logs...');
|
this._subscription = await this._uniClient.watchEvents(this._handleEvents.bind(this));
|
||||||
|
|
||||||
this._subscription = await this._ethClient.watchLogs(async (value) => {
|
|
||||||
const receipt = _.get(value, 'data.listen.relatedNode');
|
|
||||||
log('watchLogs', JSON.stringify(receipt, null, 2));
|
|
||||||
|
|
||||||
// Check if this log is for a contract we care about.
|
|
||||||
const { logContracts } = receipt;
|
|
||||||
if (logContracts && logContracts.length) {
|
|
||||||
for (let logIndex = 0; logIndex < logContracts.length; logIndex++) {
|
|
||||||
const contractAddress = logContracts[logIndex];
|
|
||||||
const isWatchedContract = await this._indexer.isUniswapContract(contractAddress);
|
|
||||||
if (isWatchedContract) {
|
|
||||||
// TODO: Move processing to background task runner.
|
|
||||||
|
|
||||||
const { ethTransactionCidByTxId: { ethHeaderCidByHeaderId: { blockHash } } } = receipt;
|
|
||||||
await this._indexer.getEvents(blockHash, contractAddress, null);
|
|
||||||
|
|
||||||
// Trigger other indexer methods based on event topic.
|
|
||||||
await this._indexer.processEvent(blockHash, contractAddress, receipt, logIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop (): Promise<void> {
|
async stop (): Promise<void> {
|
||||||
if (this._subscription) {
|
if (this._subscription) {
|
||||||
log('Stopped watching upstream logs');
|
log('Stopped watching upstream events');
|
||||||
this._subscription.unsubscribe();
|
this._subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _handleEvents ({ blockHash, blockNumber, contract, event }: { blockHash: string, blockNumber: number, contract: string, event: ResultEvent}): Promise<void> {
|
||||||
|
// TODO: Process proof (proof.data) in event.
|
||||||
|
const { event: { __typename: eventType, ...eventValues } } = event;
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case 'PoolCreatedEvent':
|
||||||
|
this._handlePoolCreated(blockHash, blockNumber, contract, eventValues as PoolCreatedEvent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handlePoolCreated (blockHash: string, blockNumber: number, contractAddress: string, poolCreatedEvent: PoolCreatedEvent): Promise<void> {
|
||||||
|
const { token0: token0Address, token1: token1Address, fee, tickSpacing, pool: poolAddress } = poolCreatedEvent;
|
||||||
|
|
||||||
|
// Load factory.
|
||||||
|
const factory = await this._db.loadFactory({ blockNumber, id: contractAddress });
|
||||||
|
factory.poolCount = factory.poolCount + 1;
|
||||||
|
|
||||||
|
// Create new Pool entity.
|
||||||
|
const pool = this._db.loadPool({ blockNumber, id: poolAddress });
|
||||||
|
|
||||||
|
// TODO: Load Token entities.
|
||||||
|
const getTokenValues = async (tokenAddress: string) => {
|
||||||
|
const { value: symbol } = await this._erc20Client.getSymbol(blockHash, tokenAddress);
|
||||||
|
return { symbol };
|
||||||
|
};
|
||||||
|
|
||||||
|
const token0 = this._db.loadToken({ blockNumber, id: token0Address }, () => getTokenValues(token0Address));
|
||||||
|
const token1 = this._db.loadToken({ blockNumber, id: token1Address }, () => getTokenValues(token1Address));
|
||||||
|
|
||||||
|
// TODO: Update Token entities.
|
||||||
|
|
||||||
|
// TODO: Update Pool entity.
|
||||||
|
|
||||||
|
// TODO: Save entities to DB.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,11 @@ export interface ValueResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlockHeight {
|
||||||
|
number: number;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
type EventsResult = Array<{
|
type EventsResult = Array<{
|
||||||
event: {
|
event: {
|
||||||
from?: string;
|
from?: string;
|
||||||
|
@ -10,6 +10,8 @@ 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 { Client as ERC20Client } from '@vulcanize/erc20-watcher';
|
||||||
|
import { Client as UniClient } from '@vulcanize/uni-watcher';
|
||||||
|
|
||||||
import typeDefs from './schema';
|
import typeDefs from './schema';
|
||||||
|
|
||||||
@ -46,20 +48,22 @@ 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, cache: cacheConfig, uniWatcher, tokenWatcher } = upstream;
|
||||||
assert(gqlEndpoint, 'Missing upstream gqlEndpoint');
|
assert(gqlEndpoint, 'Missing upstream gqlEndpoint');
|
||||||
assert(gqlSubscriptionEndpoint, 'Missing upstream gqlSubscriptionEndpoint');
|
assert(gqlSubscriptionEndpoint, 'Missing upstream gqlSubscriptionEndpoint');
|
||||||
|
|
||||||
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 uniClient = new UniClient(uniWatcher);
|
||||||
|
|
||||||
// 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 erc20Client = new ERC20Client(tokenWatcher);
|
||||||
const indexer = new Indexer(db, ethClient, pubsub);
|
const indexer = new Indexer(db, ethClient, pubsub);
|
||||||
|
|
||||||
const eventWatcher = new EventWatcher(ethClient, indexer);
|
const eventWatcher = new EventWatcher(db, uniClient, erc20Client);
|
||||||
await eventWatcher.start();
|
await eventWatcher.start();
|
||||||
|
|
||||||
const resolvers = process.env.MOCK ? await createMockResolvers() : await createResolvers(indexer);
|
const resolvers = process.env.MOCK ? await createMockResolvers() : await createResolvers(indexer);
|
||||||
|
1
packages/uni-watcher/index.ts
Normal file
1
packages/uni-watcher/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './src/client';
|
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Uniswap v3 Watcher",
|
"description": "Uniswap v3 Watcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
"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",
|
"server:mock": "MOCK=1 nodemon src/server.ts -f environments/local.toml",
|
||||||
|
50
packages/uni-watcher/src/client.ts
Normal file
50
packages/uni-watcher/src/client.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { gql } from 'apollo-server-express';
|
||||||
|
import { GraphQLClient } from '@vulcanize/ipld-eth-client';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
gqlEndpoint: string;
|
||||||
|
gqlSubscriptionEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
_config: Config;
|
||||||
|
_client: GraphQLClient;
|
||||||
|
|
||||||
|
constructor (config: Config) {
|
||||||
|
this._config = config;
|
||||||
|
|
||||||
|
this._client = new GraphQLClient(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async watchEvents (onNext: (value: any) => void): Promise<ZenObservable.Subscription> {
|
||||||
|
return this._client.subscribe(
|
||||||
|
gql`
|
||||||
|
subscription SubscriptionReceipt {
|
||||||
|
onEvent {
|
||||||
|
blockHash
|
||||||
|
blockNumber
|
||||||
|
contract
|
||||||
|
event {
|
||||||
|
proof {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
event {
|
||||||
|
__typename
|
||||||
|
... on PoolCreatedEvent {
|
||||||
|
token0
|
||||||
|
token1
|
||||||
|
fee
|
||||||
|
tickSpacing
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
({ data }) => {
|
||||||
|
onNext(data.onEvent);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
|
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
|
||||||
import { task } from "hardhat/config";
|
import { task } from "hardhat/config";
|
||||||
|
import '@nomiclabs/hardhat-ethers';
|
||||||
|
|
||||||
// This is a sample Hardhat task. To learn how to create your own go to
|
// This is a sample Hardhat task. To learn how to create your own go to
|
||||||
// https://hardhat.org/guides/create-task.html
|
// https://hardhat.org/guides/create-task.html
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
abi as FACTORY_ABI,
|
abi as FACTORY_ABI,
|
||||||
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json'
|
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json'
|
||||||
import { ContractTransaction } from "ethers";
|
import { ContractTransaction } from "ethers";
|
||||||
|
import '@nomiclabs/hardhat-ethers';
|
||||||
|
|
||||||
task("create-pool", "Creates pool using Factory contract")
|
task("create-pool", "Creates pool using Factory contract")
|
||||||
.addParam('factory', 'Address of factory contract', undefined, types.string)
|
.addParam('factory', 'Address of factory contract', undefined, types.string)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { task, types } from "hardhat/config";
|
import { task } from "hardhat/config";
|
||||||
import {
|
import {
|
||||||
abi as FACTORY_ABI,
|
abi as FACTORY_ABI,
|
||||||
bytecode as FACTORY_BYTECODE,
|
bytecode as FACTORY_BYTECODE,
|
||||||
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json'
|
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json';
|
||||||
|
import '@nomiclabs/hardhat-ethers';
|
||||||
|
|
||||||
task("deploy-factory", "Deploys Factory contract")
|
task("deploy-factory", "Deploys Factory contract")
|
||||||
.setAction(async (_, hre) => {
|
.setAction(async (_, hre) => {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { task, types } from "hardhat/config";
|
import { task, types } from "hardhat/config";
|
||||||
|
import '@nomiclabs/hardhat-ethers';
|
||||||
|
|
||||||
task("deploy-token", "Deploys new token")
|
task("deploy-token", "Deploys new token")
|
||||||
.addParam('name', 'Name of the token', undefined, types.string)
|
.addParam('name', 'Name of the token', undefined, types.string)
|
||||||
.addParam('symbol', 'Symbol of the token', undefined, types.string)
|
.addParam('symbol', 'Symbol of the token', undefined, types.string)
|
||||||
.setAction(async (args, hre) => {
|
.setAction(async (args, hre) => {
|
||||||
const { name, symbol } = args
|
const { name, symbol } = args
|
||||||
|
await hre.run("compile");
|
||||||
const Token = await hre.ethers.getContractFactory('ERC20Token');
|
const Token = await hre.ethers.getContractFactory('ERC20Token');
|
||||||
const token = await Token.deploy(name, symbol);
|
const token = await Token.deploy(name, symbol);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user