sdk-monorepo/@tornado/sdk-chain/src/index.ts

368 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 provider: Provider
private _emptySigner: VoidSigner
private _signer?: Signer
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
}
connectSigner(signer: Signer): void {
this._signer = signer
}
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 _signerConnected(parentCallName: string): void {
if (!this._signer) throw ErrorUtils.getError(`Chain.${parentCallName}: signer must be connected!`)
}
private _propertiesFetched(parentCallName: string): void {
if (!this._fetched)
throw ErrorUtils.getError(
`Chain.${parentCallName}: properties must be fetched first with \`fetchProperties\`.`
)
}
get signer(): Signer {
this._signerConnected('signer')
return this._signer!
}
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>>
}