eth-revert-reason
Version:
Get the revert reason from an Ethereum transaction hash
137 lines (117 loc) • 5.22 kB
JavaScript
const ethers = require('ethers')
/**
* Get the revert reason from just a transaction hash
* @param {string} txHash - Hash of an Ethereum transaction
* @param {string} network - Ethereum network name
* @param {number} blockNumber - A block number to make the call from
* @param {*} customProvider - Custom provider (Only ethers and web3 providers are supported at this time)
*/
async function getRevertReason (txHash, network = 'mainnet', blockNumber = undefined, customProvider = undefined) {
({ network, blockNumber } = normalizeInput(network, blockNumber))
await validateInputPreProvider(txHash, network)
const provider = getProvider(customProvider, network)
await validateInputPostProvider(txHash, network, blockNumber, provider)
try {
const tx = await provider.getTransaction(txHash)
const code = await getCode(tx, network, blockNumber, provider)
return decodeMessage(code, network)
} catch (err) {
throw new Error('Unable to decode revert reason.')
}
}
function normalizeInput(network, blockNumber) {
return {
network: network.toLowerCase(),
blockNumber: blockNumber || 'latest'
}
}
async function validateInputPreProvider(txHash, network) {
// Only accept a valid txHash
if (!(/^0x([A-Fa-f0-9]{64})$/.test(txHash)) || txHash.substring(0,2) !== '0x') {
throw new Error('Invalid transaction hash')
}
const networks = ['mainnet', 'kovan', 'goerli', 'ropsten', 'rinkeby']
if (!networks.includes(network)) {
throw new Error('Not a valid network')
}
}
function getProvider(customProvider, network) {
// If a web3 provider is passed in, wrap it in an ethers provider
// A standard web3 provider will have `.version`, while an ethers will not
if (customProvider && customProvider.version) {
customProvider = new ethers.providers.Web3Provider(customProvider.currentProvider)
}
return customProvider || ethers.getDefaultProvider(network)
}
async function validateInputPostProvider(txHash, network, blockNumber, provider) {
// NOTE: Unless the node exposes the Parity `trace` endpoints, it is not possible to get the revert
// reason of a transaction on kovan. Because of this, the call will end up here and we will return a custom message.
if (network === 'kovan') {
try {
const tx = await provider.getTransaction(txHash)
getCode(tx, network, blockNumber, provider)
} catch (err) {
throw new Error('Please use a provider that exposes the Parity trace methods to decode the revert reason.')
}
}
// Validate the block number
if (blockNumber !== 'latest') {
const currentBlockNumber = await provider.getBlockNumber()
blockNumber = Number(blockNumber)
if (blockNumber >= currentBlockNumber) {
throw new Error('You cannot use a blocknumber that has not yet happened.')
}
// A block older than 128 blocks needs access to an archive node
if (blockNumber < currentBlockNumber - 128) {
try {
// Check to see if a provider has access to an archive node
await provider.getBalance(ethers.constants.AddressZero, blockNumber)
} catch (err) {
const errCode = JSON.parse(err.responseText).error.code
// NOTE: This error code is specific to Infura. Alchemy offers an Archive node by default, so an Alchemy node will never throw here.
const infuraErrCode = -32002
if (errCode === infuraErrCode) {
throw new Error('You cannot use a blocknumber that is older than 128 blocks. Please use a provider that uses a full archival node.')
}
}
}
}
}
function decodeMessage(code, network) {
// NOTE: `code` may end with 0's which will return a text string with empty whitespace characters
// This will truncate all 0s and set up the hex string as expected
// NOTE: Parity (Kovan) returns in a different format than other clients
let codeString
const fnSelectorByteLength = 4
const dataOffsetByteLength = 32
const strLengthByteLength = 32
const strLengthStartPos = 2 + ((fnSelectorByteLength + dataOffsetByteLength) * 2)
const strDataStartPos = 2 + ((fnSelectorByteLength + dataOffsetByteLength + strLengthByteLength) * 2)
if (network === 'kovan') {
const strLengthHex = code.slice(strLengthStartPos).slice(0, strLengthByteLength * 2)
const strLengthInt = parseInt(`0x${strLengthHex}`, 16)
const strDataEndPos = strDataStartPos + (strLengthInt * 2)
if (codeString === '0x') return ''
codeString = `0x${code.slice(strDataStartPos, strDataEndPos)}`
} else {
codeString = `0x${code.substr(138)}`.replace(/0+$/, '')
}
// If the codeString is an odd number of characters, add a trailing 0
if (codeString.length % 2 === 1) {
codeString += '0'
}
return ethers.utils.toUtf8String(codeString)
}
async function getCode(tx, network, blockNumber, provider) {
if (network === 'kovan') {
try {
// NOTE: The await is intentional in order for the catch to work
return await provider.call(tx, blockNumber)
} catch (err) {
return JSON.parse(err.responseText).error.data.substr(9)
}
} else {
return provider.call(tx, blockNumber)
}
}
module.exports = getRevertReason