UNPKG

@ethereum-waffle/provider

Version:

A mock provider for your blockchain testing needs.

166 lines 8.06 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.injectRevertString = exports.appendRevertString = exports.decodeRevertString = void 0; const ethers_1 = require("ethers"); const utils_1 = require("ethers/lib/utils"); const log_1 = require("./log"); const getHardhatErrorString = (callRevertError) => { var _a; const tryDecode = (error) => { var _a; const stackTrace = error === null || error === void 0 ? void 0 : error.stackTrace; const errorBuffer = (_a = stackTrace === null || stackTrace === void 0 ? void 0 : stackTrace[stackTrace.length - 1].message) === null || _a === void 0 ? void 0 : _a.value; if (errorBuffer) { return '0x' + errorBuffer.toString('hex'); } }; return (_a = tryDecode(callRevertError)) !== null && _a !== void 0 ? _a : tryDecode(callRevertError.error); }; const getGanacheErrorString = (callRevertError) => { var _a; return (_a = callRevertError === null || callRevertError === void 0 ? void 0 : callRevertError.error) === null || _a === void 0 ? void 0 : _a.data; }; /* eslint-disable no-control-regex */ /** * Decodes a revert string from a failed call/query that reverts on chain. * @param callRevertError The error catched from performing a reverting call (query) */ const decodeRevertString = (callRevertError) => { var _a; const errorString = (_a = getHardhatErrorString(callRevertError)) !== null && _a !== void 0 ? _a : getGanacheErrorString(callRevertError); if (errorString === undefined) { return ''; } /** * https://ethereum.stackexchange.com/a/66173 * Numeric.toHexString(Hash.sha3("Error(string)".getBytes())).substring(0, 10) */ const errorMethodId = '0x08c379a0'; if (errorString.startsWith(errorMethodId)) { return (0, utils_1.toUtf8String)('0x' + errorString.substring(138)) .replace(/\x00/g, ''); // Trim null characters. } const panicCodeId = '0x4e487b71'; if (errorString.startsWith(panicCodeId)) { let panicCode = parseInt(errorString.substring(panicCodeId.length), 16).toString(16); if (panicCode.length % 2 !== 0) { panicCode = '0' + panicCode; } if (['00', '01'].includes(panicCode)) { return ''; // For backwards compatibility; } return 'panic code 0x' + panicCode; } return ''; }; exports.decodeRevertString = decodeRevertString; const appendRevertString = async (etherProvider, receipt) => { if (receipt && parseInt(receipt.status) === 0) { (0, log_1.log)('Got transaction receipt of a failed transaction. Attempting to replay to obtain revert string.'); try { const tx = await etherProvider.getTransaction(receipt.transactionHash); (0, log_1.log)('Running transaction as a call:'); (0, log_1.log)(tx); if (tx.maxPriorityFeePerGas || tx.maxFeePerGas) { (0, log_1.log)('London hardfork detected, stripping gasPrice'); delete tx['gasPrice']; } // Run the transaction as a query. It works differently in Ethers, a revert code is included. await etherProvider.call(tx, tx.blockNumber); } catch (error) { (0, log_1.log)('Caught error, attempting to extract revert string from:'); (0, log_1.log)(error); receipt.revertString = (0, exports.decodeRevertString)(error); (0, log_1.log)(`Extracted revert string: "${receipt.revertString}"`); } } }; exports.appendRevertString = appendRevertString; /** * Ethers executes a gas estimation before sending the transaction to the blockchain. * This poses a problem for Waffle - we cannot track sent transactions which eventually revert. * This is a common use case for testing, but such transaction never gets sent. * A failed gas estimation prevents it from being sent. * * In test environment, we replace the gas estimation with an always-succeeding method. * If a transaction is meant to be reverted, it will do so after it is actually send and mined. * * Additionally, we patch the method for getting transaction receipt. * Ethers does not provide the error code in the receipt that we can use to * read a revert string, so we patch it and include it using a query to the blockchain. */ const injectRevertString = (provider) => { const etherProvider = new ethers_1.providers.Web3Provider(provider); return new Proxy(provider, { get(target, prop, receiver) { const original = target[prop]; if (typeof original !== 'function') { // Some non-method property - returned as-is. return original; } // Return a function override. return function (...args) { var _a; // Get a function result from the original provider. const originalResult = original.apply(target, args); // Every method other than `provider.request()` left intact. if (prop !== 'request') return originalResult; const method = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.method; /** * A method can be: * - `eth_estimateGas` - gas estimation, typically precedes `eth_sendRawTransaction`. * - `eth_getTransactionReceipt` - getting receipt of sent transaction, * typically supersedes `eth_sendRawTransaction`. * Other methods left intact. */ if (method === 'eth_estimateGas') { return (async () => { try { return await originalResult; } catch (e) { const blockGasLimit = provider.getOptions().miner.blockGasLimit; if (!blockGasLimit) { (0, log_1.log)('Block gas limit not found for fallback eth_estimateGas value. Using default value of 15M.'); return '0xE4E1C0'; // 15_000_000 } return blockGasLimit.toString(); } })(); } else if (method === 'eth_sendRawTransaction') { /** * Because we have overriden the gas estimation not to be failing on reverts, * we add a wait during transaction sending to retain original behaviour of * having an exception when sending a failing transaction. */ return (async () => { const transactionHash = await originalResult; const tx = await etherProvider.getTransaction(transactionHash); try { await tx.wait(); // Will end in an exception if the transaction is failing. } catch (e) { (0, log_1.log)('Transaction failed after sending and waiting.'); await (0, exports.appendRevertString)(etherProvider, e.receipt); throw e; } return transactionHash; })(); } else if (method === 'eth_getTransactionReceipt') { return (async () => { const receipt = await originalResult; await (0, exports.appendRevertString)(etherProvider, receipt); return receipt; })(); } return originalResult; // Fallback for any other method. }; } }); }; exports.injectRevertString = injectRevertString; //# sourceMappingURL=revertString.js.map