import { RevertReason } from '@0x/types'; import { logUtils } from '@0x/utils'; import { NodeType } from '@0x/web3-wrapper'; import * as chai from 'chai'; import { TransactionReceipt, TransactionReceiptStatus, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; import { web3Wrapper } from './web3_wrapper'; const expect = chai.expect; let nodeType: NodeType | undefined; // Represents the return value of a `sendTransaction` call. The Promise should // resolve with either a transaction receipt or a transaction hash. export type sendTransactionResult = Promise; /** * Returns ganacheError if the backing Ethereum node is Ganache and gethError * if it is Geth. * @param ganacheError the error to be returned if the backing node is Ganache. * @param gethError the error to be returned if the backing node is Geth. * @returns either the given ganacheError or gethError depending on the backing * node. */ async function _getGanacheOrGethError(ganacheError: string, gethError: string): Promise { if (_.isUndefined(nodeType)) { nodeType = await web3Wrapper.getNodeTypeAsync(); } switch (nodeType) { case NodeType.Ganache: return ganacheError; case NodeType.Geth: return gethError; default: throw new Error(`Unknown node type: ${nodeType}`); } } async function _getInsufficientFundsErrorMessageAsync(): Promise { return _getGanacheOrGethError("sender doesn't have enough funds", 'insufficient funds'); } async function _getTransactionFailedErrorMessageAsync(): Promise { return _getGanacheOrGethError('revert', 'always failing transaction'); } async function _getContractCallFailedErrorMessageAsync(): Promise { return _getGanacheOrGethError('revert', 'Contract call failed'); } /** * Returns the expected error message for an 'invalid opcode' resulting from a * contract call. The exact error message depends on the backing Ethereum node. */ export async function getInvalidOpcodeErrorMessageForCallAsync(): Promise { return _getGanacheOrGethError('invalid opcode', 'Contract call failed'); } /** * Returns the expected error message for the given revert reason resulting from * a sendTransaction call. The exact error message depends on the backing * Ethereum node and whether it supports revert reasons. * @param reason a specific revert reason. * @returns the expected error message. */ export async function getRevertReasonOrErrorMessageForSendTransactionAsync(reason: RevertReason): Promise { return _getGanacheOrGethError(reason, 'always failing transaction'); } /** * Rejects if the given Promise does not reject with an error indicating * insufficient funds. * @param p a promise resulting from a contract call or sendTransaction call. * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectInsufficientFundsAsync(p: Promise): Promise { const errMessage = await _getInsufficientFundsErrorMessageAsync(); return expect(p).to.be.rejectedWith(errMessage); } /** * Resolves if the the sendTransaction call fails with the given revert reason. * However, since Geth does not support revert reasons for sendTransaction, this * falls back to expectTransactionFailedWithoutReasonAsync if the backing * Ethereum node is Geth. * @param p a Promise resulting from a sendTransaction call * @param reason a specific revert reason * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectTransactionFailedAsync(p: sendTransactionResult, reason: RevertReason): Promise { // HACK(albrow): This dummy `catch` should not be necessary, but if you // remove it, there is an uncaught exception and the Node process will // forcibly exit. It's possible this is a false positive in // make-promises-safe. p.catch(e => { _.noop(e); }); if (_.isUndefined(nodeType)) { nodeType = await web3Wrapper.getNodeTypeAsync(); } switch (nodeType) { case NodeType.Ganache: return expect(p).to.be.rejectedWith(reason); case NodeType.Geth: logUtils.warn( 'WARNING: Geth does not support revert reasons for sendTransaction. This test will pass if the transaction fails for any reason.', ); return expectTransactionFailedWithoutReasonAsync(p); default: throw new Error(`Unknown node type: ${nodeType}`); } } /** * Resolves if the transaction fails without a revert reason, or if the * corresponding transactionReceipt has a status of 0 or '0', indicating * failure. * @param p a Promise resulting from a sendTransaction call * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectTransactionFailedWithoutReasonAsync(p: sendTransactionResult): Promise { return p .then(async result => { let txReceiptStatus: TransactionReceiptStatus; if (_.isString(result)) { // Result is a txHash. We need to make a web3 call to get the // receipt, then get the status from the receipt. const txReceipt = await web3Wrapper.awaitTransactionMinedAsync(result); txReceiptStatus = txReceipt.status; } else if ('status' in result) { // Result is a transaction receipt, so we can get the status // directly. txReceiptStatus = result.status; } else { throw new Error('Unexpected result type: ' + typeof result); } expect(_.toString(txReceiptStatus)).to.equal( '0', 'Expected transaction to fail but receipt had a non-zero status, indicating success', ); }) .catch(async err => { // If the promise rejects, we expect a specific error message, // depending on the backing Ethereum node type. const errMessage = await _getTransactionFailedErrorMessageAsync(); expect(err.message).to.include(errMessage); }); } /** * Resolves if the the contract call fails with the given revert reason. * @param p a Promise resulting from a contract call * @param reason a specific revert reason * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectContractCallFailedAsync(p: Promise, reason: RevertReason): Promise { return expect(p).to.be.rejectedWith(reason); } /** * Resolves if the contract call fails without a revert reason. * @param p a Promise resulting from a contract call * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectContractCallFailedWithoutReasonAsync(p: Promise): Promise { const errMessage = await _getContractCallFailedErrorMessageAsync(); return expect(p).to.be.rejectedWith(errMessage); } /** * Resolves if the contract creation/deployment fails without a revert reason. * @param p a Promise resulting from a contract creation/deployment * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectContractCreationFailedAsync( p: sendTransactionResult, reason: RevertReason, ): Promise { return expectTransactionFailedAsync(p, reason); } /** * Resolves if the contract creation/deployment fails without a revert reason. * @param p a Promise resulting from a contract creation/deployment * @returns a new Promise which will reject if the conditions are not met and * otherwise resolve with no value. */ export async function expectContractCreationFailedWithoutReasonAsync(p: Promise): Promise { const errMessage = await _getTransactionFailedErrorMessageAsync(); return expect(p).to.be.rejectedWith(errMessage); }