@ethereum-waffle/provider
Version:
A mock provider for your blockchain testing needs.
166 lines • 8.06 kB
JavaScript
;
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