sdk-monorepo/src/lib/chain.ts
2023-04-27 17:34:22 +00:00

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