sdk-monorepo/src/lib/main.ts

407 lines
14 KiB
TypeScript
Raw Normal View History

// 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 }