sdk-monorepo/src/lib/chain.ts

195 lines
6.0 KiB
TypeScript
Raw Normal View History

// Tooling for interaction with the blockchain.
import * as Types from 'types/sdk/chain'
// External imports
import { TransactionRequest } from '@ethersproject/abstract-provider'
import { BaseContract, BigNumber, ContractTransaction, providers, Signer, VoidSigner } from 'ethers'
import { randomBytes } from 'crypto'
// 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'
// Local modules
import { OnchainData } from 'lib/data'
import { ErrorUtils } from './utils'
import { MarkOptional } from 'ts-essentials'
// We use a vanilla provider here, but in reality we will probably
// add a censorship-checking custom derivative of it
type Provider = providers.Provider
export function prepareAddress(address: string): string {
return (address.slice(0, 2) == '0x' ? address.slice(2) : address).toLowerCase().padStart(64, '0')
}
/**
* 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
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 latestBlockNum(): Promise<number> {
return this.provider.getBlockNumber()
}
async getAccountBalance(account: string): Promise<BigNumber> {
return this.provider.getBalance(account)
}
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' + prepareAddress(account)
}
let divisor = normalized ? BigNumber.from(10).pow(await this.getTokenDecimals(token)) : 1
return BigNumber.from(await this._emptySigner.call(treq)).div(divisor)
}
getTokenContract(tokenAddress: string): ERC20 {
return ERC20__factory.connect(tokenAddress, this.provider)
}
async populateBatchCall(
callStruct: Array<MarkOptional<Multicall3.Call3ValueStruct, 'value'>>
): Promise<TransactionRequest> {
if (callStruct[0].value)
return await Multicall3Contract__factory.connect(
await OnchainData.getMulticall3Address(String(this.chainId)),
this.provider
).populateTransaction.aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
return await Multicall3Contract__factory.connect(
await OnchainData.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 OnchainData.getMulticall3Address(String(this.chainId)),
this.signer
).aggregate3Value(callStruct as Array<Multicall3.Call3ValueStruct>)
else {
return await Multicall3Contract__factory.connect(
await OnchainData.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 Types.Contracts.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 OnchainData.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 OnchainData.getInstanceAddress(network, token, denomination),
signerOrProvider
)
)
}
return contractMap.get(key) as TornadoInstance
}
export async function getTornToken(signerOrProvider: Signer | Provider): Promise<ERC20> {
const key = '$TORN'
if (!contractMap.has(key)) {
contractMap.set(
key,
_getContract<ERC20>('ERC20', '0x77777feddddffc19ff86db637967013e6c6a116c', signerOrProvider)
)
}
return contractMap.get(key) as ERC20
}
}