/* @flow */ /* eslint-disable import/no-cycle */ import BigNumber from 'bn.js'; import { raceAgainstTimeout } from '@colony/colony-js-utils'; import type { Transaction, TransactionOptions, } from '@colony/colony-js-adapter'; import ContractClient from './ContractClient'; import ContractMethod from './ContractMethod'; import type { ContractResponse, ContractMethodArgs, SendOptions, } from '../flowtypes'; export default class ContractMethodSender< InputValues: { [inputValueName: string]: any }, OutputValues: { [outputValueName: string]: any }, IContractClient: ContractClient, ContractData: { [dataValueName: string]: any }, > extends ContractMethod< InputValues, OutputValues, IContractClient, ContractData, > { _defaultGasLimit: ?number; constructor({ defaultGasLimit, ...rest }: ContractMethodArgs & { defaultGasLimit?: number, }) { super(rest); if (defaultGasLimit) this._defaultGasLimit = defaultGasLimit; } /** * Given named input values, call the method's contract function * with `eth_call` in order to simulate the transaction. * * The result will be parsed for a `revert` reason, which will throw * an error with the parsed revert reason (if necessary). */ async call(inputValues: InputValues): Promise { // Parse the transaction data for this function from the given values const { data } = this.client.contract.interface.functions[ this.functionName ].apply(this.client.contract.interface, this.getValidatedArgs(inputValues)); const from = await this.client.adapter.wallet.getAddress(); return this.client.callTransaction({ data, from, to: this.client.contract.address, }); } /** * Given named input values, call the method's contract function in * order to get a gas estimate for calling it with those values. */ async estimate(inputValues: InputValues): Promise { const args = this.getValidatedArgs(inputValues); // Simulate the transaction before estimation; will throw if erroneous await this.call(inputValues); return this.client.estimate(this.functionName, args); } /** * Given named input values and options for sending a transaction, create a * transaction which calls the method's contract function with those * values as transformed parameters, and collect the transaction receipt * and (optionally) event data. */ async send( inputValues: InputValues, options: SendOptions, ): Promise> { const args = this.getValidatedArgs(inputValues); return this._send(args, options); } async _sendWithWaitingForMining( transaction: Transaction, timeoutMs: number, ): Promise> { const receipt = await raceAgainstTimeout( this.client.adapter.getTransactionReceipt(transaction.hash), timeoutMs, ); const eventData = this.client.getReceiptEventData(receipt); // If the transaction failed, call it directly and add the revert // reason to the receipt. if (receipt && receipt.status === 0) { try { const { from, to, data, gasPrice, gasLimit, value } = transaction; await this.client.callTransaction({ data, from, gasLimit, gasPrice, value, ...(to != null ? { to } : {}), }); } catch (caughtError) { Object.assign(receipt, { reason: caughtError.reason }); } } return { successful: receipt && receipt.status === 1, meta: { transaction, receipt, }, eventData, }; } _sendWithoutWaitingForMining( transaction: Transaction, timeoutMs: number, ): ContractResponse { const receiptPromise = raceAgainstTimeout( this.client.adapter.getTransactionReceipt(transaction.hash), timeoutMs, ); // Wait for the receipt before determining whether it was successful const successfulPromise = new Promise(async (resolve, reject) => { try { const receipt = await receiptPromise; resolve(receipt && receipt.status === 1); } catch (error) { reject(error.toString()); } }); // Wait for the receipt before attempting to decode event logs const eventDataPromise = new Promise(async (resolve, reject) => { try { const receipt = await receiptPromise; try { resolve(this.client.getReceiptEventData(receipt)); } catch (decodeError) { reject(decodeError.toString()); } } catch (receiptError) { reject(receiptError.toString()); } }); return { successfulPromise, meta: { receiptPromise, transaction, }, eventDataPromise, }; } async _send( callArgs: Array, options: SendOptions, ): Promise> { const { timeoutMs, waitForMining, ...transactionOptions } = this._getDefaultSendOptions(options); const transaction = await this._sendTransaction( callArgs, transactionOptions, ); return waitForMining ? this._sendWithWaitingForMining(transaction, timeoutMs) : this._sendWithoutWaitingForMining(transaction, timeoutMs); } async _sendTransaction( callArgs: Array, transactionOptions: TransactionOptions, ) { return this.client.send(this.functionName, callArgs, transactionOptions); } /** * Given send options, set default values for this Sender. */ _getDefaultSendOptions(options: SendOptions) { const { name: networkName } = this.client.adapter.provider; // Allow a much longer timeout for mainnet transactions. const minutes = networkName === 'mainnet' ? 60 : 5; return Object.assign( {}, { timeoutMs: 1000 * 60 * minutes, waitForMining: true, ...(this._defaultGasLimit ? { gasLimit: this._defaultGasLimit } : null), }, options, ); } }