UNPKG

@colony/colony-js-adapter-ethers

Version:

ColonyContract adapter for ethers.js

159 lines (134 loc) 4.48 kB
/* @flow */ import ethers from 'ethers'; import { raceAgainstTimeout } from '@colony/colony-js-utils'; import type { IContract, IAdapter, IProvider, IWallet, Signature, Transaction, TransactionRequest, } from '@colony/colony-js-adapter'; import type { Query } from '@colony/colony-js-contract-loader'; import ContractLoader from '@colony/colony-js-contract-loader'; import type { ConstructorArgs } from './flowtypes'; import EthersContract from './EthersContract'; import { DEFAULT_TRANSACTION_WAIT_TIMEOUT } from './defaults'; // See https://solidity.readthedocs.io/en/v0.4.24/control-structures.html#error-handling-assert-require-revert-and-exceptions const SOLIDITY_ERROR = '0x08c379a0'; export default class EthersAdapter implements IAdapter { loader: ContractLoader; provider: IProvider; wallet: IWallet; constructor({ loader, provider, wallet }: ConstructorArgs) { this.loader = loader; this.provider = provider; this.wallet = wallet; } async callTransaction(transaction: TransactionRequest): Promise<string> { const result = await this.provider.call(transaction); // Given an erroneous result, parse the revert reason and throw an error. if ( ((result.length - 2) / 2) % 32 === 4 && result.slice(0, 10) === SOLIDITY_ERROR ) { const reason = ethers.utils.AbiCoder.defaultCoder.decode( ['string'], `0x${result.slice(10)}`, ); const error = new Error(`Reverted: ${reason}`); // $FlowFixMe Object.assign(error, { reason }); throw error; } return result; } async getContract(query: Query): Promise<IContract> { const { address, abi } = (await this.loader.load(query, { abi: true, address: true, bytecode: false, })) || {}; if (typeof address !== 'string') throw new Error('Unable to parse contract address'); return new EthersContract(address, abi, this.wallet); } async getContractDeployTransaction( query: Query, contractParams: Array<any>, ): Promise<Transaction> { const { abi, bytecode } = await this.loader.load(query, { abi: true, address: false, bytecode: true, }); return ethers.Contract.getDeployTransaction( bytecode, abi, ...contractParams, ); } async _getTransactionReceipt(transactionHash: string) { const receipt = await this.provider.getTransactionReceipt(transactionHash); if (receipt == null) throw new Error( `Transaction receipt not found (transaction: ${transactionHash})`, ); return receipt; } async waitForTransaction( transactionHash: string, timeoutMs: number = DEFAULT_TRANSACTION_WAIT_TIMEOUT, ) { return raceAgainstTimeout( this.provider.waitForTransaction(transactionHash), timeoutMs, ); } async getTransactionReceipt(transactionHash: string, timeoutMs?: number) { let receipt; try { // Attempt to get the receipt immediately; the transaction may have // already been mined, or we're running on TestRPC with no mining time. receipt = await this._getTransactionReceipt(transactionHash); } catch (error) { // Ignore the error if the receipt wasn't found if (!error.toString().includes('Transaction receipt not found')) throw error; } // If we didn't get the receipt, wait for the transaction and try again. if (!receipt) { await this.waitForTransaction(transactionHash, timeoutMs); } receipt = this._getTransactionReceipt(transactionHash); return receipt; } /** * Sign a message hash (as binary) and return a split signature. */ async signMessage(messageHash: string) { const messageBytes = ethers.utils.arrayify(messageHash); const signature = await this.wallet.signMessage(messageBytes); const { r: sigR, s: sigS, v: sigV } = ethers.utils.splitSignature( signature, ); return { sigR, sigS, sigV, }; } /** * Given a message digest and a signature, recover the address used to * sign the message. */ // eslint-disable-next-line class-methods-use-this ecRecover(digest: Array<number>, { sigR, sigS, sigV }: Signature): string { // This method doesn't need to be static, but flow Interfaces don't // support static methods. const recoveryParam = sigV - 27; return ethers.SigningKey.recover(digest, sigR, sigS, recoveryParam); } }