704648ed2c
Signed-off-by: T-Hax <>
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
// 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 {
|
|
ERC20Tornado,
|
|
ERC20Tornado__factory,
|
|
ETHTornado,
|
|
ETHTornado__factory,
|
|
TornadoInstance,
|
|
TornadoInstance__factory,
|
|
TornadoProxy__factory,
|
|
TornadoProxy,
|
|
ERC20,
|
|
ERC20__factory,
|
|
Multicall3Contract__factory
|
|
} from './deth'
|
|
import { Multicall3 } from './deth/Multicall3Contract'
|
|
|
|
// Local modules
|
|
import { Onchain, Cache, Docs } from '@tornado/sdk-data'
|
|
import { ErrorUtils, HexUtils, AsyncUtils } from '@tornado/sdk-utils'
|
|
|
|
// @ts-ignore
|
|
import { parseIndexableString } from 'pouchdb-collate'
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECLARATIONS (MUST BE INLINED) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// We use a vanilla provider here, but in reality we will probably
|
|
// add a censorship-checking custom derivative of it
|
|
type Provider = providers.Provider
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ REST ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
/**
|
|
* The Chain class stores Tornado-agnostic chain data and also
|
|
* handles such interactions.
|
|
*/
|
|
export class Chain {
|
|
public signer?: Signer
|
|
public provider: Provider
|
|
private _emptySigner: VoidSigner
|
|
|
|
private _chainId?: number
|
|
private _symbol?: string
|
|
private _fetched: boolean
|
|
|
|
constructor(provider: Provider, signer?: Signer) {
|
|
this.provider = provider
|
|
this.signer = signer
|
|
this._emptySigner = new VoidSigner('0x' + randomBytes(20).toString('hex'), provider)
|
|
this._fetched = false
|
|
}
|
|
|
|
async fetchChainData(): Promise<void> {
|
|
const network = await this.provider.getNetwork()
|
|
this._chainId = network.chainId
|
|
this._symbol = await Onchain.getNetworkSymbol(String(network.chainId))
|
|
this._fetched = true
|
|
}
|
|
|
|
private _propertiesFetched(parentCallName: string): void {
|
|
if (!this._fetched)
|
|
throw ErrorUtils.getError(
|
|
`Chain.${parentCallName}: properties must be fetched first with \`fetchProperties\`.`
|
|
)
|
|
}
|
|
|
|
get id(): number {
|
|
this._propertiesFetched('id')
|
|
return this._chainId!
|
|
}
|
|
get symbol(): string {
|
|
this._propertiesFetched('symbol')
|
|
return this._symbol!
|
|
}
|
|
|
|
latestBlockNum(): Promise<number> {
|
|
return this.provider.getBlockNumber()
|
|
}
|
|
|
|
getAccountBalance(account: string): Promise<BigNumber> {
|
|
return this.provider.getBalance(account)
|
|
}
|
|
|
|
getGasPrice(): Promise<BigNumber> {
|
|
return this.provider.getGasPrice()
|
|
}
|
|
|
|
getTokenContract(tokenAddress: string): ERC20 {
|
|
return Contracts.getToken(tokenAddress, this.signer ?? this.provider)
|
|
}
|
|
|
|
async getTokenDecimals(token: string): Promise<BigNumber> {
|
|
let treq = {
|
|
to: token,
|
|
data: '0x313ce567'
|
|
}
|
|
return BigNumber.from(await this._emptySigner.call(treq))
|
|
}
|
|
|
|
async getTokenBalance(account: string, token: string, normalized: boolean = false): Promise<BigNumber> {
|
|
let treq = {
|
|
to: token,
|
|
data: '0x70a08231000000000000000000000000' + HexUtils.prepareAddress(account)
|
|
}
|
|
let divisor = normalized ? BigNumber.from(10).pow(await this.getTokenDecimals(token)) : 1
|
|
return BigNumber.from(await this._emptySigner.call(treq)).div(divisor)
|
|
}
|
|
|
|
async populateBatchCall(
|
|
callStruct: Array<MarkOptional<Multicall3.Call3ValueStruct, 'value'>>
|
|
): Promise<TransactionRequest> {
|
|
if (callStruct[0].value)
|
|
return await Multicall3Contract__factory.connect(
|
|
await Onchain.getMulticall3Address(String(this.id)),
|
|
this.provider
|
|
).populateTransaction.aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
|
|
|
return await Multicall3Contract__factory.connect(
|
|
await Onchain.getMulticall3Address(String(this.id)),
|
|
this.provider
|
|
).populateTransaction.aggregate3(callStruct)
|
|
}
|
|
|
|
async batchCall(
|
|
callStruct: Array<MarkOptional<Multicall3.Call3ValueStruct, 'value'>>
|
|
): Promise<ContractTransaction> {
|
|
if (this.signer)
|
|
if (callStruct[0].value)
|
|
return await Multicall3Contract__factory.connect(
|
|
await Onchain.getMulticall3Address(String(this.id)),
|
|
this.signer
|
|
).aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
|
else {
|
|
return await Multicall3Contract__factory.connect(
|
|
await Onchain.getMulticall3Address(String(this.id)),
|
|
this.provider
|
|
).aggregate3(callStruct)
|
|
}
|
|
else throw ErrorUtils.getError('Chain.batchCall: no signer provided.')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is Tornado-specific.
|
|
*/
|
|
export namespace Contracts {
|
|
function _getContract<C extends TornadoContracts>(
|
|
name: string,
|
|
address: string,
|
|
signerOrProvider: Signer | Provider
|
|
): C {
|
|
if (name == 'TornadoInstance') {
|
|
return TornadoInstance__factory.connect(address, signerOrProvider) as C
|
|
} else if (name == 'TornadoProxy') {
|
|
return TornadoProxy__factory.connect(address, signerOrProvider) as C
|
|
} else if (name == 'ETHTornado') {
|
|
return ETHTornado__factory.connect(address, signerOrProvider) as C
|
|
} else if (name == 'ERC20Tornado') {
|
|
return ERC20Tornado__factory.connect(address, signerOrProvider) as C
|
|
} else {
|
|
return ERC20__factory.connect(address, signerOrProvider) as C
|
|
}
|
|
}
|
|
|
|
type Path = string
|
|
const contractMap: Map<Path, BaseContract> = new Map<Path, BaseContract>()
|
|
|
|
export function getProxy(network: string, signerOrProvider: Signer | Provider): TornadoProxy {
|
|
const key = `TornadoProxy${network}`
|
|
if (!contractMap.has(key)) {
|
|
contractMap.set(
|
|
key,
|
|
_getContract<TornadoProxy>('TornadoProxy', Onchain.getProxyAddressSync(network), signerOrProvider)
|
|
)
|
|
}
|
|
return contractMap.get(`TornadoProxy${network}`) as TornadoProxy
|
|
}
|
|
|
|
export function getInstance(
|
|
network: string,
|
|
token: string,
|
|
denomination: string,
|
|
signerOrProvider: Signer | Provider
|
|
): TornadoInstance {
|
|
const key = `TornadoInstance${network}${token}${denomination}`
|
|
if (!contractMap.has(key)) {
|
|
contractMap.set(
|
|
key,
|
|
_getContract<TornadoInstance>(
|
|
'TornadoInstance',
|
|
Onchain.getInstanceAddressSync(network, token, denomination),
|
|
signerOrProvider
|
|
)
|
|
)
|
|
}
|
|
return contractMap.get(key) as TornadoInstance
|
|
}
|
|
|
|
export function getToken(tokenAddress: string, signerOrProvider: Signer | Provider): ERC20 {
|
|
if (!contractMap.has(tokenAddress))
|
|
contractMap.set(tokenAddress, _getContract<ERC20>('ERC20', tokenAddress, signerOrProvider))
|
|
return contractMap.get(tokenAddress) as ERC20
|
|
}
|
|
|
|
export function getTornToken(signerOrProvider: Signer | Provider): ERC20 {
|
|
const key = '$TORN'
|
|
if (!contractMap.has(key)) {
|
|
contractMap.set(
|
|
key,
|
|
_getContract<ERC20>('ERC20', '0x77777feddddffc19ff86db637967013e6c6a116c', signerOrProvider)
|
|
)
|
|
}
|
|
return contractMap.get(key) as ERC20
|
|
}
|
|
}
|
|
|
|
export abstract class Synchronizer extends EventEmitter {
|
|
async sync(
|
|
eventName: string,
|
|
filter: EventFilter,
|
|
contract: BaseContract,
|
|
cache: Cache.Syncable<Docs.Base>,
|
|
options?: Options.Sync
|
|
): Promise<void> {
|
|
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<any> = 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) this.listenForEvents(eventName, contract, filter, cache)
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
|
|
listenForEvents(
|
|
name: string,
|
|
contract: BaseContract,
|
|
filter: EventFilter,
|
|
cache: Cache.Syncable<Docs.Base>
|
|
) {
|
|
contract.on(filter, (...eventArgs) => {
|
|
this.emit(name, cache.name, cache.db.put(cache.buildDoc(eventArgs[eventArgs.length - 1])))
|
|
})
|
|
}
|
|
|
|
clearListenerByIndex(contract: BaseContract, event: EventFilter, listenerIndex: number = 0): void {
|
|
const listeners = contract.listeners()
|
|
contract.off(event, listeners[listenerIndex])
|
|
}
|
|
|
|
protected abstract _populateSyncOptions(options?: Options.Sync): Promise<DeepRequired<Options.Sync>>
|
|
}
|