diff --git a/.env.example b/.env.example index 0e7bbf9..fe9c5b4 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,13 @@ -# All of these are used for tests -# If someone is using the SDK, there is no reason to use .env +# RPC URLs +ETH_MAINNET_TEST_RPC= -## Test behaviour - -# Debug (Whether to log debug events) +# debug (debug events are logged to console) DEBUG= -# Tor -# Torify tests (need to make possible on each still) +# use tor (torify tests) TORIFY= -# Tor port (regular = 9050, browser = 9150) +# tor port (regular = 9050, browser = 9150) TOR_PORT= -# RPCs -ETH_MAINNET_TEST_RPC= \ No newline at end of file +# relayer DOMAIN (the example.xyz in https://example.xyz) for testing +TEST_RELAYER_DOMAIN= \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 3c9b3c4..e7a0b13 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,16 @@ # History +### 2023.04.28 (2023-04-28) + +Did: + +* Had to run those "few more tests". Finally done with those, there was a promise not being resolved which ended up being test listeners. Noted. +* Synchronization is now abstracted into the inheritable Synchronizer which will be reused for different things, for example relayer registry operations next. + +Next: + +* MONOREPO!!!!!!!!!!!!!!! STARTING TODAY + ### 2023.04.23 (2023-04-23) Did: diff --git a/package.json b/package.json index 9d21aca..9ee2890 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "zk" ], "private": false, - "version": "2023.04.23", + "version": "2023.04.28", "engines": { "node": "^18" }, diff --git a/src/lib/chain.ts b/src/lib/chain.ts index 6cb4a6c..51b94f5 100644 --- a/src/lib/chain.ts +++ b/src/lib/chain.ts @@ -1,6 +1,19 @@ -// Types -import { MarkOptional } from 'ts-essentials' -import * as Types from 'types/sdk/chain' +// Externals types +import { MarkOptional, DeepRequired } from 'ts-essentials' + +// External modules +import EventEmitter from 'events' +import { randomBytes } from 'crypto' +import { TransactionRequest } from '@ethersproject/abstract-provider' +import { + EventFilter, + BaseContract, + BigNumber, + ContractTransaction, + providers, + Signer, + VoidSigner +} from 'ethers' // Our local types import { @@ -15,15 +28,14 @@ import { Multicall3Contract__factory } from 'types/deth' import { Multicall3 } from 'types/deth/Multicall3Contract' - -// External imports -import { TransactionRequest } from '@ethersproject/abstract-provider' -import { BaseContract, BigNumber, ContractTransaction, providers, Signer, VoidSigner } from 'ethers' -import { randomBytes } from 'crypto' +import { TornadoContracts, Options } from 'types/sdk/chain' // Local modules -import { Onchain } from 'lib/data' -import { ErrorUtils, HexUtils } from 'lib/utils' +import { Onchain, Cache, Docs } from 'lib/data' +import { ErrorUtils, HexUtils, AsyncUtils } from 'lib/utils' + +// @ts-ignore +import { parseIndexableString } from 'pouchdb-collate' // We use a vanilla provider here, but in reality we will probably // add a censorship-checking custom derivative of it @@ -127,7 +139,7 @@ export class Chain { * This is Tornado-specific. */ export namespace Contracts { - function _getContract( + function _getContract( name: string, address: string, signerOrProvider: Signer | Provider @@ -199,3 +211,98 @@ export namespace Contracts { return contractMap.get(key) as ERC20 } } + +export abstract class Synchronizer extends EventEmitter { + async sync( + event: EventFilter, + contract: BaseContract, + cache: Cache.Syncable, + options?: Options.Sync + ): Promise { + const _options = await this._populateSyncOptions(options) + + // Assign pooler + cache.initializePooler(cache.getCallbacks(contract), cache.getErrorHandlers(), _options.concurrencyLimit) + + // Decide whether we have a latest block + const numEntries = (await cache.db.info()).doc_count + + // Check for synced blocks + if (0 < numEntries) { + const [lastSyncedBlock, ,] = parseIndexableString( + (await cache.db.allDocs({ descending: true, limit: 1 })).rows[0].id + ) + _options.startBlock = lastSyncedBlock < _options.startBlock ? _options.startBlock : lastSyncedBlock + _options.blockDelta = Math.floor((_options.targetBlock - _options.startBlock) / _options.blockDivisor) + } + + // Start synchronizing + let dbPromises = [] + + this.emit('debug', _options.startBlock, _options.targetBlock, _options.blockDelta) + + this.emit('sync', 'syncing') + + for ( + let currentBlock = _options.startBlock, + blockDelta = _options.blockDelta, + targetBlock = _options.targetBlock, + concurrencyLimit = _options.concurrencyLimit; + currentBlock < targetBlock; + currentBlock += blockDelta + ) { + if (cache.pooler!.pending < concurrencyLimit) { + const sum = currentBlock + blockDelta + + await AsyncUtils.timeout(_options.msTimeout) + + if (currentBlock + blockDelta < targetBlock) { + await cache.pooler!.pool(currentBlock, sum) + } else { + await cache.pooler!.pool(currentBlock, sum - (sum % targetBlock)) + } + + this.emit('debug', currentBlock++, sum) + } else { + let res: Array = await cache.pooler!.race() + + if (res.length != 0) + dbPromises.push( + cache.db.bulkDocs(res.map((el) => cache.buildDoc(el))).catch((err) => { + throw ErrorUtils.ensureError(err) + }) + ) + + currentBlock -= blockDelta + } + } + + this.emit('sync', 'synced') + + // Immediately start listening if we're doing this + if (_options.listenForEvents) { + contract = contract.on(event, (...eventArgs) => { + this.emit(cache.name, 'received', cache.db.put(cache.buildDoc(eventArgs[eventArgs.length - 1]))) + }) + } + + // Then wait for all pooler requests to resolve + let results = await cache.pooler!.all() + + // Then transform them, we know the shape in forward + results = results.reduce((res: any[], response: any[]) => { + if (response[0]) response.forEach((el: any) => res.push(cache.buildDoc(el))) + return res + }, []) + + // Then wait for old dbPromises to resolve + await Promise.all(dbPromises) + + // Add the last docs + await cache.db.bulkDocs(results).catch((err) => { + throw ErrorUtils.ensureError(err) + }) + } + + protected abstract _populateSyncOptions(options?: Options.Sync): Promise> +} diff --git a/src/lib/core.ts b/src/lib/core.ts index b2f950f..6be66f4 100644 --- a/src/lib/core.ts +++ b/src/lib/core.ts @@ -2,14 +2,14 @@ import { DeepRequired, MarkOptional } from 'ts-essentials' // Local types -import { RelayerProperties as RelayerDataProperties } from 'types/sdk/data' +import { Options as ChainOptions } from 'types/sdk/chain' +import { RelayerProperties as RelayerDataProperties, Options as DataOptions } from 'types/sdk/data' import { Options, Transactions } from 'types/sdk/core' import { ZKDepositData, InputFor } from 'types/sdk/crypto' import { TornadoInstance, TornadoProxy } from 'types/deth' // External imports -import { EventEmitter } from 'stream' -import { BigNumber, EventFilter, providers } from 'ethers' +import { BigNumber, providers } from 'ethers' import { parseUnits } from 'ethers/lib/utils' import { bigInt } from 'snarkjs' @@ -17,10 +17,75 @@ import { bigInt } from 'snarkjs' import { parseIndexableString } from 'pouchdb-collate' // Local imports -import { Docs, Cache, Types as DataTypes, Json, Constants, Onchain } from 'lib/data' import { Primitives } from 'lib/crypto' -import { Contracts, Chain } from 'lib/chain' import { ErrorUtils, ObjectUtils, AsyncUtils } from 'lib/utils' +import { Docs, Cache, Keys, Constants, Onchain } from 'lib/data' +import { Contracts, Chain, Synchronizer } from 'lib/chain' + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FOR SYNCHRONIZATION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +function tornadoSyncErrorHandler( + err: Error, + numResolvedPromises: number, + callbackIndex: number, + orderIndex: number, + ...args: any[] +): void { + err = ErrorUtils.ensureError(err) + + if (err.message.match('context deadline exceeded')) + console.error( + ErrorUtils.getError( + `Context deadline exceeded, stop if more promises do not resolve. Resolved: ${numResolvedPromises}` + ) + ) + else if (err.message.match('Invalid JSON RPC')) + console.error( + ErrorUtils.getError(`Endpoint returned invalid value (we might be rate limited), retrying.`) + ) + else { + err.message += `\nCallback args supplied: [${args.join(', ')}]\n` + throw err + } +} + +export class DepositCache extends Cache.Syncable { + buildDoc(response: any): Docs.Deposit { + return new Docs.Deposit(response) + } + + getErrorHandlers(): Array { + return [tornadoSyncErrorHandler] + } + + getCallbacks(instance: TornadoInstance): Array { + return [ + (fromBlock: number, toBlock: number) => { + return instance.queryFilter(instance.filters.Deposit(null, null, null), fromBlock, toBlock) + } + ] + } +} + +export class WithdrawalCache extends Cache.Syncable { + buildDoc(response: any): Docs.Withdrawal { + return new Docs.Withdrawal(response) + } + + getErrorHandlers(): Array { + return [tornadoSyncErrorHandler] + } + + getCallbacks(instance: TornadoInstance): Array { + return [ + (fromBlock: number, toBlock: number) => { + return instance.queryFilter(instance.filters.Withdrawal(null, null, null, null), fromBlock, toBlock) + } + ] + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CORE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ type Provider = providers.Provider @@ -37,7 +102,7 @@ type RelayerProperties = MarkOptional< 'serviceFeePercent' | 'prices' > -export class Core extends EventEmitter { +export class Core extends Synchronizer { chain: Chain caches: Map> instances: Map @@ -65,8 +130,12 @@ export class Core extends EventEmitter { } async getInstance(token: string, denomination: number | string): Promise { - const chainId = await this.chain.getChainId() - return Contracts.getInstance(String(chainId), token, String(denomination), this.chain.provider) + const chainId = String(await this.chain.getChainId()) + token = token.toLowerCase() + denomination = String(denomination) + if (this.instances.has(chainId + token + denomination)) + return this.instances.get(chainId + token + denomination)! + else return Contracts.getInstance(chainId, token, denomination, this.chain.provider) } async getProxy(): Promise { @@ -210,10 +279,9 @@ export class Core extends EventEmitter { const toWithdraw = BigNumber.from(+lookupKeys.denomination * 10 ** lookupKeys.denomination.length) .mul(decimals) .div(10 ** lookupKeys.denomination.length) - const native = lookupKeys.token !== (await this.chain.getChainSymbol()) + const native = lookupKeys.token == (await this.chain.getChainSymbol()) - // TODO: Decide if necessary - if (!tokenPrice && native) + if (!tokenPrice && !native) throw ErrorUtils.getError( 'Core.buildDepositProofs: a token price MUST be supplied if the token withdrawn is not native.' ) @@ -272,8 +340,8 @@ export class Core extends EventEmitter { ethBought: BigNumber, tokenPriceInEth?: BigNumber ): typeof bigInt { - const factor = BigNumber.from(10).pow(String(relayerServiceFee).length) - const baseRelayerFee = toWithdraw.mul(BigNumber.from(relayerServiceFee).mul(factor)).div(factor) + const factor = 10 ** String(relayerServiceFee).length + const baseRelayerFee = toWithdraw.mul(BigNumber.from(relayerServiceFee * factor)).div(factor) const txCost = gasPrice.add(gasPriceCushion).mul(5e5) if (tokenPriceInEth) { // @ts-expect-error @@ -283,9 +351,51 @@ export class Core extends EventEmitter { else return bigInt(txCost.add(baseRelayerFee).toString()) } + /** + * @param instanceName The name of the instance as created in `_sync` function. + * @param commitments The commitments for which the leaf index values are to be noted down extra. + * @returns The result of concatenating the array of leaf indices found by matching them with the provided commitment values, followed by the array of all leaf indices, including all of the formerly mentioned values given that they are valid. Values which have not been matched, meaning probably invalid values, will be `0`. + */ + private async _findLeavesAndIndices( + instanceName: string, + commitments: Array + ): Promise<[Array, Array]> { + const indices = new Array(commitments.length).fill(0) + const leaves: Array = [] + + const cache = this.loadCache>(instanceName) + const docs = await cache.db.allDocs() + + // If no docs in cache throw and stop + if (docs.total_rows === 0) { + await cache.clear() + throw ErrorUtils.getError( + `Core.buildMerkleTree: events for instance ${instanceName} have not been synchronized.` + ) + } + + // Otherwise start looking for commitment leaf indices and also pick up all other leafs on the way + for (const row of docs.rows) { + const [, leafIndex, loadedCommitment] = parseIndexableString(row.id) + const index = commitments.findIndex((commitment) => commitment === loadedCommitment) + + // If some commitment is found then add the leaf index and remove that commitment + if (index !== -1) { + indices[index] = leafIndex + commitments.splice(index, 1) + } + + // In any case push every leaf + leaves.push(BigNumber.from(loadedCommitment).toString()) + } + + // Concat matched and all leaf indices + return [leaves, indices] + } + async loadNotes( indexes?: Array, - keys?: Partial + keys?: Partial ): Promise> { const rows = await Cache.loadContents('DepositNotes') @@ -450,262 +560,111 @@ export class Core extends EventEmitter { throw ErrorUtils.ensureError(err) }) - // TODO: Decide whether to close caches by default or not - //await cache.close().catch((err) => { - // throw ErrorUtils.ensureError(err) - //}) + // TODO: Decide whether to close caches by default or not + //await cache.close().catch((err) => { + // throw ErrorUtils.ensureError(err) + //}) } - loadWithdrawalCache(name: string, options?: Options.Core.Cache): Cache.Withdrawal { + loadDepositCache(name: string, options?: ChainOptions.Sync): DepositCache { if (!this.caches.has(name)) { - this.caches.set(name, new Cache.Withdrawal(name, options)) + this.caches.set( + name, + new DepositCache( + name, + options ? { adapter: options?.cacheAdapter, persistent: options?.persistentCache } : undefined + ) + ) } - return this.caches.get(name) as Cache.Withdrawal + return this.caches.get(name) as DepositCache } - loadDepositCache(name: string, options?: Options.Core.Cache): Cache.Deposit { + loadWithdrawalCache(name: string, options?: ChainOptions.Sync): WithdrawalCache { if (!this.caches.has(name)) { - this.caches.set(name, new Cache.Deposit(name, options)) + this.caches.set( + name, + new WithdrawalCache( + name, + options ? { adapter: options?.cacheAdapter, persistent: options?.persistentCache } : undefined + ) + ) } - return this.caches.get(name) as Cache.Deposit + return this.caches.get(name) as WithdrawalCache } - loadCache>(name: string, options?: Options.Cache.Database): C { + loadCache>(name: string, options?: ChainOptions.Sync): C { if (!this.caches.has(name)) { - this.caches.set(name, new Cache.Base(name, options)) + this.caches.set( + name, + new Cache.Base( + name, + options ? { adapter: options?.cacheAdapter, persistent: options?.persistentCache } : undefined + ) + ) } return this.caches.get(name) as C } - async syncMultiple(instances: Array, syncOptions?: Options.Core.Sync): Promise { - for (const instance of instances) { - await this.sync(instance, syncOptions) - } - } - - async sync(instance: TornadoInstance, syncOptions?: Options.Core.Sync): Promise { - // Get some data + async syncDeposits(instance: TornadoInstance, options?: ChainOptions.Sync): Promise { const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address) + const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination - const populatedSyncOpts = await this._populateSyncOpts(lookupKeys, syncOptions) - - const actions = Object.entries(populatedSyncOpts).filter((el) => el[1] === true) as [string, boolean][] - - // Synchronize - for (let i = 0, bound = actions.length; i < bound; i++) { - const action = actions[i][0].charAt(0).toUpperCase() + actions[i][0].slice(1) - const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination - const name = action + 's' + pathstring.toUpperCase() - - if (action == 'Deposit') - await this._sync( - pathstring, - this.loadDepositCache(name, syncOptions?.cache), - instance.filters.Deposit(null, null, null), - instance, - populatedSyncOpts - ) - else if (action == 'Withdrawal') - await this._sync( - pathstring, - this.loadWithdrawalCache(name, syncOptions?.cache), - instance.filters.Withdrawal(null, null, null, null), - instance, - populatedSyncOpts - ) - } - } - - private async _sync( - pathstring: string, - cache: Cache.Syncable, - filter: EventFilter, - instance: TornadoInstance, - syncOptions: DeepRequired - ): Promise { - // Assign pooler - cache.sync.initializePooler(cache.getCallbacks(instance), cache.getErrorHandlers()) - - // Decide whether we have a latest block - const numEntries = (await cache.db.info()).doc_count - - // Check for synced blocks - if (0 < numEntries) { - const [lastSyncedBlock, ,] = parseIndexableString( - (await cache.db.allDocs({ descending: true, limit: 1 })).rows[0].id - ) - syncOptions.blocks.startBlock = - lastSyncedBlock < syncOptions.blocks.startBlock ? syncOptions.blocks.startBlock : lastSyncedBlock - syncOptions.blocks.blockDelta = this._getBlockDelta(syncOptions) - } - - // Start synchronizing - let dbPromises = [] - - this.emit( - 'debug', - syncOptions.blocks.startBlock, - syncOptions.blocks.targetBlock, - syncOptions.blocks.blockDelta + options = options ?? {} + options.startBlock = await Onchain.getInstanceDeployBlockNum( + lookupKeys.network, + lookupKeys.token, + lookupKeys.denomination ) - this.emit('sync', 'syncing') + const populatedOptions = await this._populateSyncOptions(options) + const cache = this.loadDepositCache('Deposits' + pathstring.toUpperCase(), populatedOptions) - for ( - let currentBlock = syncOptions.blocks.startBlock, - blockDelta = syncOptions.blocks.blockDelta, - targetBlock = syncOptions.blocks.targetBlock, - concurrencyLimit = syncOptions.cache.sync.concurrencyLimit; - currentBlock < targetBlock; - currentBlock += blockDelta - ) { - if (cache.sync.pooler!.pending < concurrencyLimit) { - const sum = currentBlock + blockDelta + await this.sync(instance.filters.Deposit(null, null, null), instance, cache, populatedOptions) - await AsyncUtils.timeout(syncOptions.msTimeout) - - if (currentBlock + blockDelta < targetBlock) { - await cache.sync.pooler!.pool(currentBlock, sum) - } else { - await cache.sync.pooler!.pool(currentBlock, sum - (sum % targetBlock)) - } - - this.emit('debug', currentBlock++, sum) - } else { - let res: Array = await cache.sync.pooler!.race() - - if (res.length != 0) - dbPromises.push( - cache.db.bulkDocs(res.map((el) => cache.buildDoc(el))).catch((err) => { - throw ErrorUtils.ensureError(err) - }) - ) - - currentBlock -= blockDelta - } - } - - this.emit('sync', 'synced') - - // Immediately start listening if we're doing this - if (syncOptions.cache.sync.listen) { - instance = instance.on(filter, (...eventArgs) => { - this.emit(cache.name, 'received', cache.db.put(cache.buildDoc(eventArgs[eventArgs.length - 1]))) - }) - } - - // Then wait for all pooler requests to resolve - let results = await cache.sync.pooler!.all() - - // Then transform them, we know the shape in forward - results = results.reduce((res: any[], response: any[]) => { - if (response[0]) response.forEach((el: any) => res.push(cache.buildDoc(el))) - return res - }, []) - - // Then wait for old dbPromises to resolve - await Promise.all(dbPromises) - - // Add the last docs - await cache.db.bulkDocs(results).catch((err) => { - throw ErrorUtils.ensureError(err) - }) - - // Finally, store the objects if (!this.instances.has(pathstring)) this.instances.set(pathstring, instance) if (!this.caches.has(cache.name)) this.caches.set(cache.name, cache) } - private async _populateSyncOpts( - lookupKeys: DataTypes.Keys.InstanceLookup, - syncOptions?: Options.Core.Sync - ): Promise> { - // Assign nonexistent - if (!syncOptions) syncOptions = {} - if (!syncOptions.blocks) syncOptions.blocks = {} - if (!syncOptions.cache) syncOptions.cache = { db: {}, sync: {} } - if (!syncOptions.cache.sync) syncOptions.cache.sync = {} - if (!syncOptions.cache.db) syncOptions.cache.db = {} + async syncWithdrawals(instance: TornadoInstance, options?: ChainOptions.Sync): Promise { + const lookupKeys = await Onchain.getInstanceLookupKeys(instance.address) + const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination - // Prepare options - - // deposit & withdraw - const both = syncOptions.deposit === undefined && syncOptions.withdrawal === undefined - syncOptions.deposit = syncOptions.deposit ?? both - syncOptions.withdrawal = syncOptions.withdrawal ?? false - - // blocks - syncOptions.blocks.startBlock = - syncOptions.blocks.startBlock ?? - (await Onchain.getInstanceDeployBlockNum(lookupKeys.network, lookupKeys.token, lookupKeys.denomination)) - - syncOptions.blocks.targetBlock = syncOptions.blocks.targetBlock ?? (await this.chain.latestBlockNum()) - - syncOptions.blocks.deltaDivisor = syncOptions.blocks.deltaDivisor ?? 100 - - syncOptions.blocks.blockDelta = this._getBlockDelta(syncOptions) - - syncOptions.msTimeout = syncOptions.msTimeout ?? 200 // 5 requests per second - - // cache - // db - syncOptions.cache.db.persistent = syncOptions.cache.db.persistent ?? true - syncOptions.cache.db.adapter = syncOptions.cache.db.adapter ?? 'leveldb' - - // sync - syncOptions.cache.sync.concurrencyLimit = syncOptions.cache.sync.concurrencyLimit ?? 8 - syncOptions.cache.sync.listen = syncOptions.cache.sync.listen ?? false - - return syncOptions as DeepRequired - } - - private _getBlockDelta(syncOptions?: Options.Core.Sync): number { - return Math.floor( - (syncOptions!.blocks!.targetBlock! - syncOptions!.blocks!.startBlock!) / - syncOptions!.blocks!.deltaDivisor! + options = options ?? {} + options.startBlock = await Onchain.getInstanceDeployBlockNum( + lookupKeys.network, + lookupKeys.token, + lookupKeys.denomination ) + + const populatedOptions = await this._populateSyncOptions(options) + const cache = this.loadWithdrawalCache('Withdrawals' + pathstring.toUpperCase(), populatedOptions) + + await this.sync(instance.filters.Withdrawal(null, null, null), instance, cache, populatedOptions) + + if (!this.instances.has(pathstring)) this.instances.set(pathstring, instance) + if (!this.caches.has(cache.name)) this.caches.set(cache.name, cache) } - /** - * @param instanceName The name of the instance as created in `_sync` function. - * @param commitments The commitments for which the leaf index values are to be noted down extra. - * @returns The result of concatenating the array of leaf indices found by matching them with the provided commitment values, followed by the array of all leaf indices, including all of the formerly mentioned values given that they are valid. Values which have not been matched, meaning probably invalid values, will be `0`. - */ - private async _findLeavesAndIndices( - instanceName: string, - commitments: Array - ): Promise<[Array, Array]> { - const indices = new Array(commitments.length).fill(0) - const leaves: Array = [] + protected async _populateSyncOptions(options: ChainOptions.Sync): Promise> { + if (!options.startBlock) throw ErrorUtils.getError('Core._populateSyncOptions: startBlock not set.') - const cache = this.loadCache>(instanceName) - const docs = await cache.db.allDocs() + options.targetBlock = options.targetBlock ?? (await this.chain.latestBlockNum()) - // If no docs in cache throw and stop - if (docs.total_rows === 0) { - await cache.clear() - throw ErrorUtils.getError( - `Core.buildMerkleTree: events for instance ${instanceName} have not been synchronized.` - ) - } + options.blockDivisor = options.blockDivisor ?? 40 - // Otherwise start looking for commitment leaf indices and also pick up all other leafs on the way - for (const row of docs.rows) { - const [, leafIndex, loadedCommitment] = parseIndexableString(row.id) - const index = commitments.findIndex((commitment) => commitment === loadedCommitment) + options.blockDelta = Math.floor((options.targetBlock - options.startBlock) / options.blockDivisor) - // If some commitment is found then add the leaf index and remove that commitment - if (index !== -1) { - indices[index] = leafIndex - commitments.splice(index, 1) - } + options.concurrencyLimit = options.concurrencyLimit ?? 8 - // In any case push every leaf - leaves.push(BigNumber.from(loadedCommitment).toString()) - } + options.msTimeout = options.msTimeout ?? 200 // 5 requests per second - // Concat matched and all leaf indices - return [leaves, indices] + options.persistentCache = options.persistentCache ?? true + + options.cacheAdapter = options.cacheAdapter ?? 'leveldb' + + options.listenForEvents = options.listenForEvents ?? false + + return options as DeepRequired } } diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 4f9d860..a281a88 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,5 +1,5 @@ // crypto types -import * as Types from 'types/sdk/crypto' +import { InputFor, OutputOf, ZKDepositData } from 'types/sdk/crypto' // External crypto import circomlib from 'circomlib' @@ -29,19 +29,20 @@ export namespace Setup { return Json.load('circuits/tornado.json') } + let cachedGroth16Prover: Groth16 | null = null + /** * @note The following is a comment from tornado-cli: `groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI`. They literally didn't check the code to see that these are just worker threads and that `groth16` has a `terminate()` function to remove them. 🤦 */ export async function getGroth16(): Promise { const defaultParams = { wasmInitialMemory: 5000 } - return await buildGroth16(defaultParams) + if (!cachedGroth16Prover) cachedGroth16Prover = await buildGroth16(defaultParams) + return cachedGroth16Prover } } export namespace Primitives { - export function calcPedersenHash( - pedersenHashData: Types.InputFor.PedersenHash - ): Types.OutputOf.PedersenHash { + export function calcPedersenHash(pedersenHashData: InputFor.PedersenHash): OutputOf.PedersenHash { return circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(pedersenHashData.msg))[0] } @@ -49,7 +50,7 @@ export namespace Primitives { return HexUtils.bufferToHex(msg, 62) } - export function parseNote(hexNote: string): Types.ZKDepositData { + export function parseNote(hexNote: string): ZKDepositData { const _hexNote = hexNote.split('_')[1] ?? hexNote const buffer = Buffer.from(_hexNote.slice(2), 'hex') return createDeposit({ @@ -61,11 +62,11 @@ export namespace Primitives { } export function createDeposit( - input: Types.InputFor.CreateDeposit = { + input: InputFor.CreateDeposit = { nullifier: NumberUtils.randomBigInteger(31), secret: NumberUtils.randomBigInteger(31) } - ): Types.ZKDepositData { + ): ZKDepositData { // @ts-expect-error let preimage = Buffer.concat([input.nullifier.leInt2Buff(31), input.secret.leInt2Buff(31)]) let commitment = calcPedersenHash({ msg: preimage }) @@ -84,14 +85,12 @@ export namespace Primitives { } } - export function buildMerkleTree(inputs: Types.InputFor.BuildMerkleTree): MerkleTree { + export function buildMerkleTree(inputs: InputFor.BuildMerkleTree): MerkleTree { // @ts-expect-error return new MerkleTreeDefault(inputs.height, inputs.leaves) } - export async function calcDepositProofs( - inputs: Array - ): Promise>> { + export async function calcDepositProofs(inputs: Array): Promise>> { const proofs: string[][] = [] const groth16 = await Setup.getGroth16() const circuit = await Setup.getTornadoCircuit() @@ -170,4 +169,4 @@ export namespace Primitives { // export function calcDepositProof(merkleProof: Crypto.InputFor.DepositProof): Crypto.OutputOf.DepositProof {} // Namespace exports -export { Types } +export { InputFor, OutputOf, ZKDepositData } diff --git a/src/lib/data.ts b/src/lib/data.ts index a674bdf..a3935ef 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -1,8 +1,6 @@ // Local types -import { TornadoInstance } from 'types/deth' -import * as Types from 'types/sdk/data' +import { ClassicInstance, TokenData, Keys, Options } from 'types/sdk/data' import { RelayerProperties } from 'types/sdk/data' -import { Options } from 'types/sdk/core' // Big modules import { BigNumber } from 'ethers' @@ -131,7 +129,7 @@ export namespace Onchain { network: string, token: string, denomination: string - ): Promise { + ): Promise { const instanceData = Json.getValue(await Json.load('onchain/instances.json'), [network, token]) return { network: +network, @@ -144,7 +142,7 @@ export namespace Onchain { } } - export async function getInstanceLookupKeys(instanceAddress: string): Promise { + export async function getInstanceLookupKeys(instanceAddress: string): Promise { // lookup some stuff first const lookupObj: { [key: string]: string } = await Json.load('onchain/instanceAddresses.json') @@ -233,7 +231,7 @@ export namespace Onchain { return Json.getValue(await Json.load('onchain/infrastructure.json'), [network, 'multicall3']) } - export async function getTokenData(network: string, token: string): Promise { + export async function getTokenData(network: string, token: string): Promise { const data = Json.getValue(await Json.load('onchain/tokens.json'), [network, token]) return { network: +network, @@ -401,7 +399,7 @@ export namespace Cache { name: string db: PouchDB.Database - constructor(name: string, options?: Options.Cache.Database) { + constructor(name: string, options?: Options.Cache) { this.name = name if (options?.persistent === false && options?.adapter !== 'memory' && options?.adapter !== null) @@ -432,11 +430,10 @@ export namespace Cache { } export abstract class Syncable extends Base { - sync: AsyncUtils.Sync + pooler?: AsyncUtils.PromisePooler - constructor(name: string, options?: { db?: Options.Cache.Database; sync?: Options.Cache.Sync }) { - super(name, options?.db) - this.sync = new AsyncUtils.Sync(options?.sync) + constructor(name: string, options?: Options.Cache) { + super(name, options) } abstract buildDoc(response: any): Docs.Base @@ -445,80 +442,28 @@ export namespace Cache { abstract getErrorHandlers(...args: Array): Array + initializePooler( + callbacks: Array, + errorHandlers: Array, + concurrencyLimit: number + ): void { + if (this.pooler) this.pooler.reset() + this.pooler = new AsyncUtils.PromisePooler(callbacks, errorHandlers, concurrencyLimit) + } + async close(): Promise { - if (this.sync.pooler!.pending) + if (this.pooler && this.pooler.pending) throw ErrorUtils.getError("Syncable.close: can't clear while pooler still has pending promises.") await super.close() } async clear(): Promise { - if (this.sync.pooler!.pending) + if (this.pooler && this.pooler.pending) throw ErrorUtils.getError("Syncable.clear: can't clear while pooler still has pending promises.") await super.clear() } } - function tornadoSyncErrorHandler( - err: Error, - numResolvedPromises: number, - callbackIndex: number, - orderIndex: number, - ...args: any[] - ): void { - err = ErrorUtils.ensureError(err) - - if (err.message.match('context deadline exceeded')) - console.error( - ErrorUtils.getError( - `Context deadline exceeded, stop if more promises do not resolve. Resolved: ${numResolvedPromises}` - ) - ) - else if (err.message.match('Invalid JSON RPC')) - console.error( - ErrorUtils.getError(`Endpoint returned invalid value (we might be rate limited), retrying.`) - ) - else { - err.message += `\nCallback args supplied: [${args.join(', ')}]\n` - throw err - } - } - - export class Deposit extends Syncable { - buildDoc(response: any): Docs.Deposit { - return new Docs.Deposit(response) - } - - getErrorHandlers(): Array { - return [tornadoSyncErrorHandler] - } - - getCallbacks(instance: TornadoInstance): Array { - return [ - (fromBlock: number, toBlock: number) => { - return instance.queryFilter(instance.filters.Deposit(null, null, null), fromBlock, toBlock) - } - ] - } - } - - export class Withdrawal extends Syncable { - buildDoc(response: any): Docs.Withdrawal { - return new Docs.Withdrawal(response) - } - - getErrorHandlers(): Array { - return [tornadoSyncErrorHandler] - } - - getCallbacks(instance: TornadoInstance): Array { - return [ - (fromBlock: number, toBlock: number) => { - return instance.queryFilter(instance.filters.Withdrawal(null, null, null, null), fromBlock, toBlock) - } - ] - } - } - type DocsArray = Array<{ doc?: T id: string @@ -552,4 +497,4 @@ export namespace Cache { } // Namespace exports -export { Types } +export { ClassicInstance, TokenData, Keys, Options } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 62ee4bd..2738ae3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -177,22 +177,6 @@ export namespace AsyncUtils { } } - export class Sync { - pooler?: PromisePooler - concurrencyLimit: number - listen: boolean - - constructor(options?: Options.Cache.Sync) { - this.concurrencyLimit = options?.concurrencyLimit ?? 1 - this.listen = options?.listen ?? false - } - - initializePooler(callbacks: Array, errorHandlers: Array): void { - if (this.pooler) this.pooler.reset() - this.pooler = new PromisePooler(callbacks, errorHandlers, this.concurrencyLimit) - } - } - export function timeout(msTimeout: number): Promise { return new Promise((resolve) => setTimeout(resolve, msTimeout)) } diff --git a/src/lib/web.ts b/src/lib/web.ts index e6e802f..645c9fd 100644 --- a/src/lib/web.ts +++ b/src/lib/web.ts @@ -1,5 +1,5 @@ // Types -import { Relayer as Types, RelayerOptions } from 'types/sdk/web' +import { Options, WithdrawalRequestResult } from 'types/sdk/web' import { RelayerProperties } from 'types/sdk/data' // HTTP and proxy @@ -95,7 +95,7 @@ export class Relayer { private _chainId?: number private _prices?: Map - constructor(options: Types.Options, properties?: RelayerProperties) { + constructor(options: Options.Relayer, properties?: RelayerProperties) { this.url = options.url this.httpClient = options.httpClient this._fetched = false @@ -217,10 +217,7 @@ export class Relayer { ) } - async handleWithdrawal( - instanceAddress: string, - proof: Array - ): Promise { + async handleWithdrawal(instanceAddress: string, proof: Array): Promise { const response = (await this.httpClient .post(this.url + '/v1/tornadoWithdraw', { contract: instanceAddress, @@ -231,7 +228,7 @@ export class Relayer { const { id } = response.data - let result: Types.WithdrawalRequestResult = { success: false }, + let result: WithdrawalRequestResult = { success: false }, finished = false while (!finished) { @@ -278,7 +275,7 @@ export class Relayer { /** * Construct a new Relayer by reading relayer data from cache. */ - static async fromCache(options: RelayerOptions): Promise { + static async fromCache(options: Options.Relayer): Promise { const cache = new Cache.Base('Relayers') // Error is ensured already diff --git a/src/test/core.test.ts b/src/test/core.test.ts index 0f8b9e8..2775e0c 100644 --- a/src/test/core.test.ts +++ b/src/test/core.test.ts @@ -1,5 +1,6 @@ import chai from 'chai' import * as ganache from 'ganache' +import { initializeRelayer } from './preload' // External import { solidity } from 'ethereum-waffle' @@ -9,17 +10,16 @@ import { parseUnits } from 'ethers/lib/utils' import { parseIndexableString } from 'pouchdb-collate' // Local -import { RelayerProperties } from 'types/sdk/data' import { ERC20, TornadoInstance } from 'types/deth' -import { Docs, Files, Onchain, Cache } from 'lib/data' +import { Files, Onchain } from 'lib/data' import { Chain, Contracts } from 'lib/chain' -import { Primitives } from 'lib/crypto' import { ErrorUtils } from 'lib/utils' -import { TorProvider, Relayer, RegularHttpClient } from 'lib/web' +import { TorProvider } from 'lib/web' import { Core } from 'lib/core' // Data import compareDeposits from './resources/deposits_eth_0.1.json' +import { Setup } from 'lib/crypto' chai.use(solidity) @@ -59,19 +59,6 @@ describe('Core', () => { const chain = new Chain(ganacheProvider) - async function initializeRelayer(): Promise { - const httpClient = new RegularHttpClient() - - const relayer = new Relayer({ - url: 'https://thornadope.xyz', - httpClient: httpClient - }) - - await relayer.fetchProperties() - - return relayer - } - after(async function () { this.timeout(0) await Files.wipeCache() @@ -112,17 +99,12 @@ describe('Core', () => { const smallEthDenomName = '1ETH0.1' - it(`sync: Should be able to fetch deposit events for ${smallEthDenomName}`, async function () { + it(`syncDeposits: Should be able to fetch deposit events for ${smallEthDenomName}`, async function () { // This is going to try syncing the entire range - await core.sync(smallestEth, { - blocks: { - deltaDivisor: 50 - }, - cache: { - sync: { - concurrencyLimit: 20 - } - } + await core.syncDeposits(smallestEth, { + blockDivisor: 50, + concurrencyLimit: 20, + msTimeout: 300 }) const cache = core.caches.get('Deposits' + smallEthDenomName) @@ -143,17 +125,12 @@ describe('Core', () => { const bigDaiDenomName = '1DAI100000' - it(`sync: Should be able to fetch deposit events for ${bigDaiDenomName}`, async function () { + it(`syncDeposits: Should be able to fetch deposit events for ${bigDaiDenomName}`, async function () { // This is going to try syncing the entire range - await core.sync(dai100K, { - blocks: { - deltaDivisor: 50 - }, - cache: { - sync: { - concurrencyLimit: 20 - } - } + await core.syncDeposits(dai100K, { + blockDivisor: 50, + concurrencyLimit: 20, + msTimeout: 300 }) }).timeout(0) }) @@ -200,7 +177,7 @@ describe('Core', () => { dai = dai.connect(daiWhaleSigner) }) - it.only('buildDepositTransaction: build a single eth deposit tx and succeed', async () => { + it('buildDepositTransaction: build a single eth deposit tx and succeed', async () => { const initBal = await needsMoney.getBalance() // Build tx and load cache for this test @@ -217,6 +194,8 @@ describe('Core', () => { ) }) + const listener = smallestEth.listeners(smallestEth.filters.Deposit(null, null, null))[0] + // Deposit and await cache updated const response = await needsMoney.sendTransaction(tx.request) await response.wait() @@ -225,12 +204,15 @@ describe('Core', () => { // Passing resolve as callback into put didn't work await await putPromise + // Turn off listener (NEEDED OR WE'RE NOT RESOLVING) + smallestEth.off(smallestEth.filters.Deposit(null, null, null), listener) + // Check deposit predicates expect(initBal).to.equal(parseUnits('1000')) expect(endBal).to.be.lte(parseUnits('999.9')) }).timeout(0) - it.only('buildDepositProof: it should be able to build an eth proof', async () => { + it('buildDepositProof: it should be able to build an eth proof', async () => { // Get withdrawer, load cache, prep note for this test const withdrawer = ganacheProvider.getSigner(2) const cache = core.loadDepositCache('Deposits1ETH0.1') @@ -240,7 +222,7 @@ describe('Core', () => { const notes = await core.loadNotes() // Build proof - let proof + let proof: any try { proof = await core.buildDepositProof( @@ -265,8 +247,8 @@ describe('Core', () => { // Withdrawal time, let's see if it works // The balance diff will be exact because withdrawer is paying for gas as relayer - await expect( - await smallestEth + await expect(() => + smallestEth .connect(withdrawer) .withdraw(proof[0], proof[1], proof[2], proof[3], proof[4], proof[5], proof[6]) ).to.changeEtherBalance(needsMoney, ethDelta) @@ -289,6 +271,8 @@ describe('Core', () => { ) }) + const listener = dai100K.listeners()[0] + // Prep for deposit await dai.transfer(needsMoneyAddress, depositAmount) dai = dai.connect(needsMoney) @@ -306,12 +290,17 @@ describe('Core', () => { // Passing resolve as callback into put didn't work await await putPromise + // Off (otherwise no resolve) + dai100K.off(dai100K.filters.Deposit(null, null, null), listener) + // Checks expect(daiBalBef).to.equal(daiBalPost.sub(depositAmount)) expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0) }).timeout(0) it.only('buildDepositProof: it should be able to build a token proof', async () => { + if (!process.env.TEST_RELAYER_DOMAIN) throw ErrorUtils.getError('core.test.ts: Need a relayer name') + // Get withdrawer, load cache, prep note for this test const withdrawer = ganacheProvider.getSigner(2) const cache = core.loadDepositCache('Deposits1DAI100000') @@ -324,7 +313,8 @@ describe('Core', () => { const note = notes[notes.length - 1] // Init properties via some relayer to make our life easier - const relayer = await initializeRelayer() + const relayer = await initializeRelayer(process.env.TEST_RELAYER_DOMAIN) + let properties = relayer.properties // Just set another address @@ -347,7 +337,7 @@ describe('Core', () => { const daiDelta = parseUnits('100000').sub(proof[5]) await expect( - await smallestEth + await dai100K .connect(withdrawer) .withdraw(proof[0], proof[1], proof[2], proof[3], proof[4], proof[5], proof[6]) ).to.changeTokenBalance(dai, needsMoney, daiDelta) diff --git a/src/test/preload.ts b/src/test/preload.ts index ca5ac26..d03fde5 100644 --- a/src/test/preload.ts +++ b/src/test/preload.ts @@ -1,2 +1,17 @@ +import { Relayer, RegularHttpClient } from 'lib/web' import * as dotenv from 'dotenv' dotenv.config() + +export async function initializeRelayer( + name: string, + httpClient = new RegularHttpClient() +): Promise { + const relayer = new Relayer({ + url: 'https://' + name, + httpClient: httpClient + }) + + await relayer.fetchProperties() + + return relayer +} diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts index bbd66fe..6f26a73 100644 --- a/src/test/utils.test.ts +++ b/src/test/utils.test.ts @@ -19,7 +19,7 @@ describe('utils', () => { }) it('pool(Many) & pending & totalAdded', async () => { - pooler = new AsyncUtils.PromisePooler([tp], 8) + pooler = new AsyncUtils.PromisePooler([tp], [], 8) pooler.pool(30) expect(pooler.pending).to.equal(1) @@ -37,7 +37,7 @@ describe('utils', () => { it('race: promises should be raced in proper order', async () => { let res = [] - pooler = new AsyncUtils.PromisePooler([tp, tp, tp, tp], 8) + pooler = new AsyncUtils.PromisePooler([tp, tp, tp, tp], [], 8) await pooler.pool(20) await pooler.poolMany([10], [20], [22], [30]) diff --git a/src/test/web.test.ts b/src/test/web.test.ts index f856c7f..e54c20a 100644 --- a/src/test/web.test.ts +++ b/src/test/web.test.ts @@ -1,24 +1,32 @@ import chai from 'chai' -import { TorHttpClient, TorProvider } from 'lib/web' +import { initializeRelayer } from './preload' + +import { TorHttpClient, RegularHttpClient, TorProvider, Relayer } from 'lib/web' // Waffle matchers import { solidity } from 'ethereum-waffle' import { ErrorUtils } from 'lib/utils' import { parseUnits } from 'ethers/lib/utils' +import { Web3Provider } from '@ethersproject/providers' chai.use(solidity) const expect = chai.expect describe('web', () => { + const torify = process.env.TORIFY === 'true' + if (!process.env.ETH_MAINNET_TEST_RPC || !process.env.TOR_PORT) throw ErrorUtils.getError('need a tor port and mainnet rpc endpoint.') - const torProvider = new TorProvider(process.env.ETH_MAINNET_TEST_RPC, { port: +process.env.TOR_PORT }) - const httpClient = new TorHttpClient({ port: +process.env.TOR_PORT }) + let torProvider: Web3Provider - if (process.env.TORIFY === 'true') + if (torify) torProvider = new TorProvider(process.env.ETH_MAINNET_TEST_RPC, { port: +process.env.TOR_PORT }) + + const httpClient = torify ? new TorHttpClient({ port: +process.env.TOR_PORT }) : new RegularHttpClient() + + if (torify) console.log( '\nSome Tor tips: Support non-profit exit node operators, host your own nodes, avoid spy nodes by configuring torrc.\n' ) @@ -29,6 +37,18 @@ describe('web', () => { throw err } + it('Relayer: should be able to initialize a Relayer and fetch properties', async () => { + if (!process.env.TEST_RELAYER_DOMAIN) + throw ErrorUtils.getError('web.test.ts: this test requires a relayer domain name.') + + const relayer = new Relayer({ + url: 'https://' + process.env.TEST_RELAYER_DOMAIN, + httpClient: httpClient + }) + + console.log(await relayer.fetchProperties()) + }).timeout(0) + it('httpClient: Should be able to send requests over Tor', async function () { try { const check = (await httpClient.get('https://check.torproject.org/api/ip')).data diff --git a/src/types/sdk/chain.ts b/src/types/sdk/chain.ts index c8295c7..b5d949e 100644 --- a/src/types/sdk/chain.ts +++ b/src/types/sdk/chain.ts @@ -1,5 +1,17 @@ import { TornadoInstance, TornadoProxy, ETHTornado, ERC20Tornado, ERC20 } from 'types/deth' -export namespace Contracts { - export type TornadoContracts = TornadoInstance | TornadoProxy | ETHTornado | ERC20Tornado | ERC20 +export type TornadoContracts = TornadoInstance | TornadoProxy | ETHTornado | ERC20Tornado | ERC20 + +export namespace Options { + export interface Sync { + startBlock?: number + targetBlock?: number + blockDelta?: number + blockDivisor?: number + concurrencyLimit?: number + msTimeout?: number + listenForEvents?: boolean + persistentCache?: true + cacheAdapter?: string + } } diff --git a/src/types/sdk/core.ts b/src/types/sdk/core.ts index 4c0f310..d64d7d7 100644 --- a/src/types/sdk/core.ts +++ b/src/types/sdk/core.ts @@ -9,36 +9,7 @@ import { TransactionRequest } from '@ethersproject/abstract-provider' import { BigNumber } from 'ethers' export namespace Options { - export namespace Cache { - export interface Database { - adapter?: string - persistent?: boolean - } - export interface Sync { - concurrencyLimit?: number - listen?: boolean - } - } - export namespace Core { - export interface Cache { - sync?: Cache.Sync - db?: Cache.Database - } - - export interface Sync { - deposit?: boolean - withdrawal?: boolean - msTimeout?: number - blocks?: { - startBlock?: number - targetBlock?: number - blockDelta?: number - deltaDivisor?: number - } - cache?: Cache - } - export interface Deposit { depositsPerInstance?: Array doNotPopulate?: boolean diff --git a/src/types/sdk/data.ts b/src/types/sdk/data.ts index 65f0e5a..c2888e0 100644 --- a/src/types/sdk/data.ts +++ b/src/types/sdk/data.ts @@ -1,24 +1,22 @@ import { BigNumber } from 'ethers' -export namespace Json { - export interface TornadoInstance { - network: number - symbol: string - decimals: number - denomination: number - deployBlock: number - address: string - } +export interface TornadoInstance { + network: number + symbol: string + decimals: number + denomination: number + deployBlock: number + address: string +} - export interface ClassicInstance extends TornadoInstance { - anonymityMiningEnabled: boolean - } +export interface ClassicInstance extends TornadoInstance { + anonymityMiningEnabled: boolean +} - export interface TokenData { - network: number - decimals: number - address: string - } +export interface TokenData { + network: number + decimals: number + address: string } export namespace Keys { @@ -38,3 +36,10 @@ export interface RelayerProperties { chainId: number prices: Map } + +export namespace Options { + export interface Cache { + adapter?: string + persistent?: boolean + } +} diff --git a/src/types/sdk/web.ts b/src/types/sdk/web.ts index 4e93390..7be6bdd 100644 --- a/src/types/sdk/web.ts +++ b/src/types/sdk/web.ts @@ -1,15 +1,15 @@ import { AxiosInstance } from 'axios' -export namespace Relayer { - export interface Options { +export namespace Options { + export interface Relayer { url: string httpClient: AxiosInstance } - - export interface WithdrawalRequestResult { - success: boolean - txHash?: string - } } -export type RelayerOptions = Relayer.Options +export interface WithdrawalRequestResult { + success: boolean + txHash?: string +} + +export type RelayerOptions = Options.Relayer