diff --git a/src/program.ts b/src/program.ts index e3a78fb..0c7812c 100644 --- a/src/program.ts +++ b/src/program.ts @@ -13,6 +13,7 @@ import { RelayerRegistry__factory, Aggregator__factory, Governance__factory, + Echoer__factory, } from '@tornado/contracts'; import { JsonRpcProvider, @@ -62,6 +63,7 @@ import { TornadoFeeOracle, TokenPriceOracle, calculateSnarkProof, + NodeEchoService, NodeEncryptedNotesService, NodeGovernanceService, RelayerClient, @@ -1257,10 +1259,11 @@ export function tornadoProgram() { registrySubgraph, tokens, routerContract, + echoContract, registryContract, ['governance.contract.tornadocash.eth']: governanceContract, deployedBlock, - constants: { GOVERNANCE_BLOCK, REGISTRY_BLOCK, ENCRYPTED_NOTES_BLOCK }, + constants: { GOVERNANCE_BLOCK, REGISTRY_BLOCK, NOTE_ACCOUNT_BLOCK, ENCRYPTED_NOTES_BLOCK }, } = config; const provider = getProgramProvider(netId, rpc, config, { @@ -1301,6 +1304,20 @@ export function tornadoProgram() { await registryService.updateEvents(); } + const echoService = new NodeEchoService({ + netId, + provider, + graphApi, + subgraphName: tornadoSubgraph, + Echoer: Echoer__factory.connect(echoContract, provider), + deployedBlock: NOTE_ACCOUNT_BLOCK, + fetchDataOptions, + cacheDirectory: EVENTS_DIR, + userDirectory: SAVED_DIR, + }); + + await echoService.updateEvents(); + const encryptedNotesService = new NodeEncryptedNotesService({ netId, provider, diff --git a/src/services/events/base.ts b/src/services/events/base.ts index 21183fd..9916554 100644 --- a/src/services/events/base.ts +++ b/src/services/events/base.ts @@ -1,5 +1,12 @@ import { BaseContract, Provider, EventLog, TransactionResponse, getAddress, Block, ContractEventName } from 'ethers'; -import type { Tornado, TornadoRouter, TornadoProxyLight, Governance, RelayerRegistry } from '@tornado/contracts'; +import type { + Tornado, + TornadoRouter, + TornadoProxyLight, + Governance, + RelayerRegistry, + Echoer, +} from '@tornado/contracts'; import * as graph from '../graphql'; import { BatchEventsService, @@ -21,6 +28,7 @@ import type { GovernanceUndelegatedEvents, RegistersEvents, BaseGraphEvents, + EchoEvents, } from './types'; export const DEPOSIT = 'deposit'; @@ -454,6 +462,76 @@ export class BaseDepositsService extends BaseEventsService { + constructor({ + netId, + provider, + graphApi, + subgraphName, + Echoer, + deployedBlock, + fetchDataOptions, + }: BaseEchoServiceConstructor) { + super({ netId, provider, graphApi, subgraphName, contract: Echoer, deployedBlock, fetchDataOptions }); + } + + getInstanceName(): string { + return `echo_${this.netId}`; + } + + getType(): string { + return 'Echo'; + } + + getGraphMethod(): string { + return 'getAllGraphEchoEvents'; + } + + async formatEvents(events: EventLog[]) { + return events + .map(({ blockNumber, index: logIndex, transactionHash, args }) => { + const { who, data } = args; + + if (who && data) { + const eventObjects = { + blockNumber, + logIndex, + transactionHash, + }; + + return { + ...eventObjects, + address: who, + encryptedAccount: data, + }; + } + }) + .filter((e) => e) as EchoEvents[]; + } + + async getEventsFromGraph({ fromBlock }: { fromBlock: number }): Promise> { + // TheGraph doesn't support our batch sync due to missing blockNumber field + if (!this.graphApi || this.graphApi.includes('api.thegraph.com')) { + return { + events: [], + lastBlock: fromBlock, + }; + } + + return super.getEventsFromGraph({ fromBlock }); + } +} + export type BaseEncryptedNotesServiceConstructor = { netId: number | string; provider: Provider; @@ -556,7 +634,7 @@ export class BaseGovernanceService extends BaseEventsService { diff --git a/src/services/events/node.ts b/src/services/events/node.ts index 9efc4a2..19bccc3 100644 --- a/src/services/events/node.ts +++ b/src/services/events/node.ts @@ -12,8 +12,17 @@ import { BaseGovernanceServiceConstructor, BaseRegistryServiceConstructor, BaseGovernanceEventTypes, + BaseEchoServiceConstructor, + BaseEchoService, } from './base'; -import type { BaseEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, RegistersEvents } from './types'; +import type { + BaseEvents, + DepositsEvents, + WithdrawalsEvents, + EncryptedNotesEvents, + RegistersEvents, + EchoEvents, +} from './types'; export type NodeDepositsServiceConstructor = BaseDepositsServiceConstructor & { cacheDirectory?: string; @@ -184,6 +193,151 @@ export class NodeDepositsService extends BaseDepositsService { } } +export type NodeEchoServiceConstructor = BaseEchoServiceConstructor & { + cacheDirectory?: string; + userDirectory?: string; +}; + +export class NodeEchoService extends BaseEchoService { + cacheDirectory?: string; + userDirectory?: string; + + constructor({ + netId, + provider, + graphApi, + subgraphName, + Echoer, + deployedBlock, + fetchDataOptions, + cacheDirectory, + userDirectory, + }: NodeEchoServiceConstructor) { + super({ + netId, + provider, + graphApi, + subgraphName, + Echoer, + deployedBlock, + fetchDataOptions, + }); + + this.cacheDirectory = cacheDirectory; + this.userDirectory = userDirectory; + } + + updateEventProgress({ type, fromBlock, toBlock, count }: Parameters[0]) { + if (toBlock) { + console.log(`fromBlock - ${fromBlock}`); + console.log(`toBlock - ${toBlock}`); + + if (count) { + console.log(`downloaded ${type} events count - ${count}`); + console.log('____________________________________________'); + console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`); + } + } + } + + updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters[0]) { + if (toBlock) { + console.log(`fromBlock - ${fromBlock}`); + console.log(`toBlock - ${toBlock}`); + + if (count) { + console.log(`downloaded ${type} events from graph node count - ${count}`); + console.log('____________________________________________'); + console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`); + } + } + } + + async getEventsFromDB() { + if (!this.userDirectory) { + console.log(`Updating events for ${this.netId} chain echo events\n`); + console.log(`savedEvents count - ${0}`); + console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`); + + return { + events: [], + lastBlock: this.deployedBlock, + }; + } + + const savedEvents = await loadSavedEvents({ + name: this.getInstanceName(), + userDirectory: this.userDirectory, + deployedBlock: this.deployedBlock, + }); + + console.log(`Updating events for ${this.netId} chain echo events\n`); + console.log(`savedEvents count - ${savedEvents.events.length}`); + console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`); + + return savedEvents; + } + + async getEventsFromCache() { + if (!this.cacheDirectory) { + console.log(`cachedEvents count - ${0}`); + console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`); + + return { + events: [], + lastBlock: this.deployedBlock, + }; + } + + const cachedEvents = await loadCachedEvents({ + name: this.getInstanceName(), + cacheDirectory: this.cacheDirectory, + deployedBlock: this.deployedBlock, + }); + + console.log(`cachedEvents count - ${cachedEvents.events.length}`); + console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`); + + return cachedEvents; + } + + async saveEvents({ events, lastBlock }: BaseEvents) { + const instanceName = this.getInstanceName(); + + console.log('\ntotalEvents count - ', events.length); + console.log( + `totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`, + ); + + const eventTable = new Table(); + + eventTable.push( + [{ colSpan: 2, content: 'Echo Accounts', hAlign: 'center' }], + ['Network', `${this.netId} chain`], + ['Events', `${events.length} events`], + [{ colSpan: 2, content: 'Latest events' }], + ...events + .slice(events.length - 10) + .reverse() + .map(({ blockNumber }, index) => { + const eventIndex = events.length - index; + + return [eventIndex, blockNumber]; + }), + ); + + console.log(eventTable.toString() + '\n'); + + if (this.userDirectory) { + await saveEvents({ + name: instanceName, + userDirectory: this.userDirectory, + events, + }); + } + } +} + export type NodeEncryptedNotesServiceConstructor = BaseEncryptedNotesServiceConstructor & { cacheDirectory?: string; userDirectory?: string; diff --git a/src/services/events/types.ts b/src/services/events/types.ts index 00f8ac5..ecdc95c 100644 --- a/src/services/events/types.ts +++ b/src/services/events/types.ts @@ -64,6 +64,11 @@ export type WithdrawalsEvents = MinimalEvents & { timestamp: number; }; +export type EchoEvents = MinimalEvents & { + address: string; + encryptedAccount: string; +}; + export type EncryptedNotesEvents = MinimalEvents & { encryptedNote: string; }; diff --git a/src/services/graphql/index.ts b/src/services/graphql/index.ts index 2fb69f9..5a9c566 100644 --- a/src/services/graphql/index.ts +++ b/src/services/graphql/index.ts @@ -8,6 +8,7 @@ import type { WithdrawalsEvents, EncryptedNotesEvents, BatchGraphOnProgress, + EchoEvents, } from '../events'; import { _META, @@ -17,6 +18,7 @@ import { GET_WITHDRAWALS, GET_NOTE_ACCOUNTS, GET_ENCRYPTED_NOTES, + GET_ECHO_EVENTS, } from './queries'; export * from './queries'; @@ -662,6 +664,132 @@ export async function getNoteAccounts({ } } +export interface GraphEchoEvents { + noteAccounts: { + id: string; + blockNumber: string; + address: string; + encryptedAccount: string; + }[]; + _meta: { + block: { + number: number; + }; + hasIndexingErrors: boolean; + }; +} + +export interface getGraphEchoEventsParams { + graphApi: string; + subgraphName: string; + fromBlock: number; + fetchDataOptions?: fetchDataOptions; + onProgress?: BatchGraphOnProgress; +} + +export function getGraphEchoEvents({ + graphApi, + subgraphName, + fromBlock, + fetchDataOptions, +}: getGraphEchoEventsParams): Promise { + return queryGraph({ + graphApi, + subgraphName, + query: GET_ECHO_EVENTS, + variables: { + first, + fromBlock, + }, + fetchDataOptions, + }); +} + +export async function getAllGraphEchoEvents({ + graphApi, + subgraphName, + fromBlock, + fetchDataOptions, + onProgress, +}: getGraphEchoEventsParams): Promise> { + try { + const events = []; + let lastSyncBlock = fromBlock; + + // eslint-disable-next-line no-constant-condition + while (true) { + let { + noteAccounts: result, + _meta: { + // eslint-disable-next-line prefer-const + block: { number: currentBlock }, + }, + } = await getGraphEchoEvents({ graphApi, subgraphName, fromBlock, fetchDataOptions }); + + lastSyncBlock = currentBlock; + + if (isEmptyArray(result)) { + break; + } + + const [firstEvent] = result; + const [lastEvent] = result.slice(-1); + + if (typeof onProgress === 'function') { + onProgress({ + type: 'EchoEvents', + fromBlock: Number(firstEvent.blockNumber), + toBlock: Number(lastEvent.blockNumber), + count: result.length, + }); + } + + if (result.length < 900) { + events.push(...result); + break; + } + + result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber); + fromBlock = Number(lastEvent.blockNumber); + + events.push(...result); + } + + if (!events.length) { + return { + events: [], + lastSyncBlock, + }; + } + + const result = events.map((e) => { + const [transactionHash, logIndex] = e.id.split('-'); + + return { + blockNumber: Number(e.blockNumber), + logIndex: Number(logIndex), + transactionHash: transactionHash, + address: e.address, + encryptedAccount: e.encryptedAccount, + }; + }); + + const [lastEvent] = result.slice(-1); + + return { + events: result, + lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock, + }; + } catch (err) { + console.log('Error from getAllGraphEchoEvents query'); + console.log(err); + return { + events: [], + lastSyncBlock: fromBlock, + }; + } +} + export interface GraphEncryptedNotes { encryptedNotes: { blockNumber: string; diff --git a/src/services/graphql/queries.ts b/src/services/graphql/queries.ts index fcb3a97..4c9179b 100644 --- a/src/services/graphql/queries.ts +++ b/src/services/graphql/queries.ts @@ -107,6 +107,23 @@ export const GET_NOTE_ACCOUNTS = ` } `; +export const GET_ECHO_EVENTS = ` + query getNoteAccounts($first: Int, $fromBlock: Int) { + noteAccounts(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) { + id + blockNumber + address + encryptedAccount + } + _meta { + block { + number + } + hasIndexingErrors + } + } +`; + export const GET_ENCRYPTED_NOTES = ` query getEncryptedNotes($first: Int, $fromBlock: Int) { encryptedNotes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {