407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
|
// ts-essentials
|
||
|
import { DeepRequired } from 'ts-essentials'
|
||
|
|
||
|
// Local types
|
||
|
import { Relayer, Options, Transactions } from 'types/sdk/main'
|
||
|
import { TornadoInstance, TornadoProxy } from 'types/deth'
|
||
|
|
||
|
// Ethers
|
||
|
import { Signer } from '@ethersproject/abstract-signer'
|
||
|
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||
|
import { BigNumber, EventFilter, providers } from 'ethers'
|
||
|
|
||
|
// Important local
|
||
|
import { Docs, Cache, Types as DataTypes, Json } from 'lib/data'
|
||
|
import { Primitives } from 'lib/crypto'
|
||
|
import { Contracts } from 'lib/chain'
|
||
|
|
||
|
// Other local imports
|
||
|
import { OnchainData } from 'lib/data'
|
||
|
import { ErrorUtils } from 'lib/utils'
|
||
|
import { Chain } from 'lib/chain'
|
||
|
import { parseUnits } from 'ethers/lib/utils'
|
||
|
|
||
|
type BackupDepositDoc = {
|
||
|
pathstring: string
|
||
|
invoice?: string
|
||
|
note?: string
|
||
|
}
|
||
|
|
||
|
export class Core {
|
||
|
chain: Chain
|
||
|
caches: Map<string, Cache.Base<Docs.Base>>
|
||
|
instances: Map<string, TornadoInstance>
|
||
|
|
||
|
constructor(provider: providers.Provider, signer?: Signer) {
|
||
|
this.chain = new Chain(provider, signer)
|
||
|
this.caches = new Map<string, Cache.Base<Docs.Base>>()
|
||
|
this.instances = new Map<string, TornadoInstance>()
|
||
|
}
|
||
|
|
||
|
connect(signer: Signer): void {
|
||
|
this.chain.signer = signer
|
||
|
}
|
||
|
|
||
|
async getInstances(
|
||
|
keys: Array<{ token: string; denomination: number | string }>
|
||
|
): Promise<Array<TornadoInstance>> {
|
||
|
const chainId = await this.chain.getChainId()
|
||
|
return await Promise.all(
|
||
|
keys.map((key) =>
|
||
|
Contracts.getInstance(
|
||
|
String(chainId),
|
||
|
key.token,
|
||
|
String(key.denomination),
|
||
|
this.chain.signer ?? this.chain.provider
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
}
|
||
|
|
||
|
async getInstance(token: string, denomination: number | string): Promise<TornadoInstance> {
|
||
|
const chainId = await this.chain.getChainId()
|
||
|
return await Contracts.getInstance(
|
||
|
String(chainId),
|
||
|
token,
|
||
|
String(denomination),
|
||
|
this.chain.signer ?? this.chain.provider
|
||
|
)
|
||
|
}
|
||
|
|
||
|
async getProxy(): Promise<TornadoProxy> {
|
||
|
const chainId = await this.chain.getChainId()
|
||
|
return await Contracts.getProxy(String(chainId), this.chain.signer ?? this.chain.provider)
|
||
|
}
|
||
|
|
||
|
async buildWithdrawalTx(
|
||
|
instance: TornadoInstance,
|
||
|
withdrawOptions?: Options.Core.Withdrawal
|
||
|
): Promise<Transactions.Withdrawal> {
|
||
|
return (await this.buildWithdrawalTxs([instance], withdrawOptions))[0]
|
||
|
}
|
||
|
|
||
|
// TODO: lots of stuff
|
||
|
async buildWithdrawalTxs(
|
||
|
instances: Array<TornadoInstance>,
|
||
|
withdrawOptions?: Options.Core.Withdrawal
|
||
|
): Promise<Array<Transactions.Withdrawal>> {
|
||
|
for (let i = 0, nInstances = instances.length; i < nInstances; i++) {
|
||
|
const lookupKeys = await this.getInstanceLookupKeys(instances[i].address)
|
||
|
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
|
||
|
const db = new Cache.Base<Docs.Deposit>('Deposits' + pathstring.toUpperCase())
|
||
|
}
|
||
|
|
||
|
// Placeholder
|
||
|
return [{ request: {} }]
|
||
|
}
|
||
|
|
||
|
async depositInMultiple(
|
||
|
instances: Array<TornadoInstance>,
|
||
|
depositOptions?: Options.Core.Deposit
|
||
|
): Promise<Array<TransactionResponse>> {
|
||
|
if (!this.chain.signer)
|
||
|
throw ErrorUtils.getError('Core.depositInMultiple: need connected signer to deposit!')
|
||
|
const txs = await this.buildDepositTxs(instances, depositOptions)
|
||
|
return await Promise.all(txs.map((tx) => this.chain.signer!.sendTransaction(tx.request)))
|
||
|
}
|
||
|
|
||
|
async depositInSingle(
|
||
|
instance: TornadoInstance,
|
||
|
depositOptions?: Omit<Options.Core.Deposit, 'depositsPerInstance'>
|
||
|
): Promise<TransactionResponse> {
|
||
|
if (!this.chain.signer)
|
||
|
throw ErrorUtils.getError('Core.depositInMultiple: need connected signer to deposit!')
|
||
|
const tx = await this.buildDepositTx(instance, depositOptions)
|
||
|
return await this.chain.signer!.sendTransaction(tx.request)
|
||
|
}
|
||
|
|
||
|
async createInvoice(
|
||
|
instance: TornadoInstance,
|
||
|
invoiceOptions?: Omit<Options.Core.Invoice, 'depositsPerInstance'>
|
||
|
): Promise<Transactions.Invoice> {
|
||
|
let opts: Options.Core.Invoice = invoiceOptions ?? {}
|
||
|
opts.depositsPerInstance = [1]
|
||
|
return (await this.createInvoices([instance], invoiceOptions))[0]
|
||
|
}
|
||
|
|
||
|
async createInvoices(
|
||
|
instances: Array<TornadoInstance>,
|
||
|
invoiceOptions?: Options.Core.Invoice
|
||
|
): Promise<Array<Transactions.Invoice>> {
|
||
|
if (!invoiceOptions) invoiceOptions = {}
|
||
|
if (!invoiceOptions.backup) invoiceOptions.backup = {}
|
||
|
invoiceOptions.backup.invoices = invoiceOptions.backup.invoices ?? true
|
||
|
invoiceOptions.backup.notes = invoiceOptions.backup.notes ?? true
|
||
|
invoiceOptions.doNotPopulate = invoiceOptions.doNotPopulate ?? true
|
||
|
return await this.buildDepositTxs(instances, invoiceOptions)
|
||
|
}
|
||
|
|
||
|
async buildDepositTx(
|
||
|
instance: TornadoInstance,
|
||
|
depositOptions?: Options.Core.Deposit
|
||
|
): Promise<Transactions.Deposit> {
|
||
|
let opts: Options.Core.Deposit = depositOptions ?? {}
|
||
|
opts.depositsPerInstance = [1]
|
||
|
return (await this.buildDepositTxs([instance], opts))[0]
|
||
|
}
|
||
|
|
||
|
async buildDepositTxs(
|
||
|
instances: Array<TornadoInstance>,
|
||
|
depositOptions?: Options.Core.Deposit
|
||
|
): Promise<Array<Transactions.Deposit>> {
|
||
|
const depositsPerInstance =
|
||
|
depositOptions?.depositsPerInstance ?? new Array<number>(instances.length).fill(1)
|
||
|
|
||
|
const doNotPopulate = depositOptions?.doNotPopulate ?? false
|
||
|
const backupNotes = depositOptions?.backup?.notes ?? true
|
||
|
const backupInvoices = depositOptions?.backup?.invoices ?? false
|
||
|
|
||
|
if (depositsPerInstance.length != instances.length)
|
||
|
throw ErrorUtils.getError(
|
||
|
'Core.buildDepositTx: number of deposit amount elements must equal the number of instances!'
|
||
|
)
|
||
|
|
||
|
const notesToBackup: Array<BackupDepositDoc> = []
|
||
|
const invoicesToBackup: Array<BackupDepositDoc> = []
|
||
|
const txs: Array<Transactions.Deposit> = []
|
||
|
const chainId = await this.chain.getChainId()
|
||
|
|
||
|
const proxy: TornadoProxy = await Contracts.getProxy(String(chainId), this.chain.provider)
|
||
|
|
||
|
for (let i = 0, nInstances = instances.length; i < nInstances; i++) {
|
||
|
const lookupKeys = await this.getInstanceLookupKeys(instances[i].address)
|
||
|
const pathstring = lookupKeys.network + lookupKeys.token + lookupKeys.denomination
|
||
|
|
||
|
for (let d = 0, nDeposits = depositsPerInstance[i]; d < nDeposits; d++) {
|
||
|
const deposit = Primitives.createDeposit()
|
||
|
const note = Primitives.createNote(deposit.preimage)
|
||
|
|
||
|
if (backupNotes) notesToBackup.push({ pathstring: pathstring, note: note })
|
||
|
|
||
|
if (backupInvoices) invoicesToBackup.push({ pathstring: pathstring, invoice: deposit.commitmentHex })
|
||
|
|
||
|
if (!doNotPopulate) {
|
||
|
txs.push({
|
||
|
request: {
|
||
|
to: proxy.address,
|
||
|
data: proxy.interface.encodeFunctionData('deposit', [
|
||
|
instances[i].address,
|
||
|
deposit.commitmentHex,
|
||
|
[]
|
||
|
]),
|
||
|
value: lookupKeys.token == 'eth' ? parseUnits(lookupKeys.denomination) : BigNumber.from(0)
|
||
|
},
|
||
|
note: pathstring + '_' + note,
|
||
|
invoice: pathstring + '_' + deposit.commitmentHex
|
||
|
})
|
||
|
} else
|
||
|
txs.push({
|
||
|
request: {},
|
||
|
note: pathstring + '_' + note,
|
||
|
invoice: pathstring + '_' + deposit.commitmentHex
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (backupNotes) await this._backupDepositData(new Cache.Base<Docs.Note>('DepositNotes'), notesToBackup)
|
||
|
|
||
|
if (backupInvoices)
|
||
|
await this._backupDepositData(new Cache.Base<Docs.Invoice>('DepositInvoices'), invoicesToBackup)
|
||
|
|
||
|
return txs
|
||
|
}
|
||
|
|
||
|
private async _backupDepositData<T extends Docs.Note | Docs.Invoice>(
|
||
|
cache: Cache.Base<T>,
|
||
|
backupData: Array<BackupDepositDoc>
|
||
|
): Promise<void> {
|
||
|
let id = +(await cache.db.info()).update_seq
|
||
|
|
||
|
await cache.db
|
||
|
.bulkDocs(
|
||
|
backupData.map((entry) => {
|
||
|
if (entry.note) return new Docs.Note(++id, entry.pathstring, entry.note)
|
||
|
else if (entry.invoice) return new Docs.Invoice(++id, entry.pathstring, entry.invoice)
|
||
|
}) as Array<T>
|
||
|
)
|
||
|
.catch((err) => {
|
||
|
throw ErrorUtils.ensureError(err)
|
||
|
})
|
||
|
|
||
|
await cache.close().catch((err) => {
|
||
|
throw ErrorUtils.ensureError(err)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
async syncMultiple(instances: Array<TornadoInstance>, syncOptions?: Options.Core.Sync): Promise<void> {
|
||
|
for (const instance of instances) {
|
||
|
await this.sync(instance, syncOptions)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async sync(instance: TornadoInstance, syncOptions?: Options.Core.Sync): Promise<void> {
|
||
|
// Get some data
|
||
|
const lookupKeys = await this.getInstanceLookupKeys(instance.address)
|
||
|
|
||
|
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++) {
|
||
|
let action = actions[i][0].charAt(0).toUpperCase() + actions[i][0].slice(1)
|
||
|
await this._sync(action, lookupKeys, instance, populatedSyncOpts)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async _sync(
|
||
|
action: string,
|
||
|
lookupKeys: DataTypes.Keys.InstanceLookup,
|
||
|
instance: TornadoInstance,
|
||
|
syncOptions: DeepRequired<Options.Core.Sync>
|
||
|
): Promise<void> {
|
||
|
const name = `${action + 's'}${lookupKeys.network}${lookupKeys.token.toUpperCase()}${
|
||
|
lookupKeys.denomination
|
||
|
}`,
|
||
|
pathstring = name.substring(action.length).toLowerCase()
|
||
|
|
||
|
let cache: Cache.Syncable<Docs.Base>, toDoc: (_: any) => Docs.Base, filter: EventFilter
|
||
|
|
||
|
if (action == 'Deposit') {
|
||
|
toDoc = (resp: any) => new Docs.Deposit(resp)
|
||
|
cache = new Cache.Deposit(name, syncOptions.cache)
|
||
|
filter = instance.filters.Deposit(null, null, null)
|
||
|
} else {
|
||
|
toDoc = (resp: any) => new Docs.Withdrawal(resp)
|
||
|
cache = new Cache.Withdrawal(name, syncOptions.cache)
|
||
|
filter = instance.filters.Withdrawal(null, null, null, null)
|
||
|
}
|
||
|
|
||
|
// Assign pooler
|
||
|
cache.sync.pooler = await cache.sync.initializePooler(cache.getCallbacks(instance))
|
||
|
|
||
|
// Start synchronizing
|
||
|
let dbPromises = []
|
||
|
|
||
|
for (
|
||
|
let currentBlock = syncOptions.blocks.startBlock,
|
||
|
blockDelta = syncOptions.blocks.blockDelta,
|
||
|
targetBlock = syncOptions.blocks.targetBlock,
|
||
|
concurrencyLimit = syncOptions.cache.sync.concurrencyLimit;
|
||
|
currentBlock < targetBlock + blockDelta;
|
||
|
currentBlock += blockDelta
|
||
|
) {
|
||
|
if (cache.sync.pooler.pending < concurrencyLimit) {
|
||
|
if (currentBlock < targetBlock) {
|
||
|
await cache.sync.pooler.pool(currentBlock, currentBlock + blockDelta)
|
||
|
} else {
|
||
|
let sum = currentBlock + blockDelta
|
||
|
await cache.sync.pooler.pool(currentBlock, sum - (sum % targetBlock))
|
||
|
}
|
||
|
} else {
|
||
|
let res: Array<any> = await cache.sync.pooler.race()
|
||
|
if (res.length != 0)
|
||
|
dbPromises.push(
|
||
|
cache.db.bulkDocs(res.map((el) => toDoc(el))).catch((err) => {
|
||
|
throw ErrorUtils.ensureError(err)
|
||
|
})
|
||
|
)
|
||
|
currentBlock -= blockDelta
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Immediately start listening if we're doing this
|
||
|
if (syncOptions.cache.sync.listen)
|
||
|
instance = instance.on(filter, (...eventArgs) => {
|
||
|
cache.db.put(toDoc(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(toDoc(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(name)) this.caches.set(name, cache)
|
||
|
}
|
||
|
|
||
|
private async _populateSyncOpts(
|
||
|
lookupKeys: DataTypes.Keys.InstanceLookup,
|
||
|
syncOptions?: Options.Core.Sync
|
||
|
): Promise<DeepRequired<Options.Core.Sync>> {
|
||
|
// 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 = {}
|
||
|
|
||
|
// 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 OnchainData.getInstanceDeployBlockNum(
|
||
|
lookupKeys.network,
|
||
|
lookupKeys.token,
|
||
|
lookupKeys.denomination
|
||
|
))
|
||
|
|
||
|
syncOptions.blocks.targetBlock = syncOptions.blocks.targetBlock ?? (await this.chain.latestBlockNum())
|
||
|
|
||
|
syncOptions.blocks.blockDelta =
|
||
|
syncOptions.blocks.blockDelta ?? (syncOptions.blocks.targetBlock - syncOptions.blocks.startBlock) / 20
|
||
|
|
||
|
// 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<Options.Core.Sync>
|
||
|
}
|
||
|
|
||
|
async getInstanceLookupKeys(instanceAddress: string): Promise<DataTypes.Keys.InstanceLookup> {
|
||
|
// lookup some stuff first
|
||
|
const lookupObj: { [key: string]: string } = Json.getValue(await Json.load('onchain/quickLookup.json'), [
|
||
|
'instanceAddresses'
|
||
|
])
|
||
|
|
||
|
const pathstring: string = Object.entries(lookupObj).find((el) => el[1] === instanceAddress)![0]
|
||
|
|
||
|
// I like JS/TS
|
||
|
const network = pathstring.match('[0-9]+')![0],
|
||
|
token = pathstring.substring(network.length).match('[a-z]+')![0],
|
||
|
denomination = pathstring.substring(network.length + token.length)
|
||
|
|
||
|
return {
|
||
|
network: network,
|
||
|
token: token,
|
||
|
denomination: denomination
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { Relayer, Transactions, Options }
|