UNPKG

dp-contract-proxy-kit

Version:

Enable batched transactions and contract account interactions using a unique deterministic Gnosis Safe.

282 lines (254 loc) 8.27 kB
import BigNumber from 'bignumber.js' import { Abi, Address } from '../../utils/basicTypes' import { zeroAddress } from '../../utils/constants' import { EthCallTx, EthersTransactionResult, EthSendTx, formatCallTx, normalizeGasLimit } from '../../utils/transactions' import EthLibAdapter, { Contract } from '../EthLibAdapter' import EthersV4ContractAdapter from './EthersV4ContractAdapter' import EthersV5ContractAdapter from './EthersV5ContractAdapter' export interface EthersAdapterConfig { ethers: any signer: any } class EthersAdapter extends EthLibAdapter { ethers: any signer: any /** * Creates an instance of EthersAdapter * * @param options - EthersAdapter configuration * @returns The EthersAdapter instance */ constructor({ ethers, signer }: EthersAdapterConfig) { super() if (!ethers) { throw new Error('ethers property missing from options') } if (!signer) { throw new Error('signer property missing from options') } this.ethers = ethers this.signer = signer } /** * Returns the current provider * * @returns The current provider */ getProvider(): any { // eslint-disable-next-line no-underscore-dangle return this.signer.provider.provider || this.signer.provider._web3Provider } /** * Sends a network request via JSON-RPC. * * @param method - JSON-RPC method * @param params - Params * @returns The request response */ providerSend(method: string, params: any[]): Promise<any> { return this.signer.provider.send(method, params) } /** * Signs data using a specific account. * * @param message - Data to sign * @param ownerAccount - Address to sign the data with * @returns The signature */ signMessage(message: string): Promise<string> { const messageArray = this.ethers.utils.arrayify(message) return this.signer.signMessage(messageArray) } /** * Returns the current network ID. * * @returns The network ID */ async getNetworkId(): Promise<number> { return (await this.signer.provider.getNetwork()).chainId } /** * Returns the default account used as the default "from" property. * * @returns The default account address */ async getAccount(): Promise<Address> { return this.signer.getAddress() } /** * Returns the balance of an address. * * @param address - The desired address * @returns The balance of the address */ async getBalance(address: Address): Promise<BigNumber> { const balance = await this.signer.provider.getBalance(address) return new BigNumber(balance.toString()) } /** * Returns the keccak256 hash of the data. * * @param data - Desired data * @returns The keccak256 of the data */ keccak256(data: string): string { return this.ethers.utils.keccak256(data) } /** * Encodes a function parameters based on its JSON interface object. * * @param types - An array with the types or a JSON interface of a function * @param values - The parameters to encode * @returns The ABI encoded parameters */ abiEncode(types: string[], values: any[]): string { return this.ethers.utils.defaultAbiCoder.encode(types, values) } /** * Decodes ABI encoded parameters to is JavaScript types. * * @param types - An array with the types or a JSON interface outputs array * @param data - The ABI byte code to decode * @returns The ABI encoded parameters */ abiDecode(types: string[], data: string): any[] { return this.ethers.utils.defaultAbiCoder.decode(types, data) } /** * Returns an instance of a contract. * * @param abi - ABI of the desired contract * @param address - Contract address * @returns The contract instance */ getContract(abi: Abi, address?: Address): Contract { const contract = new this.ethers.Contract(address || zeroAddress, abi, this.signer) const ethersVersion = this.ethers.version // TO-DO: Use semver comparison if (ethersVersion.split('.')[0] === '4') { return new EthersV4ContractAdapter(contract, this) } if (ethersVersion.split('.')[0] === 'ethers/5') { return new EthersV5ContractAdapter(contract, this) } throw new Error(`ethers version ${ethersVersion} not supported`) } /** * Deterministically returns the address where a contract will be deployed. * * @param deployer - Account that deploys the contract * @param salt - Salt * @param initCode - Code to be deployed * @returns The address where the contract will be deployed */ calcCreate2Address(deployer: Address, salt: string, initCode: string): string { return this.ethers.utils.getAddress( this.ethers.utils .solidityKeccak256( ['bytes', 'address', 'bytes32', 'bytes32'], ['0xff', deployer, salt, this.keccak256(initCode)] ) .slice(-40) ) } /** * Returns the code at a specific address. * * @param address - The desired address * @returns The code of the contract */ getCode(address: Address): Promise<string> { return this.signer.provider.getCode(address) } /** * Returns a block matching the block number or block hash. * * @param blockHashOrBlockNumber - The block number or block hash * @returns The block object */ getBlock(blockHashOrBlockNumber: string | number): Promise<{ [property: string]: any }> { return this.signer.provider.getBlock(blockHashOrBlockNumber) } /** * Returns the revert reason when a call fails. * * @param tx - Transaction to execute * @param block - Block number * @returns The revert data when the call fails */ async getCallRevertData(tx: EthCallTx, block: string | number): Promise<string> { try { // Handle old Geth/Ganache --noVMErrorsOnRPCResponse revert data return await this.ethCall(tx, block) } catch (e) { if (typeof e.data === 'string') { if (e.data.startsWith('Reverted 0x')) // handle OpenEthereum revert data format return e.data.slice(9) if (e.data.startsWith('0x')) // handle new Geth format return e.data } // handle Ganache revert data format const txHash = Object.getOwnPropertyNames(e.data).filter((k) => k.startsWith('0x'))[0] return e.data[txHash].return } } ethCall(tx: EthCallTx, block: number | string): Promise<string> { // This is to workaround https://github.com/ethers-io/ethers.js/issues/819 return this.providerSend('eth_call', [formatCallTx(tx), block]) } async checkFromAddress(from: Address): Promise<void> { const { getAddress } = this.ethers.utils const expectedFrom = await this.getAccount() if (getAddress(from) !== expectedFrom) { throw new Error(`want from ${expectedFrom} but got from ${from}`) } } /** * Sends a transaction to the network. * * @param tx - Transaction to send * @returns The transaction response */ async ethSendTransaction(tx: EthSendTx): Promise<EthersTransactionResult> { const { from, gas, ...sendTx } = normalizeGasLimit(tx) await this.checkFromAddress(from) const transactionResponse = await this.signer.sendTransaction({ gasLimit: gas, ...sendTx }) return { transactionResponse, hash: transactionResponse.hash } } /** * Formats transaction result depending on the current provider. * * @param txHash - Transaction hash * @param tx - Transaction response * @returns The formatted transaction response */ toSafeRelayTxResult(txHash: string, tx: Record<string, any>): Promise<EthersTransactionResult> { tx['hash'] = tx['txHash'] delete tx['txHash'] return new Promise((resolve, reject) => resolve({ transactionResponse: new Promise((resolve, reject) => resolve(tx)), hash: txHash }) ) } toRocksideRelayTxResult(tx: Record<string, any>): Promise<EthersTransactionResult> { tx['hash'] = tx['transaction_hash'] delete tx['transaction_hash'] return new Promise((resolve, reject) => resolve({ transactionResponse: new Promise((resolve, reject) => resolve(tx)), hash: tx['hash'] }) ) } } export default EthersAdapter