From 35f80e78feeab7a964cb4c928ebd136f32899d25 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:51:59 +1000 Subject: [PATCH] Add PastTransaction with serialization and tests, clean up revertReason fetching --- ethers/contract.nim | 26 ++++------ ethers/provider.nim | 48 ++++++++++++++++++- ethers/providers/jsonrpc.nim | 11 +++-- ethers/providers/jsonrpc/conversions.nim | 41 ++++++++++------ ethers/providers/jsonrpc/signatures.nim | 3 +- .../providers/jsonrpc/testConversions.nim | 20 ++++++-- 6 files changed, 108 insertions(+), 41 deletions(-) diff --git a/ethers/contract.nim b/ethers/contract.nim index cb62d48..885b7b3 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -258,29 +258,19 @@ proc confirm*(tx: Future[?TransactionResponse], let receipt = await response.confirm(confirmations, timeout) - if receipt.status != TransactionStatus.Success: + # TODO: handle TransactionStatus.Invalid? + if receipt.status == TransactionStatus.Failure: logScope: transactionHash = receipt.transactionHash - echo "[ethers contract] transaction failed, status: ", receipt.status - trace "transaction failed", status = receipt.status - without blockNumber =? receipt.blockNumber: - raiseContractError "Transaction reverted with unknown reason" - let provider = response.provider - without transaction =? await provider.getTransaction(receipt.transactionHash): - raiseContractError "Transaction reverted with unknown reason" + trace "transaction failed, replaying transaction to get revert reason" - try: - echo "[ethers contract] replaying transaction to get revert reason" - trace "replaying transaction to get revert reason" - await provider.replay(transaction, blockNumber) - echo "transaction replay completed, no revert reason obtained" + if revertReason =? await response.provider.getRevertReason(receipt): + trace "transaction revert reason obtained", revertReason + raiseContractError(revertReason) + else: trace "transaction replay completed, no revert reason obtained" - except ProviderError as e: - echo "transaction revert reason obtained, reason: ", e.msg - trace "transaction revert reason obtained", reason = e.msg - # should contain the revert reason - raiseContractError e.msg + raiseContractError("Transaction reverted with unknown reason") return receipt diff --git a/ethers/provider.nim b/ethers/provider.nim index a6e0b1c..198bd8c 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -1,3 +1,4 @@ +import pkg/chronicles import ./basics import ./transaction import ./blocktag @@ -55,16 +56,40 @@ type 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 + value*: UInt256 + v*, r*, s* : UInt256 const EthersDefaultConfirmations* {.intdefine.} = 12 const EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks +logScope: + topics = "ethers provider" + +template raiseProviderError(message: string) = + raise newException(ProviderError, message) + method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} = doAssert false, "not implemented" method getBlock*(provider: Provider, tag: BlockTag): Future[?Block] {.base, gcsafe.} = doAssert false, "not implemented" +method call*(provider: Provider, + tx: PastTransaction, + blockTag = BlockTag.latest): Future[seq[byte]] {.base, gcsafe.} = + doAssert false, "not implemented" + method call*(provider: Provider, tx: Transaction, blockTag = BlockTag.latest): Future[seq[byte]] {.base, gcsafe.} = @@ -81,7 +106,7 @@ method getTransactionCount*(provider: Provider, method getTransaction*(provider: Provider, txHash: TransactionHash): - Future[?Transaction] {.base, gcsafe.} = + Future[?PastTransaction] {.base, gcsafe.} = doAssert false, "not implemented" method getTransactionReceipt*(provider: Provider, @@ -119,7 +144,7 @@ method subscribe*(provider: Provider, method unsubscribe*(subscription: Subscription) {.base, async.} = doAssert false, "not implemented" -proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async.} = +proc replay*(provider: Provider, tx: PastTransaction, 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 @@ -129,6 +154,25 @@ proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async. # More information: https://snakecharmers.ethereum.org/web3py-revert-reason-parsing/ discard await provider.call(tx, BlockTag.init(blockNumber - 1)) +method getRevertReason*( + provider: Provider, + receipt: TransactionReceipt +): Future[?string] {.base, async.} = + + if receipt.status != TransactionStatus.Failure: + raiseProviderError "cannot get revert reason, transaction not failed" + + without blockNumber =? receipt.blockNumber or + transaction =? await provider.getTransaction(receipt.transactionHash): + return none string + + try: + await provider.replay(transaction, blockNumber) + return none string + except ProviderError as e: + # should contain the revert reason + return some e.msg + proc confirm*(tx: TransactionResponse, confirmations = EthersDefaultConfirmations, timeout = EthersReceiptTimeoutBlks): diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 6d359c6..eaa59ab 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -49,13 +49,11 @@ template convertError(nonce = none UInt256, body) = try: body except JsonRpcError as error: - echo "nonce for error below: ", nonce trace "jsonrpc error", error = error.msg raiseProviderError(error.msg, nonce) # Catch all ValueErrors for now, at least until JsonRpcError is actually # raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151 except ValueError as error: - echo "nonce for error below: ", nonce trace "jsonrpc error (from rpc client)", error = error.msg raiseProviderError(error.msg, nonce) @@ -147,6 +145,13 @@ method call*(provider: JsonRpcProvider, let client = await provider.client return await client.eth_call(tx, blockTag) +method call*(provider: JsonRpcProvider, + tx: PastTransaction, + blockTag = BlockTag.latest): Future[seq[byte]] {.async.} = + convertError: + let client = await provider.client + return await client.eth_call(tx, blockTag) + method getGasPrice*(provider: JsonRpcProvider): Future[UInt256] {.async.} = convertError: let client = await provider.client @@ -162,7 +167,7 @@ method getTransactionCount*(provider: JsonRpcProvider, method getTransaction*(provider: JsonRpcProvider, txHash: TransactionHash): - Future[?Transaction] {.async.} = + Future[?PastTransaction] {.async.} = convertError: let client = await provider.client return await client.eth_getTransactionByHash(txHash) diff --git a/ethers/providers/jsonrpc/conversions.nim b/ethers/providers/jsonrpc/conversions.nim index 98ccf41..568a431 100644 --- a/ethers/providers/jsonrpc/conversions.nim +++ b/ethers/providers/jsonrpc/conversions.nim @@ -10,6 +10,11 @@ import ../../provider export jsonmarshal +type JsonSerializationError = object of EthersError + +template raiseSerializationError(message: string) = + raise newException(JsonSerializationError, message) + func fromJson*(T: type, json: JsonNode, name = ""): T = fromJson(json, name, result) @@ -91,23 +96,31 @@ func `%`*(status: TransactionStatus): JsonNode = # Transaction -func fromJson*(json: JsonNode, name: string, result: var Transaction) = - # Deserializes a transaction response, eg eth_getTransactionByHash. - # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash - let expectedFields = - @["input", "from", "to", "value", "nonce", "chainId", "gasPrice"] - +proc expectFields(json: JsonNode, expectedFields: varargs[string]) = for fieldName in expectedFields: if not json.hasKey(fieldName): - raise newException(ValueError, - fmt"'{fieldName}' field not found in ${json}") + raiseSerializationError(fmt"'{fieldName}' field not found in ${json}") - result = Transaction( - sender: fromJson(?Address, json["from"], "from"), +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"), - data: seq[byte].fromJson(json["input"], "input"), + transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"), value: UInt256.fromJson(json["value"], "value"), - nonce: fromJson(?UInt256, json["nonce"], "nonce"), - chainId: fromJson(?UInt256, json["chainId"], "chainId"), - gasPrice: fromJson(?UInt256, json["gasPrice"], "gasPrice") + v: UInt256.fromJson(json["v"], "v"), + r: UInt256.fromJson(json["r"], "r"), + s: UInt256.fromJson(json["s"], "s"), ) \ No newline at end of file diff --git a/ethers/providers/jsonrpc/signatures.nim b/ethers/providers/jsonrpc/signatures.nim index 227c218..916060a 100644 --- a/ethers/providers/jsonrpc/signatures.nim +++ b/ethers/providers/jsonrpc/signatures.nim @@ -2,10 +2,11 @@ proc net_version(): string proc eth_accounts: seq[Address] proc eth_blockNumber: UInt256 proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte] +proc eth_call(transaction: PastTransaction, 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): ?Transaction +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 diff --git a/testmodule/providers/jsonrpc/testConversions.nim b/testmodule/providers/jsonrpc/testConversions.nim index d94e856..76abca4 100644 --- a/testmodule/providers/jsonrpc/testConversions.nim +++ b/testmodule/providers/jsonrpc/testConversions.nim @@ -1,6 +1,7 @@ import std/unittest import pkg/ethers/provider import pkg/ethers/providers/jsonrpc/conversions +import pkg/stew/byteutils suite "JSON Conversions": @@ -119,7 +120,7 @@ suite "JSON Conversions": expect ValueError: discard Log.fromJson(parseJson(json)) - test "getTransactionByHash correctly deserializes 'data' field from 'input' for Transaction": + test "correctly deserializes PastTransaction": let json = %*{ "blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922", "blockNumber":"0x22e", @@ -139,5 +140,18 @@ suite "JSON Conversions": "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" } - let receipt = Transaction.fromJson(json) - check receipt.data.len > 0 + let tx = PastTransaction.fromJson(json) + check tx.blockHash == BlockHash(array[32, byte].fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922")) + check tx.blockNumber == 0x22e.u256 + check tx.sender == Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get + check tx.gas == 0x4d4bb.u256 + check tx.gasPrice == 0x3b9aca07.u256 + check tx.hash == TransactionHash(array[32, byte].fromHex("0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790")) + check tx.input == hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000") + check tx.nonce == 0x3.u256 + check tx.to == Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e").get + check tx.transactionIndex == 0x3.u256 + check tx.value == 0.u256 + check tx.v == 0x181bec.u256 + check tx.r == UInt256.fromBytesBE(hexToSeqByte("0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a")) + check tx.s == UInt256.fromBytesBE(hexToSeqByte("0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"))