Compare commits

...

2 Commits

Author SHA1 Message Date
a293e541d9
Change validateEvents params 2024-11-17 23:23:00 +00:00
8644cd3c82
Add multiQueryFilter function 2024-11-17 22:37:15 +00:00
8 changed files with 842 additions and 330 deletions

17
dist/batch.d.ts vendored
View File

@ -1,4 +1,14 @@
import type { Provider, BlockTag, Block, TransactionResponse, BaseContract, ContractEventName, EventLog, TransactionReceipt } from 'ethers';
import { Provider, BlockTag, Block, TransactionResponse, BaseContract, ContractEventName, EventLog, TransactionReceipt, EventFragment, TopicFilter, Interface, Log } from 'ethers';
/**
* Copied from ethers.js as they don't export this function
* https://github.com/ethers-io/ethers.js/blob/main/src.ts/contract/contract.ts#L464
*/
export declare function getSubInfo(abiInterface: Interface, event: ContractEventName): Promise<{
fragment: null | EventFragment;
tag: string;
topics: TopicFilter;
}>;
export declare function multiQueryFilter(address: string | string[], contract: BaseContract, event: ContractEventName, fromBlock?: BlockTag, toBlock?: BlockTag): Promise<Log[]>;
export interface BatchBlockServiceConstructor {
provider: Provider;
onProgress?: BatchBlockOnProgress;
@ -65,6 +75,7 @@ export type BatchEventOnProgress = ({ percentage, type, fromBlock, toBlock, coun
count?: number;
}) => void;
export interface EventInput {
address?: string | string[];
fromBlock: number;
toBlock: number;
type: ContractEventName;
@ -82,7 +93,7 @@ export declare class BatchEventsService {
retryMax: number;
retryOn: number;
constructor({ provider, contract, onProgress, concurrencySize, blocksPerRequest, shouldRetry, retryMax, retryOn, }: BatchEventServiceConstructor);
getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]>;
getPastEvents({ address, fromBlock, toBlock, type }: EventInput): Promise<EventLog[]>;
createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[];
getBatchEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]>;
getBatchEvents({ address, fromBlock, toBlock, type }: EventInput): Promise<EventLog[]>;
}

View File

@ -50,8 +50,8 @@ export declare class BaseEventsService<EventType extends MinimalEvents> {
getLatestEvents({ fromBlock }: {
fromBlock: number;
}): Promise<BaseEvents<EventType>>;
validateEvents<S>({ events, lastBlock, hasNewEvents, }: BaseEvents<EventType> & {
hasNewEvents?: boolean;
validateEvents<S>({ events, newEvents, lastBlock, }: BaseEvents<EventType> & {
newEvents: EventType[];
}): Promise<S>;
/**
* Handle saving events
@ -83,8 +83,8 @@ export declare class BaseTornadoService extends BaseEventsService<DepositsEvents
constructor(serviceConstructor: BaseTornadoServiceConstructor);
getInstanceName(): string;
formatEvents(events: EventLog[]): Promise<(DepositsEvents | WithdrawalsEvents)[]>;
validateEvents<S>({ events, hasNewEvents, }: BaseEvents<DepositsEvents | WithdrawalsEvents> & {
hasNewEvents?: boolean;
validateEvents<S>({ events, newEvents, }: BaseEvents<DepositsEvents | WithdrawalsEvents> & {
newEvents: (DepositsEvents | WithdrawalsEvents)[];
}): Promise<S>;
getLatestEvents({ fromBlock, }: {
fromBlock: number;

130
dist/index.js vendored
View File

@ -154,6 +154,107 @@ function fromContentHash(contentHash) {
return contentHashUtils__namespace.decode(contentHash);
}
function isDeferred(value) {
return value && typeof value === "object" && "getTopicFilter" in value && typeof value.getTopicFilter === "function" && value.fragment;
}
async function getSubInfo(abiInterface, event) {
let topics;
let fragment = null;
if (Array.isArray(event)) {
const topicHashify = function(name) {
if (ethers.isHexString(name, 32)) {
return name;
}
const fragment2 = abiInterface.getEvent(name);
ethers.assertArgument(fragment2, "unknown fragment", "name", name);
return fragment2.topicHash;
};
topics = event.map((e) => {
if (e == null) {
return null;
}
if (Array.isArray(e)) {
return e.map(topicHashify);
}
return topicHashify(e);
});
} else if (event === "*") {
topics = [null];
} else if (typeof event === "string") {
if (ethers.isHexString(event, 32)) {
topics = [event];
} else {
fragment = abiInterface.getEvent(event);
ethers.assertArgument(fragment, "unknown fragment", "event", event);
topics = [fragment.topicHash];
}
} else if (isDeferred(event)) {
topics = await event.getTopicFilter();
} else if ("fragment" in event) {
fragment = event.fragment;
topics = [fragment.topicHash];
} else {
ethers.assertArgument(false, "unknown event name", "event", event);
}
topics = topics.map((t) => {
if (t == null) {
return null;
}
if (Array.isArray(t)) {
const items = Array.from(new Set(t.map((t2) => t2.toLowerCase())).values());
if (items.length === 1) {
return items[0];
}
items.sort();
return items;
}
return t.toLowerCase();
});
const tag = topics.map((t) => {
if (t == null) {
return "null";
}
if (Array.isArray(t)) {
return t.join("|");
}
return t;
}).join("&");
return { fragment, tag, topics };
}
async function multiQueryFilter(address, contract, event, fromBlock, toBlock) {
if (fromBlock == null) {
fromBlock = 0;
}
if (toBlock == null) {
toBlock = "latest";
}
const { fragment, topics } = await getSubInfo(contract.interface, event);
const filter = {
address: address === "*" ? void 0 : address,
topics,
fromBlock,
toBlock
};
const provider = contract.runner;
ethers.assert(provider, "contract runner does not have a provider", "UNSUPPORTED_OPERATION", { operation: "queryFilter" });
return (await provider.getLogs(filter)).map((log) => {
let foundFragment = fragment;
if (foundFragment == null) {
try {
foundFragment = contract.interface.getEvent(log.topics[0]);
} catch {
}
}
if (foundFragment) {
try {
return new ethers.EventLog(log, contract.interface, foundFragment);
} catch (error) {
return new ethers.UndecodedEventLog(log, error);
}
}
return new ethers.Log(log, provider);
});
}
class BatchBlockService {
provider;
onProgress;
@ -351,11 +452,14 @@ class BatchEventsService {
this.retryMax = retryMax;
this.retryOn = retryOn;
}
async getPastEvents({ fromBlock, toBlock, type }) {
async getPastEvents({ address, fromBlock, toBlock, type }) {
let err;
let retries = 0;
while (!this.shouldRetry && retries === 0 || this.shouldRetry && retries < this.retryMax) {
try {
if (address) {
return await multiQueryFilter(address, this.contract, type, fromBlock, toBlock);
}
return await this.contract.queryFilter(type, fromBlock, toBlock);
} catch (e) {
err = e;
@ -375,14 +479,14 @@ class BatchEventsService {
return this.getPastEvents(event);
});
}
async getBatchEvents({ fromBlock, toBlock, type = "*" }) {
async getBatchEvents({ address, fromBlock, toBlock, type = "*" }) {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
const eventsToSync = [];
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1;
eventsToSync.push({ fromBlock: i, toBlock: j, type });
eventsToSync.push({ address, fromBlock: i, toBlock: j, type });
}
const events = [];
const eventChunk = chunk(eventsToSync, this.concurrencySize);
@ -2086,7 +2190,7 @@ class BaseEventsService {
}
}
async getLatestEvents({ fromBlock }) {
if (this.tovarishClient?.selectedRelayer && !["Deposit", "Withdrawal"].includes(this.type)) {
if (this.tovarishClient?.selectedRelayer) {
const { events, lastSyncBlock: lastBlock } = await this.tovarishClient.getEvents({
type: this.getTovarishType(),
fromBlock
@ -2103,8 +2207,8 @@ class BaseEventsService {
/* eslint-disable @typescript-eslint/no-unused-vars */
async validateEvents({
events,
lastBlock,
hasNewEvents
newEvents,
lastBlock
}) {
return void 0;
}
@ -2140,8 +2244,8 @@ class BaseEventsService {
const lastBlock = newEvents.lastBlock || allEvents[allEvents.length - 1]?.blockNumber;
const validateResult = await this.validateEvents({
events: allEvents,
lastBlock,
hasNewEvents: Boolean(newEvents.events.length)
newEvents: newEvents.events,
lastBlock
});
if (savedEvents.fromCache || newEvents.events.length) {
await this.saveEvents({ events: allEvents, lastBlock });
@ -2220,7 +2324,7 @@ class BaseTornadoService extends BaseEventsService {
}
async validateEvents({
events,
hasNewEvents
newEvents
}) {
if (events.length && this.getType() === "Deposit") {
const depositEvents = events;
@ -2229,7 +2333,7 @@ class BaseTornadoService extends BaseEventsService {
const errMsg = `Deposit events invalid wants ${depositEvents.length - 1} leafIndex have ${lastEvent.leafIndex}`;
throw new Error(errMsg);
}
if (this.merkleTreeService && (!this.optionalTree || hasNewEvents)) {
if (this.merkleTreeService && (!this.optionalTree || newEvents.length)) {
return await this.merkleTreeService.verifyTree(depositEvents);
}
}
@ -2250,7 +2354,9 @@ class BaseTornadoService extends BaseEventsService {
lastBlock
};
}
return super.getLatestEvents({ fromBlock });
return await this.getEventsFromRpc({
fromBlock
});
}
}
class BaseEchoService extends BaseEventsService {
@ -10396,6 +10502,7 @@ exports.getProvider = getProvider;
exports.getProviderWithNetId = getProviderWithNetId;
exports.getRelayerEnsSubdomains = getRelayerEnsSubdomains;
exports.getStatusSchema = getStatusSchema;
exports.getSubInfo = getSubInfo;
exports.getSupportedInstances = getSupportedInstances;
exports.getTokenBalances = getTokenBalances;
exports.getTovarishNetworks = getTovarishNetworks;
@ -10415,6 +10522,7 @@ exports.loadDBEvents = loadDBEvents;
exports.loadRemoteEvents = loadRemoteEvents;
exports.makeLabelNodeAndParent = makeLabelNodeAndParent;
exports.mimc = mimc;
exports.multiQueryFilter = multiQueryFilter;
exports.multicall = multicall;
exports.numberFormatter = numberFormatter;
exports.packEncryptedMessage = packEncryptedMessage;

132
dist/index.mjs vendored
View File

@ -1,4 +1,4 @@
import { FetchRequest, JsonRpcProvider, Network, EnsPlugin, GasCostPlugin, Wallet, HDNodeWallet, VoidSigner, JsonRpcSigner, BrowserProvider, isAddress, parseEther, getAddress, AbiCoder, formatEther, namehash, dataSlice, dataLength, Interface, Contract, computeAddress, keccak256, EnsResolver, parseUnits, Transaction, Signature, MaxUint256, solidityPackedKeccak256, TypedDataEncoder, ZeroAddress } from 'ethers';
import { isHexString, assertArgument, assert, EventLog, UndecodedEventLog, Log, FetchRequest, JsonRpcProvider, Network, EnsPlugin, GasCostPlugin, Wallet, HDNodeWallet, VoidSigner, JsonRpcSigner, BrowserProvider, isAddress, parseEther, getAddress, AbiCoder, formatEther, namehash, dataSlice, dataLength, Interface, Contract, computeAddress, keccak256, EnsResolver, parseUnits, Transaction, Signature, MaxUint256, solidityPackedKeccak256, TypedDataEncoder, ZeroAddress } from 'ethers';
import { Tornado__factory } from '@tornado/contracts';
import { webcrypto } from 'crypto';
import BN from 'bn.js';
@ -132,6 +132,107 @@ function fromContentHash(contentHash) {
return contentHashUtils.decode(contentHash);
}
function isDeferred(value) {
return value && typeof value === "object" && "getTopicFilter" in value && typeof value.getTopicFilter === "function" && value.fragment;
}
async function getSubInfo(abiInterface, event) {
let topics;
let fragment = null;
if (Array.isArray(event)) {
const topicHashify = function(name) {
if (isHexString(name, 32)) {
return name;
}
const fragment2 = abiInterface.getEvent(name);
assertArgument(fragment2, "unknown fragment", "name", name);
return fragment2.topicHash;
};
topics = event.map((e) => {
if (e == null) {
return null;
}
if (Array.isArray(e)) {
return e.map(topicHashify);
}
return topicHashify(e);
});
} else if (event === "*") {
topics = [null];
} else if (typeof event === "string") {
if (isHexString(event, 32)) {
topics = [event];
} else {
fragment = abiInterface.getEvent(event);
assertArgument(fragment, "unknown fragment", "event", event);
topics = [fragment.topicHash];
}
} else if (isDeferred(event)) {
topics = await event.getTopicFilter();
} else if ("fragment" in event) {
fragment = event.fragment;
topics = [fragment.topicHash];
} else {
assertArgument(false, "unknown event name", "event", event);
}
topics = topics.map((t) => {
if (t == null) {
return null;
}
if (Array.isArray(t)) {
const items = Array.from(new Set(t.map((t2) => t2.toLowerCase())).values());
if (items.length === 1) {
return items[0];
}
items.sort();
return items;
}
return t.toLowerCase();
});
const tag = topics.map((t) => {
if (t == null) {
return "null";
}
if (Array.isArray(t)) {
return t.join("|");
}
return t;
}).join("&");
return { fragment, tag, topics };
}
async function multiQueryFilter(address, contract, event, fromBlock, toBlock) {
if (fromBlock == null) {
fromBlock = 0;
}
if (toBlock == null) {
toBlock = "latest";
}
const { fragment, topics } = await getSubInfo(contract.interface, event);
const filter = {
address: address === "*" ? void 0 : address,
topics,
fromBlock,
toBlock
};
const provider = contract.runner;
assert(provider, "contract runner does not have a provider", "UNSUPPORTED_OPERATION", { operation: "queryFilter" });
return (await provider.getLogs(filter)).map((log) => {
let foundFragment = fragment;
if (foundFragment == null) {
try {
foundFragment = contract.interface.getEvent(log.topics[0]);
} catch {
}
}
if (foundFragment) {
try {
return new EventLog(log, contract.interface, foundFragment);
} catch (error) {
return new UndecodedEventLog(log, error);
}
}
return new Log(log, provider);
});
}
class BatchBlockService {
provider;
onProgress;
@ -329,11 +430,14 @@ class BatchEventsService {
this.retryMax = retryMax;
this.retryOn = retryOn;
}
async getPastEvents({ fromBlock, toBlock, type }) {
async getPastEvents({ address, fromBlock, toBlock, type }) {
let err;
let retries = 0;
while (!this.shouldRetry && retries === 0 || this.shouldRetry && retries < this.retryMax) {
try {
if (address) {
return await multiQueryFilter(address, this.contract, type, fromBlock, toBlock);
}
return await this.contract.queryFilter(type, fromBlock, toBlock);
} catch (e) {
err = e;
@ -353,14 +457,14 @@ class BatchEventsService {
return this.getPastEvents(event);
});
}
async getBatchEvents({ fromBlock, toBlock, type = "*" }) {
async getBatchEvents({ address, fromBlock, toBlock, type = "*" }) {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
const eventsToSync = [];
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1;
eventsToSync.push({ fromBlock: i, toBlock: j, type });
eventsToSync.push({ address, fromBlock: i, toBlock: j, type });
}
const events = [];
const eventChunk = chunk(eventsToSync, this.concurrencySize);
@ -2064,7 +2168,7 @@ class BaseEventsService {
}
}
async getLatestEvents({ fromBlock }) {
if (this.tovarishClient?.selectedRelayer && !["Deposit", "Withdrawal"].includes(this.type)) {
if (this.tovarishClient?.selectedRelayer) {
const { events, lastSyncBlock: lastBlock } = await this.tovarishClient.getEvents({
type: this.getTovarishType(),
fromBlock
@ -2081,8 +2185,8 @@ class BaseEventsService {
/* eslint-disable @typescript-eslint/no-unused-vars */
async validateEvents({
events,
lastBlock,
hasNewEvents
newEvents,
lastBlock
}) {
return void 0;
}
@ -2118,8 +2222,8 @@ class BaseEventsService {
const lastBlock = newEvents.lastBlock || allEvents[allEvents.length - 1]?.blockNumber;
const validateResult = await this.validateEvents({
events: allEvents,
lastBlock,
hasNewEvents: Boolean(newEvents.events.length)
newEvents: newEvents.events,
lastBlock
});
if (savedEvents.fromCache || newEvents.events.length) {
await this.saveEvents({ events: allEvents, lastBlock });
@ -2198,7 +2302,7 @@ class BaseTornadoService extends BaseEventsService {
}
async validateEvents({
events,
hasNewEvents
newEvents
}) {
if (events.length && this.getType() === "Deposit") {
const depositEvents = events;
@ -2207,7 +2311,7 @@ class BaseTornadoService extends BaseEventsService {
const errMsg = `Deposit events invalid wants ${depositEvents.length - 1} leafIndex have ${lastEvent.leafIndex}`;
throw new Error(errMsg);
}
if (this.merkleTreeService && (!this.optionalTree || hasNewEvents)) {
if (this.merkleTreeService && (!this.optionalTree || newEvents.length)) {
return await this.merkleTreeService.verifyTree(depositEvents);
}
}
@ -2228,7 +2332,9 @@ class BaseTornadoService extends BaseEventsService {
lastBlock
};
}
return super.getLatestEvents({ fromBlock });
return await this.getEventsFromRpc({
fromBlock
});
}
}
class BaseEchoService extends BaseEventsService {
@ -10270,4 +10376,4 @@ async function calculateSnarkProof(input, circuit, provingKey) {
return { proof, args };
}
export { BaseEchoService, BaseEncryptedNotesService, BaseEventsService, BaseGovernanceService, BaseRegistryService, BaseRevenueService, BaseTornadoService, BatchBlockService, BatchEventsService, BatchTransactionService, DBEchoService, DBEncryptedNotesService, DBGovernanceService, DBRegistryService, DBRevenueService, DBTornadoService, Deposit, ENSNameWrapper__factory, ENSRegistry__factory, ENSResolver__factory, ENSUtils, ENS__factory, ERC20__factory, EnsContracts, INDEX_DB_ERROR, IndexedDB, Invoice, MAX_FEE, MAX_TOVARISH_EVENTS, MIN_FEE, MIN_STAKE_BALANCE, MerkleTreeService, Mimc, Multicall__factory, NetId, NoteAccount, OffchainOracle__factory, OvmGasPriceOracle__factory, Pedersen, RelayerClient, ReverseRecords__factory, TokenPriceOracle, TornadoBrowserProvider, TornadoFeeOracle, TornadoRpcSigner, TornadoVoidSigner, TornadoWallet, TovarishClient, addNetwork, addressSchemaType, ajv, base64ToBytes, bigIntReplacer, bnSchemaType, bnToBytes, buffPedersenHash, bufferToBytes, bytes32BNSchemaType, bytes32SchemaType, bytesToBN, bytesToBase64, bytesToHex, calculateScore, calculateSnarkProof, chunk, concatBytes, convertETHToTokenAmount, createDeposit, crypto, customConfig, defaultConfig, defaultUserAgent, deployHasher, depositsEventsSchema, digest, downloadZip, echoEventsSchema, enabledChains, encodedLabelToLabelhash, encryptedNotesSchema, index as factories, fetchData, fetchGetUrlFunc, fetchIp, fromContentHash, gasZipID, gasZipInbounds, gasZipInput, gasZipMinMax, getActiveTokenInstances, getActiveTokens, getConfig, getEventsSchemaValidator, getHttpAgent, getIndexedDB, getInstanceByAddress, getNetworkConfig, getPermit2CommitmentsSignature, getPermit2Signature, getPermitCommitmentsSignature, getPermitSignature, getProvider, getProviderWithNetId, getRelayerEnsSubdomains, getStatusSchema, getSupportedInstances, getTokenBalances, getTovarishNetworks, getWeightRandom, governanceEventsSchema, hasherBytecode, hexToBytes, initGroth16, isHex, isNode, jobRequestSchema, jobsSchema, labelhash, leBuff2Int, leInt2Buff, loadDBEvents, loadRemoteEvents, makeLabelNodeAndParent, mimc, multicall, numberFormatter, packEncryptedMessage, parseInvoice, parseNote, pedersen, permit2Address, pickWeightedRandomRelayer, populateTransaction, proofSchemaType, proposalState, rBigInt, rHex, relayerRegistryEventsSchema, saveDBEvents, sleep, stakeBurnedEventsSchema, substring, toContentHash, toFixedHex, toFixedLength, unpackEncryptedMessage, unzipAsync, validateUrl, withdrawalsEventsSchema, zipAsync };
export { BaseEchoService, BaseEncryptedNotesService, BaseEventsService, BaseGovernanceService, BaseRegistryService, BaseRevenueService, BaseTornadoService, BatchBlockService, BatchEventsService, BatchTransactionService, DBEchoService, DBEncryptedNotesService, DBGovernanceService, DBRegistryService, DBRevenueService, DBTornadoService, Deposit, ENSNameWrapper__factory, ENSRegistry__factory, ENSResolver__factory, ENSUtils, ENS__factory, ERC20__factory, EnsContracts, INDEX_DB_ERROR, IndexedDB, Invoice, MAX_FEE, MAX_TOVARISH_EVENTS, MIN_FEE, MIN_STAKE_BALANCE, MerkleTreeService, Mimc, Multicall__factory, NetId, NoteAccount, OffchainOracle__factory, OvmGasPriceOracle__factory, Pedersen, RelayerClient, ReverseRecords__factory, TokenPriceOracle, TornadoBrowserProvider, TornadoFeeOracle, TornadoRpcSigner, TornadoVoidSigner, TornadoWallet, TovarishClient, addNetwork, addressSchemaType, ajv, base64ToBytes, bigIntReplacer, bnSchemaType, bnToBytes, buffPedersenHash, bufferToBytes, bytes32BNSchemaType, bytes32SchemaType, bytesToBN, bytesToBase64, bytesToHex, calculateScore, calculateSnarkProof, chunk, concatBytes, convertETHToTokenAmount, createDeposit, crypto, customConfig, defaultConfig, defaultUserAgent, deployHasher, depositsEventsSchema, digest, downloadZip, echoEventsSchema, enabledChains, encodedLabelToLabelhash, encryptedNotesSchema, index as factories, fetchData, fetchGetUrlFunc, fetchIp, fromContentHash, gasZipID, gasZipInbounds, gasZipInput, gasZipMinMax, getActiveTokenInstances, getActiveTokens, getConfig, getEventsSchemaValidator, getHttpAgent, getIndexedDB, getInstanceByAddress, getNetworkConfig, getPermit2CommitmentsSignature, getPermit2Signature, getPermitCommitmentsSignature, getPermitSignature, getProvider, getProviderWithNetId, getRelayerEnsSubdomains, getStatusSchema, getSubInfo, getSupportedInstances, getTokenBalances, getTovarishNetworks, getWeightRandom, governanceEventsSchema, hasherBytecode, hexToBytes, initGroth16, isHex, isNode, jobRequestSchema, jobsSchema, labelhash, leBuff2Int, leInt2Buff, loadDBEvents, loadRemoteEvents, makeLabelNodeAndParent, mimc, multiQueryFilter, multicall, numberFormatter, packEncryptedMessage, parseInvoice, parseNote, pedersen, permit2Address, pickWeightedRandomRelayer, populateTransaction, proofSchemaType, proposalState, rBigInt, rHex, relayerRegistryEventsSchema, saveDBEvents, sleep, stakeBurnedEventsSchema, substring, toContentHash, toFixedHex, toFixedLength, unpackEncryptedMessage, unzipAsync, validateUrl, withdrawalsEventsSchema, zipAsync };

691
dist/tornado.umd.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import type {
import {
Provider,
BlockTag,
Block,
@ -7,9 +7,171 @@ import type {
ContractEventName,
EventLog,
TransactionReceipt,
isHexString,
assert,
assertArgument,
DeferredTopicFilter,
EventFragment,
TopicFilter,
Interface,
UndecodedEventLog,
Log,
} from 'ethers';
import { chunk, sleep } from './utils';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isDeferred(value: any): value is DeferredTopicFilter {
return (
value &&
typeof value === 'object' &&
'getTopicFilter' in value &&
typeof value.getTopicFilter === 'function' &&
value.fragment
);
}
/**
* Copied from ethers.js as they don't export this function
* https://github.com/ethers-io/ethers.js/blob/main/src.ts/contract/contract.ts#L464
*/
export async function getSubInfo(
abiInterface: Interface,
event: ContractEventName,
): Promise<{
fragment: null | EventFragment;
tag: string;
topics: TopicFilter;
}> {
let topics: Array<null | string | Array<string>>;
let fragment: null | EventFragment = null;
// Convert named events to topicHash and get the fragment for
// events which need deconstructing.
if (Array.isArray(event)) {
const topicHashify = function (name: string): string {
if (isHexString(name, 32)) {
return name;
}
const fragment = abiInterface.getEvent(name);
assertArgument(fragment, 'unknown fragment', 'name', name);
return fragment.topicHash;
};
// Array of Topics and Names; e.g. `[ "0x1234...89ab", "Transfer(address)" ]`
topics = event.map((e) => {
if (e == null) {
return null;
}
if (Array.isArray(e)) {
return e.map(topicHashify);
}
return topicHashify(e);
});
} else if (event === '*') {
topics = [null];
} else if (typeof event === 'string') {
if (isHexString(event, 32)) {
// Topic Hash
topics = [event];
} else {
// Name or Signature; e.g. `"Transfer", `"Transfer(address)"`
fragment = abiInterface.getEvent(event);
assertArgument(fragment, 'unknown fragment', 'event', event);
topics = [fragment.topicHash];
}
} else if (isDeferred(event)) {
// Deferred Topic Filter; e.g. `contract.filter.Transfer(from)`
topics = await event.getTopicFilter();
} else if ('fragment' in event) {
// ContractEvent; e.g. `contract.filter.Transfer`
fragment = event.fragment;
topics = [fragment.topicHash];
} else {
assertArgument(false, 'unknown event name', 'event', event);
}
// Normalize topics and sort TopicSets
topics = topics.map((t) => {
if (t == null) {
return null;
}
if (Array.isArray(t)) {
const items = Array.from(new Set(t.map((t) => t.toLowerCase())).values());
if (items.length === 1) {
return items[0];
}
items.sort();
return items;
}
return t.toLowerCase();
});
const tag = topics
.map((t) => {
if (t == null) {
return 'null';
}
if (Array.isArray(t)) {
return t.join('|');
}
return t;
})
.join('&');
return { fragment, tag, topics };
}
export async function multiQueryFilter(
// Single address will scan for a single contract, array for multiple, and * for all contracts with event topic
address: string | string[],
contract: BaseContract,
event: ContractEventName,
fromBlock?: BlockTag,
toBlock?: BlockTag,
) {
if (fromBlock == null) {
fromBlock = 0;
}
if (toBlock == null) {
toBlock = 'latest';
}
const { fragment, topics } = await getSubInfo(contract.interface, event);
const filter = {
address: address === '*' ? undefined : address,
topics,
fromBlock,
toBlock,
};
const provider = contract.runner as Provider | null;
assert(provider, 'contract runner does not have a provider', 'UNSUPPORTED_OPERATION', { operation: 'queryFilter' });
return (await provider.getLogs(filter)).map((log) => {
let foundFragment = fragment;
if (foundFragment == null) {
try {
foundFragment = contract.interface.getEvent(log.topics[0]);
// eslint-disable-next-line no-empty
} catch {}
}
if (foundFragment) {
try {
return new EventLog(log, contract.interface, foundFragment);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return new UndecodedEventLog(log, error);
}
}
return new Log(log, provider);
});
}
export interface BatchBlockServiceConstructor {
provider: Provider;
onProgress?: BatchBlockOnProgress;
@ -284,6 +446,7 @@ export type BatchEventOnProgress = ({
// To enable iteration only numbers are accepted for fromBlock input
export interface EventInput {
address?: string | string[];
fromBlock: number;
toBlock: number;
type: ContractEventName;
@ -321,13 +484,16 @@ export class BatchEventsService {
this.retryOn = retryOn;
}
async getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]> {
async getPastEvents({ address, fromBlock, toBlock, type }: EventInput): Promise<EventLog[]> {
let err;
let retries = 0;
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
try {
if (address) {
return (await multiQueryFilter(address, this.contract, type, fromBlock, toBlock)) as EventLog[];
}
return (await this.contract.queryFilter(type, fromBlock, toBlock)) as EventLog[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
@ -357,7 +523,7 @@ export class BatchEventsService {
});
}
async getBatchEvents({ fromBlock, toBlock, type = '*' }: EventInput): Promise<EventLog[]> {
async getBatchEvents({ address, fromBlock, toBlock, type = '*' }: EventInput): Promise<EventLog[]> {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
@ -367,7 +533,7 @@ export class BatchEventsService {
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1;
eventsToSync.push({ fromBlock: i, toBlock: j, type });
eventsToSync.push({ address, fromBlock: i, toBlock: j, type });
}
const events = [];

View File

@ -205,7 +205,7 @@ export class BaseEventsService<EventType extends MinimalEvents> {
}
async getLatestEvents({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<EventType>> {
if (this.tovarishClient?.selectedRelayer && !['Deposit', 'Withdrawal'].includes(this.type)) {
if (this.tovarishClient?.selectedRelayer) {
const { events, lastSyncBlock: lastBlock } = await this.tovarishClient.getEvents<EventType>({
type: this.getTovarishType(),
fromBlock,
@ -225,9 +225,9 @@ export class BaseEventsService<EventType extends MinimalEvents> {
/* eslint-disable @typescript-eslint/no-unused-vars */
async validateEvents<S>({
events,
newEvents,
lastBlock,
hasNewEvents,
}: BaseEvents<EventType> & { hasNewEvents?: boolean }): Promise<S> {
}: BaseEvents<EventType> & { newEvents: EventType[] }): Promise<S> {
return undefined as S;
}
/* eslint-enable @typescript-eslint/no-unused-vars */
@ -274,8 +274,8 @@ export class BaseEventsService<EventType extends MinimalEvents> {
const validateResult = await this.validateEvents<S>({
events: allEvents,
newEvents: newEvents.events,
lastBlock,
hasNewEvents: Boolean(newEvents.events.length),
});
// If the events are loaded from cache or we have found new events, save them
@ -380,9 +380,9 @@ export class BaseTornadoService extends BaseEventsService<DepositsEvents | Withd
async validateEvents<S>({
events,
hasNewEvents,
newEvents,
}: BaseEvents<DepositsEvents | WithdrawalsEvents> & {
hasNewEvents?: boolean;
newEvents: (DepositsEvents | WithdrawalsEvents)[];
}) {
if (events.length && this.getType() === 'Deposit') {
const depositEvents = events as DepositsEvents[];
@ -394,7 +394,7 @@ export class BaseTornadoService extends BaseEventsService<DepositsEvents | Withd
throw new Error(errMsg);
}
if (this.merkleTreeService && (!this.optionalTree || hasNewEvents)) {
if (this.merkleTreeService && (!this.optionalTree || newEvents.length)) {
return (await this.merkleTreeService.verifyTree(depositEvents)) as S;
}
}
@ -423,7 +423,9 @@ export class BaseTornadoService extends BaseEventsService<DepositsEvents | Withd
};
}
return super.getLatestEvents({ fromBlock });
return await this.getEventsFromRpc({
fromBlock,
});
}
}