Skip to content

Commit 2428b75

Browse files
authored
On transaction failure, fetch revert reason with replayed transaction (#57)
When transaction fails (receipt.status is Failed), fetch revert reason by replaying transaction.
1 parent 7eac841 commit 2428b75

File tree

12 files changed

+421
-26
lines changed

12 files changed

+421
-26
lines changed

ethers.nimble

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description = "library for interacting with Ethereum"
44
license = "MIT"
55

66
requires "nim >= 1.6.0"
7+
requires "chronicles >= 0.10.3 & < 0.11.0"
78
requires "chronos >= 3.0.0 & < 4.0.0"
89
requires "contractabi >= 0.6.0 & < 0.7.0"
910
requires "questionable >= 0.10.2 & < 0.11.0"

ethers/contract.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import std/json
22
import std/macros
33
import std/sequtils
4+
import pkg/chronicles
45
import pkg/chronos
56
import pkg/contractabi
67
import ./basics
@@ -13,6 +14,9 @@ export basics
1314
export provider
1415
export events
1516

17+
logScope:
18+
topics = "ethers contract"
19+
1620
type
1721
Contract* = ref object of RootObj
1822
provider: Provider

ethers/provider.nim

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pkg/chronicles
2+
import pkg/stew/byteutils
13
import ./basics
24
import ./transaction
35
import ./blocktag
@@ -47,18 +49,53 @@ type
4749
logs*: seq[Log]
4850
blockNumber*: ?UInt256
4951
cumulativeGasUsed*: UInt256
52+
effectiveGasPrice*: ?UInt256
5053
status*: TransactionStatus
54+
transactionType*: TransactionType
5155
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
5256
BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
5357
Topic* = array[32, byte]
5458
Block* = object
5559
number*: ?UInt256
5660
timestamp*: UInt256
5761
hash*: ?BlockHash
62+
PastTransaction* = object
63+
blockHash*: BlockHash
64+
blockNumber*: UInt256
65+
sender*: Address
66+
gas*: UInt256
67+
gasPrice*: UInt256
68+
hash*: TransactionHash
69+
input*: seq[byte]
70+
nonce*: UInt256
71+
to*: Address
72+
transactionIndex*: UInt256
73+
transactionType*: ?TransactionType
74+
chainId*: ?UInt256
75+
value*: UInt256
76+
v*, r*, s*: UInt256
5877

5978
const EthersDefaultConfirmations* {.intdefine.} = 12
6079
const EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks
6180

81+
logScope:
82+
topics = "ethers provider"
83+
84+
template raiseProviderError(msg: string) =
85+
raise newException(ProviderError, msg)
86+
87+
func toTransaction*(past: PastTransaction): Transaction =
88+
Transaction(
89+
sender: some past.sender,
90+
gasPrice: some past.gasPrice,
91+
data: past.input,
92+
nonce: some past.nonce,
93+
to: past.to,
94+
transactionType: past.transactionType,
95+
gasLimit: some past.gas,
96+
chainId: past.chainId
97+
)
98+
6299
method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
63100
doAssert false, "not implemented"
64101

@@ -79,6 +116,11 @@ method getTransactionCount*(provider: Provider,
79116
Future[UInt256] {.base, gcsafe.} =
80117
doAssert false, "not implemented"
81118

119+
method getTransaction*(provider: Provider,
120+
txHash: TransactionHash):
121+
Future[?PastTransaction] {.base, gcsafe.} =
122+
doAssert false, "not implemented"
123+
82124
method getTransactionReceipt*(provider: Provider,
83125
txHash: TransactionHash):
84126
Future[?TransactionReceipt] {.base, gcsafe.} =
@@ -94,7 +136,8 @@ method getLogs*(provider: Provider,
94136
doAssert false, "not implemented"
95137

96138
method estimateGas*(provider: Provider,
97-
transaction: Transaction): Future[UInt256] {.base, gcsafe.} =
139+
transaction: Transaction,
140+
blockTag = BlockTag.latest): Future[UInt256] {.base, gcsafe.} =
98141
doAssert false, "not implemented"
99142

100143
method getChainId*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
@@ -114,11 +157,73 @@ method subscribe*(provider: Provider,
114157
method unsubscribe*(subscription: Subscription) {.base, async.} =
115158
doAssert false, "not implemented"
116159

160+
proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async.} =
161+
# Replay transaction at block. Useful for fetching revert reasons, which will
162+
# be present in the raised error message. The replayed block number should
163+
# include the state of the chain in the block previous to the block in which
164+
# the transaction was mined. This means that transactions that were mined in
165+
# the same block BEFORE this transaction will not have their state transitions
166+
# included in the replay.
167+
# More information: https://snakecharmers.ethereum.org/web3py-revert-reason-parsing/
168+
trace "replaying transaction", gasLimit = tx.gasLimit, tx = $tx
169+
discard await provider.call(tx, BlockTag.init(blockNumber))
170+
171+
method getRevertReason*(
172+
provider: Provider,
173+
hash: TransactionHash,
174+
blockNumber: UInt256
175+
): Future[?string] {.base, async.} =
176+
177+
without pastTx =? await provider.getTransaction(hash):
178+
return none string
179+
180+
try:
181+
await provider.replay(pastTx.toTransaction, blockNumber)
182+
return none string
183+
except ProviderError as e:
184+
# should contain the revert reason
185+
return some e.msg
186+
187+
method getRevertReason*(
188+
provider: Provider,
189+
receipt: TransactionReceipt
190+
): Future[?string] {.base, async.} =
191+
192+
if receipt.status != TransactionStatus.Failure:
193+
return none string
194+
195+
without blockNumber =? receipt.blockNumber:
196+
return none string
197+
198+
return await provider.getRevertReason(receipt.transactionHash, blockNumber - 1)
199+
200+
proc ensureSuccess(
201+
provider: Provider,
202+
receipt: TransactionReceipt
203+
) {.async, upraises: [ProviderError].} =
204+
## If the receipt.status is Failed, the tx is replayed to obtain a revert
205+
## reason, after which a ProviderError with the revert reason is raised.
206+
## If no revert reason was obtained
207+
208+
# TODO: handle TransactionStatus.Invalid?
209+
if receipt.status == TransactionStatus.Failure:
210+
logScope:
211+
transactionHash = receipt.transactionHash.to0xHex
212+
213+
trace "transaction failed, replaying transaction to get revert reason"
214+
215+
if revertReason =? await provider.getRevertReason(receipt):
216+
trace "transaction revert reason obtained", revertReason
217+
raiseProviderError(revertReason)
218+
else:
219+
trace "transaction replay completed, no revert reason obtained"
220+
raiseProviderError("Transaction reverted with unknown reason")
221+
117222
proc confirm*(tx: TransactionResponse,
118223
confirmations = EthersDefaultConfirmations,
119224
timeout = EthersReceiptTimeoutBlks):
120225
Future[TransactionReceipt]
121-
{.async, upraises: [EthersError].} =
226+
{.async, upraises: [ProviderError, EthersError].} =
122227
## Waits for a transaction to be mined and for the specified number of blocks
123228
## to pass since it was mined (confirmations).
124229
## A timeout, in blocks, can be specified that will raise an error if too many
@@ -157,6 +262,7 @@ proc confirm*(tx: TransactionResponse,
157262

158263
if txBlockNumber + confirmations.u256 <= blockNumber + 1:
159264
await subscription.unsubscribe()
265+
await tx.provider.ensureSuccess(receipt)
160266
return receipt
161267

162268
proc confirm*(tx: Future[TransactionResponse],

ethers/providers/jsonrpc.nim

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import std/json
22
import std/tables
33
import std/uri
4+
import pkg/chronicles
5+
import pkg/eth/common/eth_types_json_serialization
46
import pkg/json_rpc/rpcclient
57
import pkg/json_rpc/errors
68
import ../basics
@@ -13,9 +15,13 @@ import ./jsonrpc/subscriptions
1315
export json
1416
export basics
1517
export provider
18+
export chronicles
1619

1720
push: {.upraises: [].}
1821

22+
logScope:
23+
topics = "ethers jsonrpc"
24+
1925
type
2026
JsonRpcProvider* = ref object of Provider
2127
client: Future[RpcClient]
@@ -137,6 +143,13 @@ method getTransactionCount*(provider: JsonRpcProvider,
137143
let client = await provider.client
138144
return await client.eth_getTransactionCount(address, blockTag)
139145

146+
method getTransaction*(provider: JsonRpcProvider,
147+
txHash: TransactionHash):
148+
Future[?PastTransaction] {.async.} =
149+
convertError:
150+
let client = await provider.client
151+
return await client.eth_getTransactionByHash(txHash)
152+
140153
method getTransactionReceipt*(provider: JsonRpcProvider,
141154
txHash: TransactionHash):
142155
Future[?TransactionReceipt] {.async.} =
@@ -164,10 +177,11 @@ method getLogs*(provider: JsonRpcProvider,
164177
return logs
165178

166179
method estimateGas*(provider: JsonRpcProvider,
167-
transaction: Transaction): Future[UInt256] {.async.} =
180+
transaction: Transaction,
181+
blockTag = BlockTag.latest): Future[UInt256] {.async.} =
168182
convertError:
169183
let client = await provider.client
170-
return await client.eth_estimateGas(transaction)
184+
return await client.eth_estimateGas(transaction, blockTag)
171185

172186
method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
173187
convertError:

ethers/providers/jsonrpc/conversions.nim

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import std/json
2+
import std/strformat
23
import std/strutils
34
import pkg/json_rpc/jsonmarshal
45
import pkg/stew/byteutils
@@ -9,6 +10,16 @@ import ../../provider
910

1011
export jsonmarshal
1112

13+
type JsonSerializationError = object of EthersError
14+
15+
template raiseSerializationError(message: string) =
16+
raise newException(JsonSerializationError, message)
17+
18+
proc expectFields(json: JsonNode, expectedFields: varargs[string]) =
19+
for fieldName in expectedFields:
20+
if not json.hasKey(fieldName):
21+
raiseSerializationError(fmt"'{fieldName}' field not found in ${json}")
22+
1223
func fromJson*(T: type, json: JsonNode, name = ""): T =
1324
fromJson(json, name, result)
1425

@@ -47,6 +58,15 @@ func `%`*(integer: UInt256): JsonNode =
4758
func fromJson*(json: JsonNode, name: string, result: var UInt256) =
4859
result = UInt256.fromHex(json.getStr())
4960
61+
# TransactionType
62+
63+
func fromJson*(json: JsonNode, name: string, result: var TransactionType) =
64+
let val = fromHex[int](json.getStr)
65+
result = TransactionType(val)
66+
67+
func `%`*(txType: TransactionType): JsonNode =
68+
%("0x" & txType.int.toHex(1))
69+
5070
# Transaction
5171
5272
func `%`*(transaction: Transaction): JsonNode =
@@ -70,6 +90,9 @@ func `%`*(blockTag: BlockTag): JsonNode =
7090
# Log
7191

7292
func fromJson*(json: JsonNode, name: string, result: var Log) =
93+
if not (json.hasKey("data") and json.hasKey("topics")):
94+
raise newException(ValueError, "'data' and/or 'topics' fields not found")
95+
7396
var data: seq[byte]
7497
var topics: seq[Topic]
7598
fromJson(json["data"], "data", data)
@@ -83,4 +106,83 @@ func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) =
83106
result = TransactionStatus(val)
84107

85108
func `%`*(status: TransactionStatus): JsonNode =
86-
%(status.int.toHex)
109+
%("0x" & status.int.toHex(1))
110+
111+
# PastTransaction
112+
113+
func fromJson*(json: JsonNode, name: string, result: var PastTransaction) =
114+
# Deserializes a past transaction, eg eth_getTransactionByHash.
115+
# Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash
116+
json.expectFields "blockHash", "blockNumber", "from", "gas", "gasPrice",
117+
"hash", "input", "nonce", "to", "transactionIndex", "value",
118+
"v", "r", "s"
119+
120+
result = PastTransaction(
121+
blockHash: BlockHash.fromJson(json["blockHash"], "blockHash"),
122+
blockNumber: UInt256.fromJson(json["blockNumber"], "blockNumber"),
123+
sender: Address.fromJson(json["from"], "from"),
124+
gas: UInt256.fromJson(json["gas"], "gas"),
125+
gasPrice: UInt256.fromJson(json["gasPrice"], "gasPrice"),
126+
hash: TransactionHash.fromJson(json["hash"], "hash"),
127+
input: seq[byte].fromJson(json["input"], "input"),
128+
nonce: UInt256.fromJson(json["nonce"], "nonce"),
129+
to: Address.fromJson(json["to"], "to"),
130+
transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"),
131+
value: UInt256.fromJson(json["value"], "value"),
132+
v: UInt256.fromJson(json["v"], "v"),
133+
r: UInt256.fromJson(json["r"], "r"),
134+
s: UInt256.fromJson(json["s"], "s"),
135+
)
136+
if json.hasKey("type"):
137+
result.transactionType = fromJson(?TransactionType, json["type"], "type")
138+
if json.hasKey("chainId"):
139+
result.chainId = fromJson(?UInt256, json["chainId"], "chainId")
140+
141+
func `%`*(tx: PastTransaction): JsonNode =
142+
let json = %*{
143+
"blockHash": tx.blockHash,
144+
"blockNumber": tx.blockNumber,
145+
"from": tx.sender,
146+
"gas": tx.gas,
147+
"gasPrice": tx.gasPrice,
148+
"hash": tx.hash,
149+
"input": tx.input,
150+
"nonce": tx.nonce,
151+
"to": tx.to,
152+
"transactionIndex": tx.transactionIndex,
153+
"value": tx.value,
154+
"v": tx.v,
155+
"r": tx.r,
156+
"s": tx.s
157+
}
158+
if txType =? tx.transactionType:
159+
json["type"] = %txType
160+
if chainId =? tx.chainId:
161+
json["chainId"] = %chainId
162+
return json
163+
164+
# TransactionReceipt
165+
166+
func fromJson*(json: JsonNode, name: string, result: var TransactionReceipt) =
167+
# Deserializes a transaction receipt, eg eth_getTransactionReceipt.
168+
# Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt
169+
json.expectFields "transactionHash", "transactionIndex", "cumulativeGasUsed",
170+
"effectiveGasPrice", "gasUsed", "logs", "logsBloom", "type",
171+
"status"
172+
173+
result = TransactionReceipt(
174+
transactionHash: fromJson(TransactionHash, json["transactionHash"], "transactionHash"),
175+
transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"),
176+
blockHash: fromJson(?BlockHash, json["blockHash"], "blockHash"),
177+
blockNumber: fromJson(?UInt256, json["blockNumber"], "blockNumber"),
178+
sender: fromJson(?Address, json["from"], "from"),
179+
to: fromJson(?Address, json["to"], "to"),
180+
cumulativeGasUsed: UInt256.fromJson(json["cumulativeGasUsed"], "cumulativeGasUsed"),
181+
effectiveGasPrice: fromJson(?UInt256, json["effectiveGasPrice"], "effectiveGasPrice"),
182+
gasUsed: UInt256.fromJson(json["gasUsed"], "gasUsed"),
183+
contractAddress: fromJson(?Address, json["contractAddress"], "contractAddress"),
184+
logs: seq[Log].fromJson(json["logs"], "logs"),
185+
logsBloom: seq[byte].fromJson(json["logsBloom"], "logsBloom"),
186+
transactionType: TransactionType.fromJson(json["type"], "type"),
187+
status: TransactionStatus.fromJson(json["status"], "status")
188+
)

ethers/providers/jsonrpc/signatures.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte]
55
proc eth_gasPrice(): UInt256
66
proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
77
proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode
8+
proc eth_getTransactionByHash(hash: TransactionHash): ?PastTransaction
89
proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block
910
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256
10-
proc eth_estimateGas(transaction: Transaction): UInt256
11+
proc eth_estimateGas(transaction: Transaction, blockTag: BlockTag): UInt256
1112
proc eth_chainId(): UInt256
1213
proc eth_sendTransaction(transaction: Transaction): TransactionHash
1314
proc eth_sendRawTransaction(data: seq[byte]): TransactionHash

0 commit comments

Comments
 (0)