Skip to content

Commit

Permalink
On transaction failure, fetch revert reason with replayed transaction (
Browse files Browse the repository at this point in the history
…#57)

When transaction fails (receipt.status is Failed), fetch revert reason by replaying transaction.
  • Loading branch information
emizzle authored Oct 25, 2023
1 parent 7eac841 commit 2428b75
Show file tree
Hide file tree
Showing 12 changed files with 421 additions and 26 deletions.
1 change: 1 addition & 0 deletions ethers.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description = "library for interacting with Ethereum"
license = "MIT"

requires "nim >= 1.6.0"
requires "chronicles >= 0.10.3 & < 0.11.0"
requires "chronos >= 3.0.0 & < 4.0.0"
requires "contractabi >= 0.6.0 & < 0.7.0"
requires "questionable >= 0.10.2 & < 0.11.0"
Expand Down
4 changes: 4 additions & 0 deletions ethers/contract.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import std/json
import std/macros
import std/sequtils
import pkg/chronicles
import pkg/chronos
import pkg/contractabi
import ./basics
Expand All @@ -13,6 +14,9 @@ export basics
export provider
export events

logScope:
topics = "ethers contract"

type
Contract* = ref object of RootObj
provider: Provider
Expand Down
110 changes: 108 additions & 2 deletions ethers/provider.nim
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pkg/chronicles
import pkg/stew/byteutils
import ./basics
import ./transaction
import ./blocktag
Expand Down Expand Up @@ -47,18 +49,53 @@ type
logs*: seq[Log]
blockNumber*: ?UInt256
cumulativeGasUsed*: UInt256
effectiveGasPrice*: ?UInt256
status*: TransactionStatus
transactionType*: TransactionType
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
Topic* = array[32, byte]
Block* = object
number*: ?UInt256
timestamp*: UInt256
hash*: ?BlockHash
PastTransaction* = object
blockHash*: BlockHash
blockNumber*: UInt256
sender*: Address
gas*: UInt256
gasPrice*: UInt256
hash*: TransactionHash
input*: seq[byte]
nonce*: UInt256
to*: Address
transactionIndex*: UInt256
transactionType*: ?TransactionType
chainId*: ?UInt256
value*: UInt256
v*, r*, s*: UInt256

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

logScope:
topics = "ethers provider"

template raiseProviderError(msg: string) =
raise newException(ProviderError, msg)

func toTransaction*(past: PastTransaction): Transaction =
Transaction(
sender: some past.sender,
gasPrice: some past.gasPrice,
data: past.input,
nonce: some past.nonce,
to: past.to,
transactionType: past.transactionType,
gasLimit: some past.gas,
chainId: past.chainId
)

method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented"

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

method getTransaction*(provider: Provider,
txHash: TransactionHash):
Future[?PastTransaction] {.base, gcsafe.} =
doAssert false, "not implemented"

method getTransactionReceipt*(provider: Provider,
txHash: TransactionHash):
Future[?TransactionReceipt] {.base, gcsafe.} =
Expand All @@ -94,7 +136,8 @@ method getLogs*(provider: Provider,
doAssert false, "not implemented"

method estimateGas*(provider: Provider,
transaction: Transaction): Future[UInt256] {.base, gcsafe.} =
transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented"

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

proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async.} =
# Replay transaction at block. Useful for fetching revert reasons, which will
# be present in the raised error message. The replayed block number should
# include the state of the chain in the block previous to the block in which
# the transaction was mined. This means that transactions that were mined in
# the same block BEFORE this transaction will not have their state transitions
# included in the replay.
# More information: https://snakecharmers.ethereum.org/web3py-revert-reason-parsing/
trace "replaying transaction", gasLimit = tx.gasLimit, tx = $tx
discard await provider.call(tx, BlockTag.init(blockNumber))

method getRevertReason*(
provider: Provider,
hash: TransactionHash,
blockNumber: UInt256
): Future[?string] {.base, async.} =

without pastTx =? await provider.getTransaction(hash):
return none string

try:
await provider.replay(pastTx.toTransaction, blockNumber)
return none string
except ProviderError as e:
# should contain the revert reason
return some e.msg

method getRevertReason*(
provider: Provider,
receipt: TransactionReceipt
): Future[?string] {.base, async.} =

if receipt.status != TransactionStatus.Failure:
return none string

without blockNumber =? receipt.blockNumber:
return none string

return await provider.getRevertReason(receipt.transactionHash, blockNumber - 1)

proc ensureSuccess(
provider: Provider,
receipt: TransactionReceipt
) {.async, upraises: [ProviderError].} =
## If the receipt.status is Failed, the tx is replayed to obtain a revert
## reason, after which a ProviderError with the revert reason is raised.
## If no revert reason was obtained

# TODO: handle TransactionStatus.Invalid?
if receipt.status == TransactionStatus.Failure:
logScope:
transactionHash = receipt.transactionHash.to0xHex

trace "transaction failed, replaying transaction to get revert reason"

if revertReason =? await provider.getRevertReason(receipt):
trace "transaction revert reason obtained", revertReason
raiseProviderError(revertReason)
else:
trace "transaction replay completed, no revert reason obtained"
raiseProviderError("Transaction reverted with unknown reason")

proc confirm*(tx: TransactionResponse,
confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks):
Future[TransactionReceipt]
{.async, upraises: [EthersError].} =
{.async, upraises: [ProviderError, EthersError].} =
## Waits for a transaction to be mined and for the specified number of blocks
## to pass since it was mined (confirmations).
## A timeout, in blocks, can be specified that will raise an error if too many
Expand Down Expand Up @@ -157,6 +262,7 @@ proc confirm*(tx: TransactionResponse,

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

proc confirm*(tx: Future[TransactionResponse],
Expand Down
18 changes: 16 additions & 2 deletions ethers/providers/jsonrpc.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import std/json
import std/tables
import std/uri
import pkg/chronicles
import pkg/eth/common/eth_types_json_serialization
import pkg/json_rpc/rpcclient
import pkg/json_rpc/errors
import ../basics
Expand All @@ -13,9 +15,13 @@ import ./jsonrpc/subscriptions
export json
export basics
export provider
export chronicles

push: {.upraises: [].}

logScope:
topics = "ethers jsonrpc"

type
JsonRpcProvider* = ref object of Provider
client: Future[RpcClient]
Expand Down Expand Up @@ -137,6 +143,13 @@ method getTransactionCount*(provider: JsonRpcProvider,
let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag)

method getTransaction*(provider: JsonRpcProvider,
txHash: TransactionHash):
Future[?PastTransaction] {.async.} =
convertError:
let client = await provider.client
return await client.eth_getTransactionByHash(txHash)

method getTransactionReceipt*(provider: JsonRpcProvider,
txHash: TransactionHash):
Future[?TransactionReceipt] {.async.} =
Expand Down Expand Up @@ -164,10 +177,11 @@ method getLogs*(provider: JsonRpcProvider,
return logs

method estimateGas*(provider: JsonRpcProvider,
transaction: Transaction): Future[UInt256] {.async.} =
transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.async.} =
convertError:
let client = await provider.client
return await client.eth_estimateGas(transaction)
return await client.eth_estimateGas(transaction, blockTag)

method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
convertError:
Expand Down
104 changes: 103 additions & 1 deletion ethers/providers/jsonrpc/conversions.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import std/json
import std/strformat
import std/strutils
import pkg/json_rpc/jsonmarshal
import pkg/stew/byteutils
Expand All @@ -9,6 +10,16 @@ import ../../provider

export jsonmarshal

type JsonSerializationError = object of EthersError

template raiseSerializationError(message: string) =
raise newException(JsonSerializationError, message)

proc expectFields(json: JsonNode, expectedFields: varargs[string]) =
for fieldName in expectedFields:
if not json.hasKey(fieldName):
raiseSerializationError(fmt"'{fieldName}' field not found in ${json}")

func fromJson*(T: type, json: JsonNode, name = ""): T =
fromJson(json, name, result)

Expand Down Expand Up @@ -47,6 +58,15 @@ func `%`*(integer: UInt256): JsonNode =
func fromJson*(json: JsonNode, name: string, result: var UInt256) =
result = UInt256.fromHex(json.getStr())

# TransactionType

func fromJson*(json: JsonNode, name: string, result: var TransactionType) =
let val = fromHex[int](json.getStr)
result = TransactionType(val)

func `%`*(txType: TransactionType): JsonNode =
%("0x" & txType.int.toHex(1))

# Transaction

func `%`*(transaction: Transaction): JsonNode =
Expand All @@ -70,6 +90,9 @@ func `%`*(blockTag: BlockTag): JsonNode =
# Log

func fromJson*(json: JsonNode, name: string, result: var Log) =
if not (json.hasKey("data") and json.hasKey("topics")):
raise newException(ValueError, "'data' and/or 'topics' fields not found")

var data: seq[byte]
var topics: seq[Topic]
fromJson(json["data"], "data", data)
Expand All @@ -83,4 +106,83 @@ func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) =
result = TransactionStatus(val)

func `%`*(status: TransactionStatus): JsonNode =
%(status.int.toHex)
%("0x" & status.int.toHex(1))

# PastTransaction

func fromJson*(json: JsonNode, name: string, result: var PastTransaction) =
# Deserializes a past transaction, eg eth_getTransactionByHash.
# Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash
json.expectFields "blockHash", "blockNumber", "from", "gas", "gasPrice",
"hash", "input", "nonce", "to", "transactionIndex", "value",
"v", "r", "s"

result = PastTransaction(
blockHash: BlockHash.fromJson(json["blockHash"], "blockHash"),
blockNumber: UInt256.fromJson(json["blockNumber"], "blockNumber"),
sender: Address.fromJson(json["from"], "from"),
gas: UInt256.fromJson(json["gas"], "gas"),
gasPrice: UInt256.fromJson(json["gasPrice"], "gasPrice"),
hash: TransactionHash.fromJson(json["hash"], "hash"),
input: seq[byte].fromJson(json["input"], "input"),
nonce: UInt256.fromJson(json["nonce"], "nonce"),
to: Address.fromJson(json["to"], "to"),
transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"),
value: UInt256.fromJson(json["value"], "value"),
v: UInt256.fromJson(json["v"], "v"),
r: UInt256.fromJson(json["r"], "r"),
s: UInt256.fromJson(json["s"], "s"),
)
if json.hasKey("type"):
result.transactionType = fromJson(?TransactionType, json["type"], "type")
if json.hasKey("chainId"):
result.chainId = fromJson(?UInt256, json["chainId"], "chainId")

func `%`*(tx: PastTransaction): JsonNode =
let json = %*{
"blockHash": tx.blockHash,
"blockNumber": tx.blockNumber,
"from": tx.sender,
"gas": tx.gas,
"gasPrice": tx.gasPrice,
"hash": tx.hash,
"input": tx.input,
"nonce": tx.nonce,
"to": tx.to,
"transactionIndex": tx.transactionIndex,
"value": tx.value,
"v": tx.v,
"r": tx.r,
"s": tx.s
}
if txType =? tx.transactionType:
json["type"] = %txType
if chainId =? tx.chainId:
json["chainId"] = %chainId
return json

# TransactionReceipt

func fromJson*(json: JsonNode, name: string, result: var TransactionReceipt) =
# Deserializes a transaction receipt, eg eth_getTransactionReceipt.
# Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt
json.expectFields "transactionHash", "transactionIndex", "cumulativeGasUsed",
"effectiveGasPrice", "gasUsed", "logs", "logsBloom", "type",
"status"

result = TransactionReceipt(
transactionHash: fromJson(TransactionHash, json["transactionHash"], "transactionHash"),
transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"),
blockHash: fromJson(?BlockHash, json["blockHash"], "blockHash"),
blockNumber: fromJson(?UInt256, json["blockNumber"], "blockNumber"),
sender: fromJson(?Address, json["from"], "from"),
to: fromJson(?Address, json["to"], "to"),
cumulativeGasUsed: UInt256.fromJson(json["cumulativeGasUsed"], "cumulativeGasUsed"),
effectiveGasPrice: fromJson(?UInt256, json["effectiveGasPrice"], "effectiveGasPrice"),
gasUsed: UInt256.fromJson(json["gasUsed"], "gasUsed"),
contractAddress: fromJson(?Address, json["contractAddress"], "contractAddress"),
logs: seq[Log].fromJson(json["logs"], "logs"),
logsBloom: seq[byte].fromJson(json["logsBloom"], "logsBloom"),
transactionType: TransactionType.fromJson(json["type"], "type"),
status: TransactionStatus.fromJson(json["status"], "status")
)
3 changes: 2 additions & 1 deletion ethers/providers/jsonrpc/signatures.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte]
proc eth_gasPrice(): UInt256
proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode
proc eth_getTransactionByHash(hash: TransactionHash): ?PastTransaction
proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256
proc eth_estimateGas(transaction: Transaction): UInt256
proc eth_estimateGas(transaction: Transaction, blockTag: BlockTag): UInt256
proc eth_chainId(): UInt256
proc eth_sendTransaction(transaction: Transaction): TransactionHash
proc eth_sendRawTransaction(data: seq[byte]): TransactionHash
Expand Down
Loading

0 comments on commit 2428b75

Please sign in to comment.