├── index.d.ts ├── cli.js ├── LICENSE ├── package.json ├── .gitignore ├── README.md ├── index.js └── test └── test.js /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'eth-revert-reason'; 2 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const getRevertReason = require('./') 4 | 5 | const [,, ...args] = process.argv 6 | 7 | async function run() { 8 | const txHash = args[0] 9 | const network = args[1] || 'mainnet' 10 | const blockNumber = args[2] || undefined 11 | const provider = args[3] || undefined 12 | 13 | console.log(await getRevertReason(txHash, network, blockNumber, provider)) 14 | } 15 | 16 | run() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (C) 2020 Shane Fontaine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-revert-reason", 3 | "version": "1.0.3", 4 | "description": "Get the revert reason from an Ethereum transaction hash", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "publish-module": "npm publish --access public", 9 | "dist-tag": "npm dist-tag add \"eth-revert-reason@$(jq -r .version ", 28 | "Chris Whinfrey ", 29 | "Adam Hanna " 30 | ], 31 | "license": "MIT", 32 | "bin": { 33 | "getRevertReason": "cli.js" 34 | }, 35 | "dependencies": { 36 | "ethers": "^4.0.46" 37 | }, 38 | "devDependencies": { 39 | "chai": "^4.2.0", 40 | "jest": "^25.4.0", 41 | "mocha": "^7.1.1" 42 | }, 43 | "keywords": [ 44 | "ethereum", 45 | "revert", 46 | "string", 47 | "transaction" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | //this will affect all the git repos 2 | git config --global core.excludesfile ~/.gitignore 3 | 4 | 5 | //update files since .ignore won't if already tracked 6 | git rm --cached 7 | 8 | # Compiled source # 9 | ################### 10 | *.com 11 | *.class 12 | *.dll 13 | *.exe 14 | *.o 15 | *.so 16 | 17 | # Packages # 18 | ############ 19 | # it's better to unpack these files and commit the raw source 20 | # git has its own built in compression methods 21 | *.7z 22 | *.dmg 23 | *.gz 24 | *.iso 25 | *.jar 26 | *.rar 27 | *.tar 28 | *.zip 29 | 30 | # Logs and databases # 31 | ###################### 32 | *.log 33 | *.sql 34 | *.sqlite 35 | 36 | # OS generated files # 37 | ###################### 38 | .DS_Store 39 | .DS_Store? 40 | ._* 41 | .Spotlight-V100 42 | .Trashes 43 | # Icon? 44 | ehthumbs.db 45 | Thumbs.db 46 | .cache 47 | .project 48 | .settings 49 | .tmproj 50 | *.esproj 51 | nbproject 52 | 53 | # Numerous always-ignore extensions # 54 | ##################################### 55 | *.diff 56 | *.err 57 | *.orig 58 | *.rej 59 | *.swn 60 | *.swo 61 | *.swp 62 | *.vi 63 | *~ 64 | *.sass-cache 65 | *.grunt 66 | *.tmp 67 | 68 | # Dreamweaver added files # 69 | ########################### 70 | _notes 71 | dwsync.xml 72 | 73 | # Komodo # 74 | ########################### 75 | *.komodoproject 76 | .komodotools 77 | 78 | # Vim files to ignore # 79 | ####################### 80 | .VimballRecord 81 | .netrwhist 82 | 83 | # Node # 84 | ##################### 85 | node_modules 86 | 87 | # Bower # 88 | ##################### 89 | bower_components 90 | 91 | # Folders to ignore # 92 | ##################### 93 | .hg 94 | .svn 95 | .CVS 96 | intermediate 97 | publish 98 | .idea 99 | .graphics 100 | _test 101 | _archive 102 | uploads 103 | tmp 104 | /coverage 105 | .nyc_output/ 106 | 107 | # Files to ignore # 108 | ##################### 109 | package-lock.json 110 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eth-revert-reason 2 | 3 | > Get the revert reason from a tx hash on Ethereum 4 | 5 | [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/authereum/eth-revert-reason/master/LICENSE) 6 | [![NPM version](https://badge.fury.io/js/eth-revert-reason.svg)](http://badge.fury.io/js/eth-revert-reason) 7 | 8 | ## Install 9 | 10 | ```bash 11 | npm install eth-revert-reason 12 | ``` 13 | 14 | ## Notes 15 | 1. For now, this works consistently with the [Infura](https://infura.io/) and [Alchemy](https://docs.alchemyapi.io/) providers. Any other providers that you pass in may not work. 16 | 2. There are rare cases where a revert reason may be 'x' from the context of one block but it will be 'y' from the context of another block. This may cause inconsistencies. 17 | 3. This package relies on the ethers.js default provider. This provider may be subject to rate limits or inconsistencies. For consistent results, please pass in your own provider. 18 | 4. Alchemy's provider v2 uses Geth. 19 | 20 | 21 | ## Getting started 22 | 23 | ```javascript 24 | const getRevertReason = require('eth-revert-reason') 25 | 26 | // Failed with revert reason "Failed test" 27 | console.log(await getRevertReason('0xf212cc42d0eded75041225d71da6c3a8348bdb9102f2b73434b480419d31d69a')) // 'Failed test' 28 | console.log(await getRevertReason('0x640d2e0d1f4cff9b6e273458216451efb0dc08ebc13c30f6c88d48be7b35872a', 'goerli')) // 'Failed test' 29 | 30 | // Failed with no revert reason 31 | console.log(await getRevertReason('0x95ac5a6a1752ccac9647eb21ef8614ca2d3e40a5dbb99914adf87690fb1e6ccf')) // '' 32 | 33 | // Successful transaction 34 | console.log(await getRevertReason('0x02b8f8a00a0c0e9dcf60ddebd37ea305483fb30fd61233a505b73036408cae75')) // '' 35 | 36 | // Call from the context of a previous block with a custom provider 37 | let txHash = '0x6ea1798a2d0d21db18d6e45ca00f230160b05f172f6022aa138a0b605831d740' 38 | let network = 'mainnet' 39 | let blockNumber = 9892243 40 | let provider = getAlchemyProvider(network) // NOTE: getAlchemyProvider is not exposed in this package 41 | console.log(await getRevertReason(txHash, network, blockNumber, provider)) // 'BA: Insufficient gas (ETH) for refund' 42 | ``` 43 | 44 | ## Future work 45 | The following features will be added over time: 46 | 47 | 1. A better way to determine whether or not a node is full-archive. 48 | 2. A better way to determine whether or not a node exposes Parity `trace` methods. 49 | 3. Reduce the number of calls made by the provider. 50 | 4. Use raw RPC calls instead of a library 51 | - Will require unwrapping the provider from the library if provider is still a parameter 52 | - Note: this would still require using the ethers default provider 53 | 54 | 55 | ## Test 56 | 57 | ```bash 58 | npm test 59 | ``` 60 | 61 | ## License 62 | 63 | [MIT](LICENSE) 64 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ethers = require('ethers') 2 | 3 | /** 4 | * Get the revert reason from just a transaction hash 5 | * @param {string} txHash - Hash of an Ethereum transaction 6 | * @param {string} network - Ethereum network name 7 | * @param {number} blockNumber - A block number to make the call from 8 | * @param {*} customProvider - Custom provider (Only ethers and web3 providers are supported at this time) 9 | */ 10 | 11 | async function getRevertReason (txHash, network = 'mainnet', blockNumber = undefined, customProvider = undefined) { 12 | ({ network, blockNumber } = normalizeInput(network, blockNumber)) 13 | 14 | await validateInputPreProvider(txHash, network) 15 | const provider = getProvider(customProvider, network) 16 | await validateInputPostProvider(txHash, network, blockNumber, provider) 17 | 18 | try { 19 | const tx = await provider.getTransaction(txHash) 20 | const code = await getCode(tx, network, blockNumber, provider) 21 | return decodeMessage(code, network) 22 | } catch (err) { 23 | throw new Error('Unable to decode revert reason.') 24 | } 25 | } 26 | 27 | function normalizeInput(network, blockNumber) { 28 | return { 29 | network: network.toLowerCase(), 30 | blockNumber: blockNumber || 'latest' 31 | } 32 | } 33 | 34 | async function validateInputPreProvider(txHash, network) { 35 | // Only accept a valid txHash 36 | if (!(/^0x([A-Fa-f0-9]{64})$/.test(txHash)) || txHash.substring(0,2) !== '0x') { 37 | throw new Error('Invalid transaction hash') 38 | } 39 | 40 | const networks = ['mainnet', 'kovan', 'goerli', 'ropsten', 'rinkeby'] 41 | if (!networks.includes(network)) { 42 | throw new Error('Not a valid network') 43 | } 44 | } 45 | 46 | function getProvider(customProvider, network) { 47 | // If a web3 provider is passed in, wrap it in an ethers provider 48 | // A standard web3 provider will have `.version`, while an ethers will not 49 | if (customProvider && customProvider.version) { 50 | customProvider = new ethers.providers.Web3Provider(customProvider.currentProvider) 51 | } 52 | return customProvider || ethers.getDefaultProvider(network) 53 | } 54 | 55 | async function validateInputPostProvider(txHash, network, blockNumber, provider) { 56 | // NOTE: Unless the node exposes the Parity `trace` endpoints, it is not possible to get the revert 57 | // reason of a transaction on kovan. Because of this, the call will end up here and we will return a custom message. 58 | if (network === 'kovan') { 59 | try { 60 | const tx = await provider.getTransaction(txHash) 61 | getCode(tx, network, blockNumber, provider) 62 | } catch (err) { 63 | throw new Error('Please use a provider that exposes the Parity trace methods to decode the revert reason.') 64 | } 65 | } 66 | 67 | // Validate the block number 68 | if (blockNumber !== 'latest') { 69 | const currentBlockNumber = await provider.getBlockNumber() 70 | blockNumber = Number(blockNumber) 71 | 72 | if (blockNumber >= currentBlockNumber) { 73 | throw new Error('You cannot use a blocknumber that has not yet happened.') 74 | } 75 | 76 | // A block older than 128 blocks needs access to an archive node 77 | if (blockNumber < currentBlockNumber - 128) { 78 | try { 79 | // Check to see if a provider has access to an archive node 80 | await provider.getBalance(ethers.constants.AddressZero, blockNumber) 81 | } catch (err) { 82 | const errCode = JSON.parse(err.responseText).error.code 83 | // NOTE: This error code is specific to Infura. Alchemy offers an Archive node by default, so an Alchemy node will never throw here. 84 | const infuraErrCode = -32002 85 | if (errCode === infuraErrCode) { 86 | throw new Error('You cannot use a blocknumber that is older than 128 blocks. Please use a provider that uses a full archival node.') 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | function decodeMessage(code, network) { 94 | // NOTE: `code` may end with 0's which will return a text string with empty whitespace characters 95 | // This will truncate all 0s and set up the hex string as expected 96 | // NOTE: Parity (Kovan) returns in a different format than other clients 97 | let codeString 98 | const fnSelectorByteLength = 4 99 | const dataOffsetByteLength = 32 100 | const strLengthByteLength = 32 101 | const strLengthStartPos = 2 + ((fnSelectorByteLength + dataOffsetByteLength) * 2) 102 | const strDataStartPos = 2 + ((fnSelectorByteLength + dataOffsetByteLength + strLengthByteLength) * 2) 103 | 104 | if (network === 'kovan') { 105 | const strLengthHex = code.slice(strLengthStartPos).slice(0, strLengthByteLength * 2) 106 | const strLengthInt = parseInt(`0x${strLengthHex}`, 16) 107 | const strDataEndPos = strDataStartPos + (strLengthInt * 2) 108 | if (codeString === '0x') return '' 109 | codeString = `0x${code.slice(strDataStartPos, strDataEndPos)}` 110 | } else { 111 | codeString = `0x${code.substr(138)}`.replace(/0+$/, '') 112 | } 113 | 114 | // If the codeString is an odd number of characters, add a trailing 0 115 | if (codeString.length % 2 === 1) { 116 | codeString += '0' 117 | } 118 | 119 | return ethers.utils.toUtf8String(codeString) 120 | 121 | } 122 | 123 | async function getCode(tx, network, blockNumber, provider) { 124 | if (network === 'kovan') { 125 | try { 126 | // NOTE: The await is intentional in order for the catch to work 127 | return await provider.call(tx, blockNumber) 128 | } catch (err) { 129 | return JSON.parse(err.responseText).error.data.substr(9) 130 | } 131 | } else { 132 | return provider.call(tx, blockNumber) 133 | } 134 | } 135 | 136 | module.exports = getRevertReason 137 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const getRevertReason = require('../') 2 | const ethers = require('ethers') 3 | 4 | describe('getRevertReason', () => { 5 | const TX_HASH = { 6 | FAILED_AUTHEREUM_TX: { 7 | MAINNET: '0x6ea1798a2d0d21db18d6e45ca00f230160b05f172f6022aa138a0b605831d740', 8 | KOVAN: '0x371670e8e8dae59844f21845a6d9a96b1724a6f9e38842184e918b059fd143c7', 9 | GOERLI: '0x33d83df611f67f1cc609e3e6c44a7b3c95c38c0213199f08c9f4877a2a4b7baf', 10 | ROPSTEN: '0x80c344509e59b91c7b1e4ce5ec61204ad81ff4ee6198c59e0ab476513d7b7ea7', 11 | RINKEBY: '0xccd466d1fb2b5798ff751a760e132e22ae5f3672315d244d91e8a19b205c60b0' 12 | }, 13 | // The failure message is 'Failed test' 14 | FAILED_RANDOM_TX: { 15 | MAINNET: '0xf212cc42d0eded75041225d71da6c3a8348bdb9102f2b73434b480419d31d69a', 16 | KOVAN: '0xf683310b52501194b0e9dacb089bcfd00071ba71099682b1230e6c73e5d3b215', 17 | GOERLI: '0x640d2e0d1f4cff9b6e273458216451efb0dc08ebc13c30f6c88d48be7b35872a', 18 | ROPSTEN: '0xa41c14a059a8012e86733242ecff470683e524258d475426818303ab0ff5945c', 19 | RINKEBY: '0x2429ac3f86da6522d500a3a0d15d0a6e9a1c27d3189cf18f5628b5b8c60be8e9' 20 | }, 21 | NO_REVERT_MSG: { 22 | MAINNET: '0x95ac5a6a1752ccac9647eb21ef8614ca2d3e40a5dbb99914adf87690fb1e6ccf', 23 | KOVAN: '0x7df41e36899e0f4329afbc56985197751981ff245413503e57be375089b028f9', 24 | GOERLI: '0xa80d6653300142800d1bc2b675aec5eee3297575c987495d081b7f88b5fd31d6', 25 | ROPSTEN: '0x9a46afe833dfe42afc039e4ff4d3b248aa74652a468838e0d3e93c10ed7a96ef', 26 | RINKEBY: '0xad9cf1f3c332e03eff9badc668f7b673ca6a8adb012e672a18796080caf5e770' 27 | }, 28 | OUT_OF_GAS: { 29 | MAINNET: '0x5871434b2e89ac62d643cc42a6c44ba75e16f229dbbdba3994e8202df5f1102f', 30 | KOVAN: '0xbfcb5416557910fac32346c868a4ff8d254a7b744eb5a7a6089d0e0a96f1781b', 31 | GOERLI: '0xe478a5a661821ee839d0cb7db51d4d9a8df9e9a5e1f5e28ef8eb7d3cc6c570c4', 32 | ROPSTEN: '0x2439fa82407f070e4e0e4aad353780fc98fd3a7f406c527c2b3d2af405ac2243', 33 | RINKEBY: '0xaf9c2cebfcf15bbf6421ed1c1cb0d3b463c29ab0e7704fa0cc2feb1a53152671' 34 | }, 35 | BAD_INSTRUCTION: { 36 | MAINNET: '0xb84a441996ba3cffc82b17c5dd282b02feeb63826846931a8d16211fbece45bf', 37 | KOVAN: '0x3a7019d68dc220df3e402ebb45de7fe9fd9983b20d78fe94b9f0f89ec327db99', 38 | GOERLI: '0x6e9a8114b7acd8ec73411b5ed246f974be575cf849348239c1433466d5e21c2d', 39 | ROPSTEN: '0x2cb358e26fd80c267b5a2142eb2e877440a6920d52bc2ba8287efdb87ed9fb89', 40 | RINKEBY: '0xf5f98e8e2032e07b1695c9b80df51ffae8b91157740c2e130162914e486983a6' 41 | }, 42 | SUCCESSFUL_TX: { 43 | MAINNET: '0x02b8f8a00a0c0e9dcf60ddebd37ea305483fb30fd61233a505b73036408cae75', 44 | KOVAN: '0xdbfef20645354e200a00a7e1a815722d832e9071f727f9cd0158f4906384e97f', 45 | GOERLI: '0x5e5dfb4d08ebcfbc3de543879d2ea2183bab5503bc2a68f8b2bbc622a901b2b4', 46 | ROPSTEN: '0x31590a132bc0b858ad69be3d409d3de625ebab98b48ad07db97edf5d0cfce786', 47 | RINKEBY: '0x2a215ad51888b09c585c4244d4ed5e9d9bccb773d400497881ba387b8bd7cc61' 48 | }, 49 | // The error message is: "Tried to read `uint64` from a `CBOR.Value` with majorType != 0" 50 | // NOTE: This error message also tests the odd-length revert messages 51 | SPECIAL_CHARACTERS: { 52 | MAINNET: '0xceee8c5fea071d03008e0e29fe65d1387a1c9871f11bc20870b1b74ef44d1bd7', 53 | KOVAN: '0x22186d2d5da40b4cbadd1097b35471afa64c2f0eca8be79d8ee7ea354765bfbe', 54 | GOERLI: '0x0d2835694314658d752631611357a2e08029045bcfbabf2a38be5ff6a4a21f59', 55 | ROPSTEN: '0x6d8f63fa85c66f8ba65afea6c0b57a8bad9963be55712a64fe52f08a35f03958', 56 | RINKEBY: '0xfe0f6540e9b5dc15d31b131196ee4857b2c183166f8fcdc636bd3549aa8b5717' 57 | } 58 | } 59 | 60 | // REVERT_REASONS 61 | const REVERT_REASON = { 62 | // NOTE: The real reason why this transaction failed is 'BA: Insufficient gas (token) for refund', but 63 | // since the address has made another transaction since then, the `call` from this new context will say 64 | // the auth key is invalid, since the signature is technically invalid for this state. 65 | FAILED_AUTHEREUM_TX: 'LKMTA: Login key is expired', 66 | FAILED_RANDOM_TX: 'Failed test', 67 | UNABLE_TO_DECODE: 'Unable to decode revert reason', 68 | PARTY_TRACE_NOT_AVAILABLE: 'Please use a provider that exposes the Parity trace methods to decode the revert reason', 69 | SUCCESSFUL_TX: '', 70 | FAILURE_WITH_NO_REVERT_REASON: '', 71 | OUT_OF_GAS: '', 72 | BAD_INSTRUCTION: '', 73 | SPECIAL_CHARACTERS: 'Tried to read `uint64` from a `CBOR.Value` with majorType != 0', 74 | INVALID_TX_HASH: 'Invalid transaction hash', 75 | NOT_VALID_NETWORK: 'Not a valid network', 76 | FUTURE_BLOCK_NUMBER: 'You cannot use a blocknumber that has not yet happened.', 77 | ARCHIVE_NODE_REQUIRED: 'You cannot use a blocknumber that is older than 128 blocks. Please use a provider that uses a full archival node.', 78 | INSUFFICIENT_FUNDS: 'BA: Insufficient gas (ETH) for refund' 79 | } 80 | 81 | describe('Happy Path', () => { 82 | describe('mainnet', () => { 83 | const _network = 'mainnet' 84 | test('authereum transaction', async () => { 85 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 86 | }) 87 | test('random transaction', async () => { 88 | expect(await getRevertReason(TX_HASH.FAILED_RANDOM_TX.MAINNET, _network)).toEqual(REVERT_REASON.FAILED_RANDOM_TX) 89 | }) 90 | test('failure with no revert reason', async () => { 91 | expect(await getRevertReason(TX_HASH.NO_REVERT_MSG.MAINNET, _network)).toEqual(REVERT_REASON.FAILURE_WITH_NO_REVERT_REASON) 92 | }) 93 | }) 94 | describe('kovan', () => { 95 | const _network = 'kovan' 96 | test('authereum transaction', async () => { 97 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.KOVAN, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 98 | }) 99 | test('random transaction', async () => { 100 | expect(await getRevertReason(TX_HASH.FAILED_RANDOM_TX.KOVAN, _network)).toEqual(REVERT_REASON.FAILED_RANDOM_TX) 101 | }) 102 | test('failure with no revert reason', async () => { 103 | expect(await getRevertReason(TX_HASH.NO_REVERT_MSG.KOVAN, _network)).toEqual(REVERT_REASON.FAILURE_WITH_NO_REVERT_REASON) 104 | }) 105 | }) 106 | describe('goerli', () => { 107 | const _network = 'goerli' 108 | test('authereum transaction', async () => { 109 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.GOERLI, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 110 | }) 111 | test('random transaction', async () => { 112 | expect(await getRevertReason(TX_HASH.FAILED_RANDOM_TX.GOERLI, _network)).toEqual(REVERT_REASON.FAILED_RANDOM_TX) 113 | }) 114 | test('failure with no revert reason', async () => { 115 | expect(await getRevertReason(TX_HASH.NO_REVERT_MSG.GOERLI, _network)).toEqual(REVERT_REASON.FAILURE_WITH_NO_REVERT_REASON) 116 | }) 117 | }) 118 | describe('ropsten', () => { 119 | const _network = 'ropsten' 120 | test('authereum transaction', async () => { 121 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.ROPSTEN, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 122 | }) 123 | test('random transaction', async () => { 124 | expect(await getRevertReason(TX_HASH.FAILED_RANDOM_TX.ROPSTEN, _network)).toEqual(REVERT_REASON.FAILED_RANDOM_TX) 125 | }) 126 | test('failure with no revert reason', async () => { 127 | expect(await getRevertReason(TX_HASH.NO_REVERT_MSG.ROPSTEN, _network)).toEqual(REVERT_REASON.FAILURE_WITH_NO_REVERT_REASON) 128 | }) 129 | }) 130 | describe('rinkeby', () => { 131 | const _network = 'rinkeby' 132 | test('authereum transaction', async () => { 133 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.RINKEBY, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 134 | }) 135 | test('random transaction', async () => { 136 | expect(await getRevertReason(TX_HASH.FAILED_RANDOM_TX.RINKEBY, _network)).toEqual(REVERT_REASON.FAILED_RANDOM_TX) 137 | }) 138 | test('failure with no revert reason', async () => { 139 | expect(await getRevertReason(TX_HASH.NO_REVERT_MSG.RINKEBY, _network)).toEqual(REVERT_REASON.FAILURE_WITH_NO_REVERT_REASON) 140 | }) 141 | }) 142 | }) 143 | describe('Non-Happy Path', () => { 144 | describe('mainnet', () => { 145 | const _network = 'mainnet' 146 | test('successful transaction', async () => { 147 | expect(await getRevertReason(TX_HASH.SUCCESSFUL_TX.MAINNET, _network)).toEqual(REVERT_REASON.SUCCESSFUL_TX) 148 | }) 149 | test('out of gas', async () => { 150 | expect(await getRevertReason(TX_HASH.OUT_OF_GAS.MAINNET, _network)).toEqual(REVERT_REASON.OUT_OF_GAS) 151 | }) 152 | test('bad instruction', async () => { 153 | expect(await getRevertReason(TX_HASH.BAD_INSTRUCTION.MAINNET, _network)).toEqual(REVERT_REASON.BAD_INSTRUCTION) 154 | }) 155 | test('special characters', async () => { 156 | expect(await getRevertReason(TX_HASH.SPECIAL_CHARACTERS.MAINNET, _network)).toEqual(REVERT_REASON.SPECIAL_CHARACTERS) 157 | }) 158 | }) 159 | describe('kovan', () => { 160 | const _network = 'kovan' 161 | test('successful transaction', async () => { 162 | expect(await getRevertReason(TX_HASH.SUCCESSFUL_TX.KOVAN, _network)).toEqual(REVERT_REASON.SUCCESSFUL_TX) 163 | }) 164 | test('out of gas', async () => { 165 | expect(await getRevertReason(TX_HASH.OUT_OF_GAS.KOVAN, _network)).toEqual(REVERT_REASON.OUT_OF_GAS) 166 | }) 167 | test('bad instruction', async () => { 168 | expect(await getRevertReason(TX_HASH.BAD_INSTRUCTION.KOVAN, _network)).toEqual(REVERT_REASON.BAD_INSTRUCTION) 169 | }) 170 | test('special characters', async () => { 171 | expect(await getRevertReason(TX_HASH.SPECIAL_CHARACTERS.KOVAN, _network)).toEqual(REVERT_REASON.SPECIAL_CHARACTERS) 172 | }) 173 | }) 174 | describe('goerli', () => { 175 | const _network = 'goerli' 176 | test('successful transaction', async () => { 177 | expect(await getRevertReason(TX_HASH.SUCCESSFUL_TX.GOERLI, _network)).toEqual(REVERT_REASON.SUCCESSFUL_TX) 178 | }) 179 | test('out of gas', async () => { 180 | expect(await getRevertReason(TX_HASH.OUT_OF_GAS.GOERLI, _network)).toEqual(REVERT_REASON.OUT_OF_GAS) 181 | }) 182 | test('bad instruction', async () => { 183 | expect(await getRevertReason(TX_HASH.BAD_INSTRUCTION.GOERLI, _network)).toEqual(REVERT_REASON.BAD_INSTRUCTION) 184 | }) 185 | test('special characters', async () => { 186 | expect(await getRevertReason(TX_HASH.SPECIAL_CHARACTERS.GOERLI, _network)).toEqual(REVERT_REASON.SPECIAL_CHARACTERS) 187 | }) 188 | }) 189 | describe('ropsten', () => { 190 | const _network = 'ropsten' 191 | test('successful transaction', async () => { 192 | expect(await getRevertReason(TX_HASH.SUCCESSFUL_TX.ROPSTEN, _network)).toEqual(REVERT_REASON.SUCCESSFUL_TX) 193 | }) 194 | test('out of gas', async () => { 195 | expect(await getRevertReason(TX_HASH.OUT_OF_GAS.ROPSTEN, _network)).toEqual(REVERT_REASON.OUT_OF_GAS) 196 | }) 197 | test('bad instruction', async () => { 198 | expect(await getRevertReason(TX_HASH.BAD_INSTRUCTION.ROPSTEN, _network)).toEqual(REVERT_REASON.BAD_INSTRUCTION) 199 | }) 200 | test('special characters', async () => { 201 | expect(await getRevertReason(TX_HASH.SPECIAL_CHARACTERS.ROPSTEN, _network)).toEqual(REVERT_REASON.SPECIAL_CHARACTERS) 202 | }) 203 | }) 204 | describe('rinkeby', () => { 205 | const _network = 'rinkeby' 206 | test('successful transaction', async () => { 207 | expect(await getRevertReason(TX_HASH.SUCCESSFUL_TX.RINKEBY, _network)).toEqual(REVERT_REASON.SUCCESSFUL_TX) 208 | }) 209 | test('out of gas', async () => { 210 | expect(await getRevertReason(TX_HASH.OUT_OF_GAS.RINKEBY, _network)).toEqual(REVERT_REASON.OUT_OF_GAS) 211 | }) 212 | test('bad instruction', async () => { 213 | expect(await getRevertReason(TX_HASH.BAD_INSTRUCTION.RINKEBY, _network)).toEqual(REVERT_REASON.BAD_INSTRUCTION) 214 | }) 215 | test('special characters', async () => { 216 | expect(await getRevertReason(TX_HASH.SPECIAL_CHARACTERS.RINKEBY, _network)).toEqual(REVERT_REASON.SPECIAL_CHARACTERS) 217 | }) 218 | }) 219 | }) 220 | describe('other tests', () => { 221 | test('invalid txHash - invalid length', async () => { 222 | const _txHash = '0x123' 223 | const _network = 'mainnet' 224 | await expect(getRevertReason(_txHash, _network)).rejects.toThrow(new Error(REVERT_REASON.INVALID_TX_HASH)) 225 | }) 226 | test('invalid txHash - invalid characters', async () => { 227 | const _txHash = '0xzzz1798a2d0d21db18d6e45ca00f230160b05f172f6022aa138a0b605831d740' 228 | const _network = 'mainnet' 229 | await expect(getRevertReason(_txHash, _network)).rejects.toThrow(new Error(REVERT_REASON.INVALID_TX_HASH)) 230 | }) 231 | test('invalid txHash - no 0x prefix', async () => { 232 | const _txHash = 'aa6ea1798a2d0d21db18d6e45ca00f230160b05f172f6022aa138a0b605831d740' 233 | const _network = 'mainnet' 234 | await expect(getRevertReason(_txHash, _network)).rejects.toThrow(new Error(REVERT_REASON.INVALID_TX_HASH)) 235 | }) 236 | test('abnormal txHash - all upper case', async () => { 237 | const _txHash = '0xF212CC42D0EDED75041225D71DA6C3A8348BDB9102F2B73434B480419D31D69A' 238 | const _network = 'mainnet' 239 | expect(await getRevertReason(_txHash, _network)).toEqual(REVERT_REASON.FAILED_RANDOM_TX) 240 | }) 241 | test('unknown network', async () => { 242 | const _network = 'test' 243 | await expect(getRevertReason(TX_HASH.SUCCESSFUL_TX.KOVAN, _network)).rejects.toThrow(new Error(REVERT_REASON.NOT_VALID_NETWORK)) 244 | }) 245 | test('upercase network', async () => { 246 | const _network = 'MaInNeT' 247 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 248 | }) 249 | test('upercase network', async () => { 250 | const _network = 'MaInNeT' 251 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 252 | }) 253 | test('future block number', async () => { 254 | const _network = 'mainnet' 255 | const _blockNumber = 999999999999 256 | await expect(getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network, _blockNumber)).rejects.toThrow(new Error(REVERT_REASON.FUTURE_BLOCK_NUMBER)) 257 | }) 258 | test('early block number with no archive node', async () => { 259 | const _network = 'mainnet' 260 | const _blockNumber = 123 261 | await expect(getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network, _blockNumber)).rejects.toThrow(new Error(REVERT_REASON.ARCHIVE_NODE_REQUIRED)) 262 | }) 263 | test('get revert reason from the context of an early block', async () => { 264 | const _network = 'mainnet' 265 | // Revert reason from 'latest' context should be REVERT_REASON.FAILED_AUTHEREUM_TX 266 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network)).toEqual(REVERT_REASON.FAILED_AUTHEREUM_TX) 267 | 268 | // Revert reason from block 9892243 context should be REVERT_REASON.INSUFFICIENT_FUNDS 269 | const _blockNumber = 9892243 270 | const _provider = getAlchemyProvider(_network) 271 | expect(await getRevertReason(TX_HASH.FAILED_AUTHEREUM_TX.MAINNET, _network, _blockNumber, _provider)).toEqual(REVERT_REASON.INSUFFICIENT_FUNDS) 272 | }) 273 | }) 274 | }) 275 | 276 | function getAlchemyProvider(network) { 277 | const rpcUrl = `https://eth-${network}.alchemyapi.io/v2/demo` 278 | return new ethers.providers.JsonRpcProvider(rpcUrl) 279 | } 280 | --------------------------------------------------------------------------------