Add PastTransaction with serialization and tests, clean up revertReason fetching

This commit is contained in:
Eric 2023-09-21 17:51:59 +10:00
parent f52ce98c6d
commit 35f80e78fe
No known key found for this signature in database
6 changed files with 108 additions and 41 deletions

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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"),
)

View File

@ -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

View File

@ -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"))