ddb47e423a
Signed-off-by: T-Hax <>
309 lines
9.5 KiB
TypeScript
309 lines
9.5 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__factory,
|
|
ETHTornado__factory,
|
|
TornadoInstance,
|
|
TornadoInstance__factory,
|
|
TornadoProxy__factory,
|
|
TornadoProxy,
|
|
ERC20__factory,
|
|
ERC20,
|
|
Multicall3Contract__factory
|
|
} from 'types/deth'
|
|
import { Multicall3 } from 'types/deth/Multicall3Contract'
|
|
import { TornadoContracts, Options } from 'types/sdk/chain'
|
|
|
|
// Local modules
|
|
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
|
|
type Provider = providers.Provider
|
|
|
|
/**
|
|
* 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
|
|
public chainId?: number
|
|
public symbol?: string
|
|
|
|
constructor(provider: Provider, signer?: Signer) {
|
|
this.provider = provider
|
|
this.signer = signer
|
|
this._emptySigner = new VoidSigner('0x' + randomBytes(20).toString('hex'), provider)
|
|
}
|
|
|
|
async getChainId(): Promise<number> {
|
|
if (!this.chainId) this.chainId = (await this.provider.getNetwork()).chainId
|
|
return this.chainId
|
|
}
|
|
|
|
async getChainSymbol(): Promise<string> {
|
|
if (!this.symbol) this.symbol = await Onchain.getNetworkSymbol(String(await this.getChainId()))
|
|
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.chainId)),
|
|
this.provider
|
|
).populateTransaction.aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
|
|
|
return await Multicall3Contract__factory.connect(
|
|
await Onchain.getMulticall3Address(String(this.chainId)),
|
|
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.chainId)),
|
|
this.signer
|
|
).aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
|
|
else {
|
|
return await Multicall3Contract__factory.connect(
|
|
await Onchain.getMulticall3Address(String(this.chainId)),
|
|
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 async function getProxy(
|
|
network: string,
|
|
signerOrProvider: Signer | Provider
|
|
): Promise<TornadoProxy> {
|
|
const key = `TornadoProxy${network}`
|
|
if (!contractMap.has(key)) {
|
|
contractMap.set(
|
|
key,
|
|
_getContract<TornadoProxy>('TornadoProxy', await Onchain.getProxyAddress(network), signerOrProvider)
|
|
)
|
|
}
|
|
return contractMap.get(`TornadoProxy${network}`) as TornadoProxy
|
|
}
|
|
|
|
export async function getInstance(
|
|
network: string,
|
|
token: string,
|
|
denomination: string,
|
|
signerOrProvider: Signer | Provider
|
|
): Promise<TornadoInstance> {
|
|
const key = `TornadoInstance${network}${token}${denomination}`
|
|
if (!contractMap.has(key)) {
|
|
contractMap.set(
|
|
key,
|
|
_getContract<TornadoInstance>(
|
|
'TornadoInstance',
|
|
await Onchain.getInstanceAddress(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(
|
|
event: 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) {
|
|
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<DeepRequired<Options.Sync>>
|
|
}
|