828 lines
30 KiB
TypeScript
828 lines
30 KiB
TypeScript
// ts-essentials
|
|
import { DeepRequired, MarkOptional, Merge } from 'ts-essentials'
|
|
|
|
// Local types
|
|
import { TornadoInstance, TornadoRouter } from './deth'
|
|
|
|
// Monorepo
|
|
import { RelayerProperties as RelayerDataProperties } from '@tornado/sdk-data'
|
|
import { DepositInfo, InputFor } from '@tornado/sdk-crypto'
|
|
|
|
// External imports
|
|
import { TransactionRequest } from '@ethersproject/abstract-provider'
|
|
import { BigNumber, EventFilter, providers } from 'ethers'
|
|
import { parseUnits } from 'ethers/lib/utils'
|
|
import { bigInt } from 'snarkjs'
|
|
|
|
// @ts-ignore
|
|
import { parseIndexableString } from 'pouchdb-collate'
|
|
|
|
// Local imports
|
|
import { Primitives } from '@tornado/sdk-crypto'
|
|
import { ErrorUtils, ObjectUtils, AsyncUtils } from '@tornado/sdk-utils'
|
|
import { Docs, Cache, Keys, Constants, Onchain, Options as DataOptions } from '@tornado/sdk-data'
|
|
import { Contracts, Chain, Synchronizer, Options as ChainOptions, syncErrorHandler } from '@tornado/sdk-chain'
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECLARATIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
export namespace Options {
|
|
export namespace Core {
|
|
export interface Deposit {
|
|
depositsPerInstance?: Array<number>
|
|
callInstanceDirectly?: boolean
|
|
doNotPopulate?: boolean
|
|
}
|
|
|
|
export type Invoice = Deposit
|
|
|
|
export interface BuildDepositProof {
|
|
gasPrice?: BigNumber
|
|
gasPriceCushion?: BigNumber
|
|
tokenDecimals?: number
|
|
ethPurchaseAmounts?: Array<BigNumber>
|
|
checkNotesSpent?: boolean
|
|
checkKnownRoot?: boolean
|
|
merkleTreeHeight?: number
|
|
}
|
|
}
|
|
|
|
export type Sync = ChainOptions.Sync
|
|
export type Cache = DataOptions.Cache
|
|
}
|
|
|
|
export namespace Transactions {
|
|
export interface Deposit {
|
|
request: TransactionRequest
|
|
invoice?: string
|
|
note?: string
|
|
}
|
|
export type Invoice = Deposit
|
|
}
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FOR SYNCHRONIZATION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
export class DepositCache extends Cache.Syncable<Docs.Deposit> {
|
|
buildDoc(response: any): Docs.Deposit {
|
|
return new Docs.Deposit(response)
|
|
}
|
|
|
|
getErrorHandlers(): Array<AsyncUtils.ErrorHandler> {
|
|
return [syncErrorHandler]
|
|
}
|
|
|
|
getCallbacks(instance: TornadoInstance): Array<AsyncUtils.Callback> {
|
|
return [
|
|
(fromBlock: number, toBlock: number) => {
|
|
return instance.queryFilter(instance.filters.Deposit(null, null, null), fromBlock, toBlock)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
export class WithdrawalCache extends Cache.Syncable<Docs.Withdrawal> {
|
|
buildDoc(response: any): Docs.Withdrawal {
|
|
return new Docs.Withdrawal(response)
|
|
}
|
|
|
|
getErrorHandlers(): Array<AsyncUtils.ErrorHandler> {
|
|
return [syncErrorHandler]
|
|
}
|
|
|
|
getCallbacks(instance: TornadoInstance): Array<AsyncUtils.Callback> {
|
|
return [
|
|
(fromBlock: number, toBlock: number) => {
|
|
return instance.queryFilter(instance.filters.Withdrawal(null, null, null, null), fromBlock, toBlock)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CORE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
type Provider = providers.Provider
|
|
|
|
type RelayerProperties = MarkOptional<
|
|
Pick<RelayerDataProperties, 'address' | 'serviceFeePercent' | 'prices'>,
|
|
'serviceFeePercent' | 'prices'
|
|
>
|
|
|
|
export class Core extends Synchronizer {
|
|
private _mutex: AsyncUtils.SimpleMutex
|
|
private _chain?: Chain
|
|
|
|
private _notes?: Cache.Base<Docs.Note>
|
|
private _invoices?: Cache.Base<Docs.Invoice>
|
|
|
|
get chain(): Chain {
|
|
this._checkProvider('chain')
|
|
return this._chain!
|
|
}
|
|
|
|
constructor() {
|
|
super()
|
|
this._mutex = new AsyncUtils.SimpleMutex()
|
|
}
|
|
|
|
private _checkProvider(parentCallName: string): void {
|
|
if (!this._chain)
|
|
throw ErrorUtils.getError('Core.' + parentCallName + ': you must first connect a provider!')
|
|
}
|
|
|
|
async connect(provider: Provider): Promise<void> {
|
|
if (!this._chain) this._chain = new Chain(provider)
|
|
else this._chain.provider = provider
|
|
await this._chain.fetchChainData()
|
|
}
|
|
|
|
getInstances(keys: Array<{ token: string; denomination: number | string }>): Array<TornadoInstance> {
|
|
this._checkProvider('getInstances')
|
|
return keys.map((key) =>
|
|
Contracts.getInstance(String(this.chain.id), key.token, String(key.denomination), this.chain.provider)
|
|
)
|
|
}
|
|
|
|
getInstance(token: string, denomination: number | string): TornadoInstance {
|
|
this._checkProvider('getInstance')
|
|
return this.loadInstance(this.chain.id, token, denomination)
|
|
}
|
|
|
|
getRouter(): TornadoRouter {
|
|
this._checkProvider('getRouter')
|
|
return Contracts.getRouter(String(this.chain.id), this.chain.provider)
|
|
}
|
|
|
|
async createDepositProof(
|
|
instance: TornadoInstance,
|
|
relayerProperties: RelayerProperties,
|
|
recipientAddress: string,
|
|
depositInfo: DepositInfo,
|
|
options?: Options.Core.BuildDepositProof
|
|
): Promise<Array<string>> {
|
|
return (
|
|
await this.createDepositProofs(instance, relayerProperties, [recipientAddress], [depositInfo], options)
|
|
)[0]
|
|
}
|
|
|
|
/**
|
|
* @param instance This is the Tornado Instance which will be withdrawn from.
|
|
* @param relayerProperties The properties of the relayer that is going to be used for the withdrawals. If the service fee is 0, it is assumed that there is no relayer, but that a manual wallet withdraw is being made. These properties are included in the ZK proof.
|
|
* @param recipientAddresses The recipient addresses which should receive the withdrawals, in order.
|
|
* @param depositInfo These represent the public and private values, reconstructed from the deposit note, generated during the building of deposit transactions, used for building the proof of knowledge statement for withdrawal, for each withdrawal (in this context).
|
|
* @param options Numerous options which most importantly allow a user to specify whether he is buying ETH, whether to check proof data validity and finally to modulate the gas prices which will be used to calculate the gas fees paid to the relayer.
|
|
* @returns The proofs for which the user should then decide whether to use a relayer (recommended, but decide carefully which one) or use his own wallet (if needed).
|
|
*/
|
|
async createDepositProofs(
|
|
instance: TornadoInstance,
|
|
relayerProperties: RelayerProperties,
|
|
recipientAddresses: Array<string>,
|
|
depositInfo: Array<DepositInfo>,
|
|
options?: Options.Core.BuildDepositProof
|
|
): Promise<Array<Array<string>>> {
|
|
this._checkProvider('createDepositProofs')
|
|
|
|
/* ~~~~~~~~~~~~~~~~~~~~~~ EXTRACT NOTES, COMMITMENTS & BASIC CHECKS ~~~~~~~~~~~~~~~~~~~~~~ */
|
|
|
|
const hexCommitments: string[] = []
|
|
|
|
const hexNullifierHashes: string[] = []
|
|
|
|
const purchaseAmounts =
|
|
options?.ethPurchaseAmounts ?? new Array(depositInfo.length).fill(BigNumber.from(0))
|
|
|
|
// Inputs must equal in length
|
|
|
|
if (depositInfo.length !== recipientAddresses.length)
|
|
throw ErrorUtils.getError(
|
|
'Core.createDepositProofs: the number of recipients must equal the length of depositInfo.'
|
|
)
|
|
|
|
if (depositInfo.length !== purchaseAmounts.length)
|
|
throw ErrorUtils.getError(
|
|
'Core.createDepositProofs: if purchase amounts is specified, it must equal the length of depositInfo.'
|
|
)
|
|
|
|
// Extract all notes and commitments from the deposit info
|
|
|
|
depositInfo.forEach((deposit) => {
|
|
hexCommitments.push(deposit.hexCommitment)
|
|
hexNullifierHashes.push(deposit.hexNullifierHash)
|
|
})
|
|
|
|
/* ~~~~~~~~~~~~~~~~~~~~~~ FETCH WHETHER SOME OR ALL NOTES HAVE BEEN SPENT ~~~~~~~~~~~~~~~~~~~~~~ */
|
|
|
|
// Determine whether we will be checking whether notes are spent
|
|
const checkSpent = options?.checkNotesSpent !== false
|
|
|
|
this.emit('debug', `\nCheck spent notes? => ${checkSpent}`)
|
|
|
|
// If yes, immediately check it with the supplied Tornado Instance
|
|
const checkSpentArray = checkSpent ? await instance.isSpentArray(hexNullifierHashes) : undefined
|
|
|
|
if (checkSpent) this.emit('debug', `\nSpent array: [${checkSpentArray?.join(', ')}]`)
|
|
|
|
/* ~~~~~~~~~~~~~~~~~~~~~~ COLLECT LEAVES, ALL COMMITMENTS MUST HAVE A LEAF ~~~~~~~~~~~~~~~~~~~~~~ */
|
|
|
|
// Determine cache name
|
|
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
|
|
const name = 'Deposits' + (network + token + denomination).toUpperCase()
|
|
|
|
// Find all leaves & indices by reading from cache
|
|
const [leaves, leafIndices] = await this._findLeavesAndIndices(name, hexCommitments)
|
|
|
|
this.emit(
|
|
'debug',
|
|
`\nFound leaves and indices, num leaves: ${leaves.length}, indices: [${leafIndices.join(', ')}]`
|
|
)
|
|
|
|
/* ~~~~~~~~~~~~~~~~~~~~~~ VALIDATE NOTES AND COMMITMENT ~~~~~~~~~~~~~~~~~~~~~~ */
|
|
|
|
// Check whether a commitment has not been found in all deposits, meaning that it is invalid
|
|
// Also add the invalid commitments. We can do leafIndices[i] because the matched one are concatenated
|
|
// at the start
|
|
|
|
const spentNotes: string[] = []
|
|
const invalidCommitments: string[] = []
|
|
|
|
for (let i = 0, len = depositInfo.length; i < len; i++) {
|
|
if (!leafIndices[i]) invalidCommitments.push(hexCommitments[i])
|
|
if (checkSpent && checkSpentArray![i]) spentNotes.push(hexNullifierHashes[i])
|
|
}
|
|
|
|
// If something is wrong, throw
|
|
const commitmentsAreInvalid = invalidCommitments.length !== 0
|
|
const notesAreSpent = spentNotes.length !== 0
|
|
|
|
if (commitmentsAreInvalid || notesAreSpent)
|
|
throw ErrorUtils.getError(
|
|
`Core.createDepositProofs: ` +
|
|
(commitmentsAreInvalid
|
|
? `following commitments are invalid:\n\n${invalidCommitments.join('\n')}\n\n`
|
|
: '') +
|
|
(notesAreSpent
|
|
? `${
|
|
commitmentsAreInvalid ? 'and ' : ''
|
|
}following notes are already spent or invalid:\n\n${spentNotes.join('\n')}\n\n`
|
|
: '')
|
|
)
|
|
|
|
/* ~~~~~~~~~~~~~~~~~~~~~~ BUILD MERKLE TREE & TEST VALIDITY ~~~~~~~~~~~~~~~~~~~~~~ */
|
|
|
|
// Otherwise, build the merkle tree from the leaves
|
|
const merkleTree = Primitives.buildMerkleTree({
|
|
height: options?.merkleTreeHeight ?? Constants.MERKLE_TREE_HEIGHT,
|
|
leaves: leaves
|
|
})
|
|
|
|
const root: string = BigNumber.from(merkleTree.root).toHexString()
|
|
const checkKnownRoot: boolean = options?.checkKnownRoot ?? true
|
|
|
|
this.emit('debug', `\nMerkle root: ${root}, check known? => ${checkKnownRoot}`)
|
|
|
|
// Check whether the root is valid
|
|
if (checkKnownRoot && !(await instance.isKnownRoot(root)))
|
|
throw ErrorUtils.getError(
|
|
'Core.createDepositProofs: the merkle tree created is not valid, something went wrong with syncing.'
|
|
)
|
|
|
|
/* ~~~~~~~~~~~~~~~~~~~~~~ PREPARE DATA FOR PROOF & BUILD PROOF ~~~~~~~~~~~~~~~~~~~~~~ */
|
|
|
|
// Rest of note invariant arguments
|
|
const inputsForProofs: InputFor.DepositProof[] = []
|
|
const gasPrice = options?.gasPrice ?? (await this.chain.getGasPrice())
|
|
const gasPriceCushion = options?.gasPrice ?? gasPrice.mul(10).div(100)
|
|
|
|
// In reality, if a manual withdraw is made, we don't differentiate it from a relayer withdraw
|
|
// Since it is only serviceFee 0 AND without a token price, the function will not buy more tokens
|
|
const serviceFeePercent = relayerProperties.serviceFeePercent ?? 0
|
|
const tokenPrice = relayerProperties.prices?.get(token)
|
|
|
|
const decimals = BigNumber.from(10).pow(
|
|
options?.tokenDecimals ?? (await Onchain.getTokenDecimals(network, token))
|
|
)
|
|
|
|
const toWithdraw = BigNumber.from(+denomination * 10 ** denomination.length)
|
|
.mul(decimals)
|
|
.div(10 ** denomination.length)
|
|
|
|
const native = token == this.chain.symbol
|
|
|
|
if (!tokenPrice && !native)
|
|
throw ErrorUtils.getError(
|
|
'Core.createDepositProofs: a token price MUST be supplied if the token withdrawn is not native.'
|
|
)
|
|
|
|
this.emit(
|
|
'debug',
|
|
`\nProof building, invariant data: [${[
|
|
gasPrice.toString(),
|
|
gasPriceCushion.toString(),
|
|
serviceFeePercent,
|
|
tokenPrice,
|
|
decimals.toString(),
|
|
toWithdraw.toString()
|
|
].join(', ')}]\n`
|
|
)
|
|
|
|
// Collect all data for proofs
|
|
for (let i = 0, len = depositInfo.length; i < len; i++) {
|
|
inputsForProofs.push({
|
|
public: {
|
|
root: root,
|
|
tree: merkleTree,
|
|
leafIndex: leafIndices[i],
|
|
hexNullifierHash: depositInfo[i].hexNullifierHash,
|
|
recipientAddress: recipientAddresses[i],
|
|
relayerAddress: relayerProperties.address,
|
|
fee: this._calcWithdrawalFee(
|
|
toWithdraw,
|
|
decimals,
|
|
gasPrice,
|
|
gasPriceCushion,
|
|
serviceFeePercent,
|
|
purchaseAmounts[i],
|
|
// This is our flag whether it's a token or not
|
|
native ? undefined : tokenPrice
|
|
),
|
|
// @ts-expect-error
|
|
refund: purchaseAmounts[i] ? bigInt(purchaseAmounts[i].toString()) : bigInt(0)
|
|
},
|
|
private: {
|
|
nullifier: depositInfo[i].nullifier,
|
|
secret: depositInfo[i].secret
|
|
}
|
|
})
|
|
}
|
|
|
|
// Compute proofs and return
|
|
return await Primitives.calcDepositProofs(inputsForProofs)
|
|
}
|
|
|
|
private _calcWithdrawalFee(
|
|
toWithdraw: BigNumber,
|
|
decimals: BigNumber,
|
|
gasPrice: BigNumber,
|
|
gasPriceCushion: BigNumber,
|
|
relayerServiceFee: number,
|
|
ethBought: BigNumber,
|
|
tokenPriceInEth?: BigNumber
|
|
): typeof bigInt {
|
|
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
|
|
return bigInt(txCost.add(ethBought).mul(decimals).div(tokenPriceInEth).add(baseRelayerFee).toString())
|
|
}
|
|
// @ts-expect-error
|
|
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<string>
|
|
): Promise<[Array<string>, Array<number>]> {
|
|
const indices = new Array<number>(commitments.length).fill(0)
|
|
const leaves: Array<string> = []
|
|
|
|
const cache = this.loadCache<Docs.Deposit>(instanceName)
|
|
|
|
// Shallow copy so we can find indexes again for commitments
|
|
const commitmentsCopy = [...commitments]
|
|
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) {
|
|
let index = -1
|
|
|
|
const [, leafIndex, loadedCommitment] = parseIndexableString(row.id)
|
|
|
|
// Search only if there is some left
|
|
if (commitments.length !== 0)
|
|
index = commitments.findIndex((commitment) => commitment === loadedCommitment)
|
|
|
|
// If some commitment is found then add the leaf index
|
|
if (index !== -1) {
|
|
// Add it there where we intended for it to be originally
|
|
indices[commitmentsCopy.findIndex((commitment) => commitment === loadedCommitment)] = leafIndex
|
|
commitments.splice(index, 1)
|
|
this.emit(
|
|
'debug',
|
|
`\nMatched commitment ${loadedCommitment} @ leaf index ${leafIndex}, leftover commitments:\n\n${commitments.join(
|
|
'\n'
|
|
)}\n`
|
|
)
|
|
}
|
|
|
|
// 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<number>,
|
|
keys?: Partial<Keys.InstanceLookup>,
|
|
options?: Options.Cache
|
|
): Promise<Array<DepositInfo>> {
|
|
const cache = this._notes ?? new Cache.Base<Docs.Note>('DepositNotes', options)
|
|
|
|
const rows = await cache.getRows()
|
|
|
|
let docs: Array<Docs.Note | undefined> = []
|
|
let notes: Array<string> = []
|
|
|
|
if (indexes)
|
|
for (let i = 0, len = rows.length; i < len; i++) {
|
|
docs.push(rows[indexes[i]].doc)
|
|
}
|
|
else docs = rows.map((row) => row.doc)
|
|
|
|
if (keys)
|
|
docs.forEach((doc) => {
|
|
const idNetworkMatches = doc && (keys.network ? keys.network === doc?.network : true)
|
|
const andTokenSymbolMatches = idNetworkMatches && (keys.token ? keys.token === doc?.token : true)
|
|
const lastlyDenominationMatches =
|
|
andTokenSymbolMatches && (keys.denomination ? keys.denomination === doc?.denomination : true)
|
|
if (lastlyDenominationMatches && doc?.note) notes.push(doc.note)
|
|
})
|
|
else notes = docs.filter((doc) => ObjectUtils.exists(doc?.note)).map((doc) => doc!.note)
|
|
|
|
return this.parseNotes(notes)
|
|
}
|
|
|
|
parseNotes(notes: Array<string>): Array<DepositInfo> {
|
|
return notes.map((note) => Primitives.parseNote(note))
|
|
}
|
|
|
|
parseNote(note: string): DepositInfo {
|
|
return this.parseNotes([note])[0]
|
|
}
|
|
|
|
clearListener(
|
|
instance: TornadoInstance | string,
|
|
event: Function | number = 0,
|
|
listenerIndex: number = 0
|
|
): void {
|
|
const _instance = this._resolveInstance(instance)
|
|
const filter = this._resolveInstanceEvent(_instance, event)
|
|
this.clearListenerByIndex(
|
|
_instance,
|
|
this._instanceEventToFilter(filter, _instance.filters.Deposit),
|
|
listenerIndex
|
|
)
|
|
}
|
|
|
|
clearListeners(instance: TornadoInstance | string): void {
|
|
this._resolveInstance(instance).removeAllListeners()
|
|
}
|
|
|
|
listenForDeposits(instance: TornadoInstance | string): void {
|
|
this.listenForInstanceEvents(instance, 0)
|
|
}
|
|
|
|
listenForWithdrawals(instance: TornadoInstance | string): void {
|
|
this.listenForInstanceEvents(instance, 1)
|
|
}
|
|
|
|
listenForInstanceEvents(instance: TornadoInstance | string, event: Function | number = 0): void {
|
|
let _instance: TornadoInstance
|
|
let key: string
|
|
|
|
if (typeof instance !== 'string') {
|
|
const { network, token, denomination } = Onchain.getInstanceLookupKeysSync(instance.address)
|
|
_instance = this.loadInstance(network, token, denomination)
|
|
key = network + token + denomination
|
|
} else {
|
|
key = instance.toLowerCase()
|
|
_instance = this._resolveInstance(key)
|
|
}
|
|
|
|
const filter = this._resolveInstanceEvent(_instance!, event)
|
|
|
|
const isDeposit = filter == _instance.filters.Deposit
|
|
|
|
const cache = isDeposit
|
|
? this.loadDepositCache('Deposits' + key.toUpperCase())
|
|
: this.loadWithdrawalCache('Withdrawals' + key.toUpperCase())
|
|
|
|
this.listenForEvents(
|
|
isDeposit ? 'deposit' : 'withdrawal',
|
|
_instance!,
|
|
this._instanceEventToFilter(filter, _instance.filters.Deposit),
|
|
cache
|
|
)
|
|
}
|
|
|
|
private _instanceEventToFilter(event: Function, depositEvent: Function): EventFilter {
|
|
return event == depositEvent ? event(null, null, null) : event(null, null, null, null)
|
|
}
|
|
|
|
private _resolveInstanceEvent(instance: TornadoInstance, event: Function | number = 0): Function {
|
|
let filter: Function
|
|
|
|
if (typeof event === 'number') {
|
|
filter = event === 0 ? instance.filters.Deposit : instance.filters.Withdrawal
|
|
} else filter = event
|
|
|
|
return filter
|
|
}
|
|
|
|
private _resolveInstance(instance: TornadoInstance | string): TornadoInstance {
|
|
let _instance: TornadoInstance
|
|
|
|
if (typeof instance === 'string') {
|
|
instance = instance.toLowerCase()
|
|
|
|
const regexp = /([0-9]+)([a-z]+)([0-9.]+)/
|
|
const matches = instance.match(regexp)?.slice(1)
|
|
|
|
if (!matches || matches.length === 0)
|
|
throw ErrorUtils.getError('Core._resolveInstance: instance string key invalid.')
|
|
|
|
_instance = this.loadInstance(matches[0], matches[1], matches[2])
|
|
} else _instance = instance
|
|
|
|
return _instance
|
|
}
|
|
|
|
/**
|
|
* This is the main function to build a single Tornado Cash Classic deposit. An address need not be supplied because the returned note proves a deposit.
|
|
* @param instance The TornadoInstance for which to build transactions.
|
|
* @param options Whether or not to populate the transactions (only in the sense of encoding transaction data), and whether to backup notes and invoices. Defaults: `depositsPerInstance = [1], doNotPopulate = false, backup { notes = true, invoices = false }` Deposits per instance are hardcoded to 1, since we're doing a single transaction.
|
|
* @returns A promise which resolves to the created transaction.
|
|
*/
|
|
createDepositTransaction(instance: TornadoInstance, options?: Options.Core.Deposit): Transactions.Deposit {
|
|
let opts: Options.Core.Deposit = options ?? {}
|
|
opts.depositsPerInstance = [1]
|
|
return this.createDepositTransactions([instance], opts)[0]
|
|
}
|
|
|
|
/**
|
|
* This is the main function which is used to build Tornado Cash Classic deposit transactions. An address need not be supplied because the returned note proves a deposit.
|
|
* @param instances The TornadoInstance instances for which to build transactions.
|
|
* @param options The number of deposits per instance, whether or not to populate the transactions (only in the sense of encoding transaction data), whether to backup notes and invoices, and whether to call the instance directly instead of the router. Defaults: `depositsPerInstance = [1]*instance_num, doNotPopulate = false, callInstanceDirectly = false, backup { notes = true, invoices = false }`
|
|
* @returns A promise which resolves to the created transactions.
|
|
* @todo TODO: Maybe this should be sync and deposit backups should be async somewhere else
|
|
*/
|
|
createDepositTransactions(
|
|
instances: Array<TornadoInstance>,
|
|
options?: Options.Core.Deposit
|
|
): Array<Transactions.Deposit> {
|
|
this._checkProvider('createDepositTransactions')
|
|
|
|
const depositsPerInstance = options?.depositsPerInstance ?? new Array<number>(instances.length).fill(1)
|
|
|
|
const callInstanceDirectly = options?.callInstanceDirectly ?? false
|
|
|
|
const doNotPopulate = options?.doNotPopulate ?? false
|
|
|
|
if (depositsPerInstance.length != instances.length)
|
|
throw ErrorUtils.getError(
|
|
'Core.createDepositTx: number of deposit amount elements must equal the number of instances!'
|
|
)
|
|
|
|
const router: TornadoRouter = this.getRouter()
|
|
|
|
const txs: Array<Transactions.Deposit> = []
|
|
|
|
for (let i = 0, nInstances = instances.length; i < nInstances; i++) {
|
|
const { network, token, denomination } = Onchain.getInstanceLookupKeysSync(instances[i].address)
|
|
const pathstring = network + token + denomination
|
|
|
|
for (let d = 0, nDeposits = depositsPerInstance[i]; d < nDeposits; d++) {
|
|
const deposit = Primitives.createDeposit()
|
|
const note = Primitives.createNote(deposit.preimage)
|
|
|
|
if (!doNotPopulate) {
|
|
txs.push({
|
|
request: !callInstanceDirectly
|
|
? {
|
|
to: router.address,
|
|
data: router.interface.encodeFunctionData('deposit', [
|
|
instances[i].address,
|
|
deposit.hexCommitment,
|
|
[]
|
|
]),
|
|
value: token == 'eth' ? parseUnits(denomination) : BigNumber.from(0)
|
|
}
|
|
: {
|
|
to: instances[i].address,
|
|
data: instances[i].interface.encodeFunctionData('deposit', [deposit.hexCommitment]),
|
|
value: token == 'eth' ? parseUnits(denomination) : BigNumber.from(0)
|
|
},
|
|
note: pathstring + '_' + note,
|
|
invoice: pathstring + '_' + deposit.hexCommitment
|
|
})
|
|
} else
|
|
txs.push({
|
|
request: {},
|
|
note: pathstring + '_' + note,
|
|
invoice: pathstring + '_' + deposit.hexCommitment
|
|
})
|
|
}
|
|
}
|
|
|
|
return txs
|
|
}
|
|
|
|
async syncDeposits(instance: TornadoInstance, options?: Merge<Options.Sync, Options.Cache>): Promise<void> {
|
|
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
|
|
|
|
const pathstring = network + token + denomination
|
|
|
|
options = options ?? {}
|
|
|
|
options.startBlock = await Onchain.getInstanceDeployBlockNum(network, token, denomination)
|
|
|
|
const cache = this.loadDepositCache('Deposits' + pathstring.toUpperCase(), {
|
|
dirPath: options.dirPath,
|
|
adapter: options.adapter,
|
|
persistent: options.persistent
|
|
})
|
|
|
|
await this.sync('deposit', instance.filters.Deposit(null, null, null), instance, cache, options)
|
|
}
|
|
|
|
async syncWithdrawals(
|
|
instance: TornadoInstance,
|
|
options?: Merge<Options.Sync, Options.Cache>
|
|
): Promise<void> {
|
|
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(instance.address)
|
|
|
|
const pathstring = network + token + denomination
|
|
|
|
options = options ?? {}
|
|
|
|
options.startBlock = await Onchain.getInstanceDeployBlockNum(network, token, denomination)
|
|
|
|
const cache = this.loadWithdrawalCache('Withdrawals' + pathstring.toUpperCase(), {
|
|
dirPath: options.dirPath,
|
|
adapter: options.adapter,
|
|
persistent: options.persistent
|
|
})
|
|
|
|
await this.sync('withdrawal', instance.filters.Withdrawal(null, null, null), instance, cache, options)
|
|
}
|
|
|
|
protected async _populateSyncOptions(options: Options.Sync): Promise<DeepRequired<Options.Sync>> {
|
|
options.targetBlock = options.targetBlock ?? (await this.chain.latestBlockNum())
|
|
return super._populateSyncOptions(options)
|
|
}
|
|
|
|
async backupNote(
|
|
instance: TornadoInstance,
|
|
transaction: Transactions.Deposit,
|
|
options?: Options.Cache
|
|
): Promise<void> {
|
|
await this.backupNotes(instance, [transaction], options)
|
|
}
|
|
|
|
async backupInvoice(
|
|
instance: TornadoInstance,
|
|
transaction: Transactions.Deposit,
|
|
options?: Options.Cache
|
|
): Promise<void> {
|
|
await this.backupInvoices(instance, [transaction], options)
|
|
}
|
|
|
|
async backupNotes(
|
|
instance: TornadoInstance,
|
|
transactions: Array<Transactions.Deposit>,
|
|
options?: Options.Cache
|
|
): Promise<void> {
|
|
let cache = this._notes ?? new Cache.Base<Docs.Note>('DepositNotes', options)
|
|
|
|
await this._backupDepositData(instance.address, transactions, cache)
|
|
|
|
if (!this._notes) this._notes = cache
|
|
}
|
|
|
|
async backupInvoices(
|
|
instance: TornadoInstance,
|
|
transactions: Array<Transactions.Deposit>,
|
|
options?: Options.Cache
|
|
): Promise<void> {
|
|
let cache = this._invoices ?? new Cache.Base<Docs.Invoice>('DepositInvoices', options)
|
|
|
|
await this._backupDepositData(instance.address, transactions, cache)
|
|
|
|
if (!this._invoices) this._invoices = cache
|
|
}
|
|
|
|
private async _backupDepositData<T extends Docs.Note | Docs.Invoice>(
|
|
address: string,
|
|
transactions: Array<Transactions.Deposit>,
|
|
cache: Cache.Base<T>
|
|
): Promise<void> {
|
|
const { network, token, denomination } = await Onchain.getInstanceLookupKeys(address)
|
|
|
|
const notes = cache.name.length === 12 ? true : false
|
|
const name = notes ? 'notes' : 'invoices'
|
|
|
|
// We need a mutex here
|
|
const release = await this._mutex.acquire(name)
|
|
|
|
let id = +(await cache.db.info()).update_seq
|
|
|
|
await cache.db
|
|
.bulkDocs(
|
|
transactions.map((transaction) => {
|
|
if (notes) return new Docs.Note(++id, network, token, denomination, transaction.note!)
|
|
else return new Docs.Invoice(++id, network, token, denomination, transaction.invoice!)
|
|
}) as Array<T>
|
|
)
|
|
.catch((err) => {
|
|
throw ErrorUtils.ensureError(err)
|
|
})
|
|
|
|
// Release
|
|
release()
|
|
}
|
|
|
|
async exportCacheZip(
|
|
cacheName: string,
|
|
outDirPath?: string,
|
|
close?: boolean,
|
|
options?: Options.Cache
|
|
): Promise<void> {
|
|
const cache = new Cache.Base<Docs.Base>(cacheName, options)
|
|
await cache.zip(outDirPath, close)
|
|
if (close === true) this.caches.delete(cacheName)
|
|
}
|
|
|
|
async exportCacheJson(
|
|
cacheName: string,
|
|
outDirPath?: string,
|
|
close?: boolean,
|
|
options?: Options.Cache
|
|
): Promise<void> {
|
|
const cache = new Cache.Base<Docs.Base>(cacheName, options)
|
|
await cache.jsonify(outDirPath)
|
|
if (close === true) this.caches.delete(cacheName)
|
|
}
|
|
|
|
loadNotesCache(options?: Options.Cache): Cache.Base<Docs.Note> {
|
|
if (!this._notes) this._notes = new Cache.Base<Docs.Note>('DepositNotes', options)
|
|
return this._notes
|
|
}
|
|
|
|
loadInvoicesCache(options?: Options.Cache): Cache.Base<Docs.Invoice> {
|
|
if (!this._invoices) this._invoices = new Cache.Base<Docs.Invoice>('DepositInvoices', options)
|
|
return this._invoices
|
|
}
|
|
|
|
loadDepositCache(name: string, options?: Options.Cache): DepositCache {
|
|
return this.loadCache<Docs.Deposit, DepositCache>(name, options)
|
|
}
|
|
|
|
loadWithdrawalCache(name: string, options?: Options.Cache): WithdrawalCache {
|
|
return this.loadCache<Docs.Withdrawal, WithdrawalCache>(name, options)
|
|
}
|
|
|
|
loadCache<D extends Docs.Base, C extends Cache.Syncable<D> = Cache.Syncable<D>>(
|
|
name: string,
|
|
options?: Options.Cache
|
|
): C {
|
|
let constructor,
|
|
cache = super.loadCache(name)
|
|
|
|
if (!cache) {
|
|
const regexp = /([A-Za-z]+)([0-9]+)([A-Z]+)([0-9.]+)/
|
|
const matches = name.match(regexp)
|
|
|
|
if (!matches || matches.length !== 5)
|
|
throw ErrorUtils.getError(`Core.loadCache: name ${name} has wrong format for cache`)
|
|
|
|
if (matches[1] === 'Deposits') {
|
|
constructor = (name: string, options?: Options.Cache) => new DepositCache(name, options)
|
|
} else if (matches[1] === 'Withdrawals') {
|
|
constructor = (name: string, options?: Options.Cache) => new WithdrawalCache(name, options)
|
|
} else throw ErrorUtils.getError(`Core.loadCache: there exists no such event ${matches[1]}`)
|
|
|
|
cache = constructor(name, options)
|
|
this.caches.set(name, cache)
|
|
}
|
|
return cache as C
|
|
}
|
|
|
|
loadInstance(chainId: number | string, token: string, denomination: number | string): TornadoInstance {
|
|
token = token.toLowerCase()
|
|
return Contracts.getInstance('' + chainId, token, '' + denomination, this.chain.provider)
|
|
}
|
|
}
|