mirror of
https://github.com/logos-storage/nim-ethers.git
synced 2026-01-10 01:23:05 +00:00
Add PastTransaction with serialization and tests, clean up revertReason fetching
This commit is contained in:
parent
f52ce98c6d
commit
35f80e78fe
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"),
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user