@nomiclabs/truffle-contract
Version:
A better contract abstraction for Ethereum (Buidler's fork)
114 lines (104 loc) • 3.79 kB
JavaScript
const DebugUtils = require("@truffle/debug-utils");
/**
* Methods to fetch and decode reason string from ganache when a tx errors.
*/
const reason = {
/**
* Extracts a reason string from `eth_call` response
* @param {Object} res response from `eth_call` to extract reason
* @param {Web3} web3 a helpful friend
* @param {InterfaceAdapter} interfaceAdapter a new helpful friend
* @return {String|Undefined} decoded reason string
*/
_extract: function (res, web3, _interfaceAdapter) {
//I'm not sure why interfaceAdapter is here if it's not used,
//so I just put an underscore in front of its name for now...
if (!res || (!res.error && !res.result)) return;
const isObject =
res && typeof res === "object" && res.error && res.error.data;
const isString =
res && typeof res === "object" && typeof res.result === "string";
if (isObject) {
// NOTE that Ganache >=2 returns the reason string when
// vmErrorsOnRPCResponse === true, which this code could
// be updated to respect (instead of computing here)
const data = res.error.data;
let resData;
if (typeof data === "string") {
resData = data; // geth, Ganache >7.0.0
} else if ("result" in data) {
// there is a single result (Ganache 7.0.0)
resData = data.result;
} else {
// handle `evm_mine`, `miner_start`, batch payloads, and ganache 2.0
// NOTE this only works for a single failed transaction at a time.
const hash = Object.keys(data)[0];
const errorDetails = data[hash];
resData = errorDetails.return /* ganache 2.0 */;
}
return reason._decode(resData, web3);
} else if (isString) {
return reason._decode(res.result, web3);
} else {
return undefined;
}
},
_decode: function (rawData, web3) {
const errorStringHash = "0x08c379a0";
const panicCodeHash = "0x4e487b71";
const selectorLength = 2 + 2 * 4; //0x then 4 bytes (0x then 8 hex digits)
const wordLength = 2 * 32; //32 bytes (64 hex digits)
if (!rawData) {
return undefined;
} else if (rawData === "0x") {
//no revert message
return undefined;
} else if (rawData.startsWith(errorStringHash)) {
try {
return web3.eth.abi.decodeParameter(
"string",
rawData.slice(selectorLength)
);
} catch (_) {
//no reasonable way to handle this case at present
return undefined;
}
} else if (rawData.startsWith(panicCodeHash)) {
if (rawData.length === selectorLength + wordLength) {
const panicCode = web3.eth.abi.decodeParameter(
"uint256",
rawData.slice(selectorLength)
); //this returns a decimal string
return `Panic: ${DebugUtils.panicString(panicCode)}`;
} else {
//incorrectly encoded panic...?
return undefined;
}
} else {
//we can't reasonably handle custom errors here
//(but we can probably assume it is one?)
return "Custom error (could not decode)";
}
},
/**
* Runs tx via `eth_call` and resolves a reason string if it exists on the response.
* @param {Object} web3
* @param {Object} interfaceAdapter
* @return {String|Undefined}
*/
get: function (params, web3, interfaceAdapter) {
const packet = {
jsonrpc: "2.0",
method: "eth_call",
params: [params, "latest"],
id: new Date().getTime()
};
return new Promise(resolve => {
web3.currentProvider.send(packet, (err, response) => {
const reasonString = reason._extract(response, web3, interfaceAdapter);
resolve(reasonString);
});
});
}
};
module.exports = reason;