On transaction failure, fetch revert reason with replayed transaction (#57)

When transaction fails (receipt.status is Failed), fetch revert reason by replaying transaction.
This commit is contained in:
Eric 2023-10-25 11:36:00 +11:00 committed by GitHub
parent 7eac8410af
commit 2428b756d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 421 additions and 26 deletions

View File

@ -4,6 +4,7 @@ description = "library for interacting with Ethereum"
license = "MIT" license = "MIT"
requires "nim >= 1.6.0" requires "nim >= 1.6.0"
requires "chronicles >= 0.10.3 & < 0.11.0"
requires "chronos >= 3.0.0 & < 4.0.0" requires "chronos >= 3.0.0 & < 4.0.0"
requires "contractabi >= 0.6.0 & < 0.7.0" requires "contractabi >= 0.6.0 & < 0.7.0"
requires "questionable >= 0.10.2 & < 0.11.0" requires "questionable >= 0.10.2 & < 0.11.0"

View File

@ -1,6 +1,7 @@
import std/json import std/json
import std/macros import std/macros
import std/sequtils import std/sequtils
import pkg/chronicles
import pkg/chronos import pkg/chronos
import pkg/contractabi import pkg/contractabi
import ./basics import ./basics
@ -13,6 +14,9 @@ export basics
export provider export provider
export events export events
logScope:
topics = "ethers contract"
type type
Contract* = ref object of RootObj Contract* = ref object of RootObj
provider: Provider provider: Provider

View File

@ -1,3 +1,5 @@
import pkg/chronicles
import pkg/stew/byteutils
import ./basics import ./basics
import ./transaction import ./transaction
import ./blocktag import ./blocktag
@ -47,7 +49,9 @@ type
logs*: seq[Log] logs*: seq[Log]
blockNumber*: ?UInt256 blockNumber*: ?UInt256
cumulativeGasUsed*: UInt256 cumulativeGasUsed*: UInt256
effectiveGasPrice*: ?UInt256
status*: TransactionStatus status*: TransactionStatus
transactionType*: TransactionType
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].} LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
Topic* = array[32, byte] Topic* = array[32, byte]
@ -55,10 +59,43 @@ type
number*: ?UInt256 number*: ?UInt256
timestamp*: UInt256 timestamp*: UInt256
hash*: ?BlockHash 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 EthersDefaultConfirmations* {.intdefine.} = 12
const EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks 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.} = method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
@ -79,6 +116,11 @@ method getTransactionCount*(provider: Provider,
Future[UInt256] {.base, gcsafe.} = Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransaction*(provider: Provider,
txHash: TransactionHash):
Future[?PastTransaction] {.base, gcsafe.} =
doAssert false, "not implemented"
method getTransactionReceipt*(provider: Provider, method getTransactionReceipt*(provider: Provider,
txHash: TransactionHash): txHash: TransactionHash):
Future[?TransactionReceipt] {.base, gcsafe.} = Future[?TransactionReceipt] {.base, gcsafe.} =
@ -94,7 +136,8 @@ method getLogs*(provider: Provider,
doAssert false, "not implemented" doAssert false, "not implemented"
method estimateGas*(provider: Provider, method estimateGas*(provider: Provider,
transaction: Transaction): Future[UInt256] {.base, gcsafe.} = transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getChainId*(provider: Provider): Future[UInt256] {.base, gcsafe.} = method getChainId*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
@ -114,11 +157,73 @@ method subscribe*(provider: Provider,
method unsubscribe*(subscription: Subscription) {.base, async.} = method unsubscribe*(subscription: Subscription) {.base, async.} =
doAssert false, "not implemented" 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, proc confirm*(tx: TransactionResponse,
confirmations = EthersDefaultConfirmations, confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks): timeout = EthersReceiptTimeoutBlks):
Future[TransactionReceipt] Future[TransactionReceipt]
{.async, upraises: [EthersError].} = {.async, upraises: [ProviderError, EthersError].} =
## Waits for a transaction to be mined and for the specified number of blocks ## Waits for a transaction to be mined and for the specified number of blocks
## to pass since it was mined (confirmations). ## to pass since it was mined (confirmations).
## A timeout, in blocks, can be specified that will raise an error if too many ## A timeout, in blocks, can be specified that will raise an error if too many
@ -157,6 +262,7 @@ proc confirm*(tx: TransactionResponse,
if txBlockNumber + confirmations.u256 <= blockNumber + 1: if txBlockNumber + confirmations.u256 <= blockNumber + 1:
await subscription.unsubscribe() await subscription.unsubscribe()
await tx.provider.ensureSuccess(receipt)
return receipt return receipt
proc confirm*(tx: Future[TransactionResponse], proc confirm*(tx: Future[TransactionResponse],

View File

@ -1,6 +1,8 @@
import std/json import std/json
import std/tables import std/tables
import std/uri import std/uri
import pkg/chronicles
import pkg/eth/common/eth_types_json_serialization
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/json_rpc/errors import pkg/json_rpc/errors
import ../basics import ../basics
@ -13,9 +15,13 @@ import ./jsonrpc/subscriptions
export json export json
export basics export basics
export provider export provider
export chronicles
push: {.upraises: [].} push: {.upraises: [].}
logScope:
topics = "ethers jsonrpc"
type type
JsonRpcProvider* = ref object of Provider JsonRpcProvider* = ref object of Provider
client: Future[RpcClient] client: Future[RpcClient]
@ -137,6 +143,13 @@ method getTransactionCount*(provider: JsonRpcProvider,
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag) 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, method getTransactionReceipt*(provider: JsonRpcProvider,
txHash: TransactionHash): txHash: TransactionHash):
Future[?TransactionReceipt] {.async.} = Future[?TransactionReceipt] {.async.} =
@ -164,10 +177,11 @@ method getLogs*(provider: JsonRpcProvider,
return logs return logs
method estimateGas*(provider: JsonRpcProvider, method estimateGas*(provider: JsonRpcProvider,
transaction: Transaction): Future[UInt256] {.async.} = transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.async.} =
convertError: convertError:
let client = await provider.client 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.} = method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
convertError: convertError:

View File

@ -1,4 +1,5 @@
import std/json import std/json
import std/strformat
import std/strutils import std/strutils
import pkg/json_rpc/jsonmarshal import pkg/json_rpc/jsonmarshal
import pkg/stew/byteutils import pkg/stew/byteutils
@ -9,6 +10,16 @@ import ../../provider
export jsonmarshal 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 = func fromJson*(T: type, json: JsonNode, name = ""): T =
fromJson(json, name, result) fromJson(json, name, result)
@ -47,6 +58,15 @@ func `%`*(integer: UInt256): JsonNode =
func fromJson*(json: JsonNode, name: string, result: var UInt256) = func fromJson*(json: JsonNode, name: string, result: var UInt256) =
result = UInt256.fromHex(json.getStr()) 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 # Transaction
func `%`*(transaction: Transaction): JsonNode = func `%`*(transaction: Transaction): JsonNode =
@ -70,6 +90,9 @@ func `%`*(blockTag: BlockTag): JsonNode =
# Log # Log
func fromJson*(json: JsonNode, name: string, result: var 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 data: seq[byte]
var topics: seq[Topic] var topics: seq[Topic]
fromJson(json["data"], "data", data) fromJson(json["data"], "data", data)
@ -83,4 +106,83 @@ func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) =
result = TransactionStatus(val) result = TransactionStatus(val)
func `%`*(status: TransactionStatus): JsonNode = 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")
)

View File

@ -5,9 +5,10 @@ proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte]
proc eth_gasPrice(): UInt256 proc eth_gasPrice(): UInt256
proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode
proc eth_getTransactionByHash(hash: TransactionHash): ?PastTransaction
proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256 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_chainId(): UInt256
proc eth_sendTransaction(transaction: Transaction): TransactionHash proc eth_sendTransaction(transaction: Transaction): TransactionHash
proc eth_sendRawTransaction(data: seq[byte]): TransactionHash proc eth_sendRawTransaction(data: seq[byte]): TransactionHash

View File

@ -50,7 +50,8 @@ method getTransactionCount*(signer: Signer,
return await signer.provider.getTransactionCount(address, blockTag) return await signer.provider.getTransactionCount(address, blockTag)
method estimateGas*(signer: Signer, method estimateGas*(signer: Signer,
transaction: Transaction): Future[UInt256] {.base, async.} = transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, async.} =
var transaction = transaction var transaction = transaction
transaction.sender = some(await signer.getAddress) transaction.sender = some(await signer.getAddress)
try: try:
@ -106,7 +107,7 @@ method populateTransaction*(signer: Signer,
populated.sender = some(await signer.getAddress()) populated.sender = some(await signer.getAddress())
if transaction.chainId.isNone: if transaction.chainId.isNone:
populated.chainId = some(await signer.getChainId()) populated.chainId = some(await signer.getChainId())
if transaction.gasPrice.isNone and (populated.maxFee.isNone or populated.maxPriorityFee.isNone): if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone):
populated.gasPrice = some(await signer.getGasPrice()) populated.gasPrice = some(await signer.getGasPrice())
if transaction.nonce.isNone and transaction.gasLimit.isNone: if transaction.nonce.isNone and transaction.gasLimit.isNone:

View File

@ -1,17 +1,23 @@
import pkg/stew/byteutils import pkg/stew/byteutils
import ./basics import ./basics
type Transaction* = object type
sender*: ?Address TransactionType* = enum
to*: Address Legacy = 0,
data*: seq[byte] AccessList = 1,
value*: UInt256 Dynamic = 2
nonce*: ?UInt256 Transaction* = object
chainId*: ?UInt256 sender*: ?Address
gasPrice*: ?UInt256 to*: Address
maxFee*: ?UInt256 data*: seq[byte]
maxPriorityFee*: ?UInt256 value*: UInt256
gasLimit*: ?UInt256 nonce*: ?UInt256
chainId*: ?UInt256
gasPrice*: ?UInt256
maxFee*: ?UInt256
maxPriorityFee*: ?UInt256
gasLimit*: ?UInt256
transactionType*: ?TransactionType
func `$`*(transaction: Transaction): string = func `$`*(transaction: Transaction): string =
result = "(" result = "("
@ -19,7 +25,7 @@ func `$`*(transaction: Transaction): string =
result &= "from: " & $sender & ", " result &= "from: " & $sender & ", "
result &= "to: " & $transaction.to & ", " result &= "to: " & $transaction.to & ", "
result &= "value: " & $transaction.value & ", " result &= "value: " & $transaction.value & ", "
result &= "data: 0x" & $transaction.data.toHex result &= "data: 0x" & $(transaction.data.toHex)
if nonce =? transaction.nonce: if nonce =? transaction.nonce:
result &= ", nonce: " & $nonce result &= ", nonce: " & $nonce
if chainId =? transaction.chainId: if chainId =? transaction.chainId:
@ -28,4 +34,6 @@ func `$`*(transaction: Transaction): string =
result &= ", gasPrice: " & $gasPrice result &= ", gasPrice: " & $gasPrice
if gasLimit =? transaction.gasLimit: if gasLimit =? transaction.gasLimit:
result &= ", gasLimit: " & $gasLimit result &= ", gasLimit: " & $gasLimit
if txType =? transaction.transactionType:
result &= ", type: " & $txType
result &= ")" result &= ")"

View File

@ -8,6 +8,7 @@ import ./wallet/signing
export keys export keys
export WalletError export WalletError
export signing
var rng {.threadvar.}: ref HmacDrbgContext var rng {.threadvar.}: ref HmacDrbgContext

View File

@ -1,8 +1,10 @@
import pkg/eth/keys import pkg/eth/keys
import pkg/eth/rlp import pkg/eth/rlp
import pkg/eth/common/transaction as eth import pkg/eth/common/transaction as eth
import pkg/eth/common/eth_hash
import ../basics import ../basics
import ../transaction as ethers import ../transaction as ethers
import ../provider
import ./error import ./error
type type
@ -62,3 +64,6 @@ func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] =
func sign*(key: PrivateKey, transaction: Transaction): seq[byte] = func sign*(key: PrivateKey, transaction: Transaction): seq[byte] =
key.sign(transaction.toSignableTransaction()) key.sign(transaction.toSignableTransaction())
func toTransactionHash*(bytes: seq[byte]): TransactionHash =
TransactionHash(bytes.keccakHash.data)

View File

@ -1,6 +1,12 @@
import std/strutils
import std/unittest import std/unittest
import pkg/ethers/provider import pkg/ethers/provider
import pkg/ethers/providers/jsonrpc/conversions import pkg/ethers/providers/jsonrpc/conversions
import pkg/stew/byteutils
func flatten(s: string): string =
s.replace(" ")
.replace("\n")
suite "JSON Conversions": suite "JSON Conversions":
@ -36,7 +42,7 @@ suite "JSON Conversions":
test "missing block number in TransactionReceipt isNone": test "missing block number in TransactionReceipt isNone":
var json = %*{ var json = %*{
"sender": newJNull(), "from": newJNull(),
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"contractAddress": newJNull(), "contractAddress": newJNull(),
"transactionIndex": "0x0", "transactionIndex": "0x0",
@ -56,7 +62,9 @@ suite "JSON Conversions":
], ],
"blockNumber": newJNull(), "blockNumber": newJNull(),
"cumulativeGasUsed": "0x10db1", "cumulativeGasUsed": "0x10db1",
"status": "0000000000000001" "status": "0x1",
"effectiveGasPrice": "0x3b9aca08",
"type": "0x0"
} }
var receipt = TransactionReceipt.fromJson(json) var receipt = TransactionReceipt.fromJson(json)
@ -69,7 +77,7 @@ suite "JSON Conversions":
test "missing block hash in TransactionReceipt isNone": test "missing block hash in TransactionReceipt isNone":
let json = %*{ let json = %*{
"sender": newJNull(), "from": newJNull(),
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"contractAddress": newJNull(), "contractAddress": newJNull(),
"transactionIndex": "0x0", "transactionIndex": "0x0",
@ -89,8 +97,143 @@ suite "JSON Conversions":
], ],
"blockNumber": newJNull(), "blockNumber": newJNull(),
"cumulativeGasUsed": "0x10db1", "cumulativeGasUsed": "0x10db1",
"status": "0000000000000001" "status": "0x1",
"effectiveGasPrice": "0x3b9aca08",
"type": "0x0"
} }
let receipt = TransactionReceipt.fromJson(json) let receipt = TransactionReceipt.fromJson(json)
check receipt.blockHash.isNone check receipt.blockHash.isNone
test "newHeads subcription raises exception when deserializing to Log":
let json = """{
"parentHash":"0xd68d4d0f29307df51e1284fc8a13595ae700ef0f1128830a69e6854381363d42",
"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"miner":"0x0000000000000000000000000000000000000000",
"stateRoot":"0x1f6f2d05de35bbfd50213be96ddf960d62b978b472c55d6ac223cd648cbbbbb0",
"transactionsRoot":"0xb9bb8a26abe091bb628ab2b6585c5af151aeb3984f4ba47a3c65d438283e069d",
"receiptsRoot":"0x33f229b7133e1ba3fb524b8af22d8184ca10b2da5bb170092a219c61ca023c1d",
"logsBloom":"0x00000000000000000000000000000000000000000020000000000002000000000000000000000000000000000000000000000000000008080000100200200000000000000000000000000008000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000010000040000000000100000000000800000000000000000000000000000000020000000000020000000000000000000000000000040000008000000000000000000020000000000002000000000000000000000000000000000000000000000000000001000010000000000000000020002000000020000000000000008002000000000000",
"difficulty":"0x2",
"number":"0x21d",
"gasLimit":"0x1c1b59a7",
"gasUsed":"0xda41b",
"timestamp":"0x6509410e",
"extraData":"0xd883010b05846765746888676f312e32302e32856c696e7578000000000000007102a27d75709b90ca9eb23cdaaccf4fc2d571d710f3bc5a7dc874f43af116a93ff832576a53c16f0d0aa1cd9e9a1dc0a60126c4d420f72b0866fc96ba6664f601",
"mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x0000000000000000",
"baseFeePerGas":"0x7",
"withdrawalsRoot":null,
"hash":"0x64066c7150c660e5357c4b6b02d836c10353dfa8edb32c805fca9367fd29c6e7"
}"""
expect ValueError:
discard Log.fromJson(parseJson(json))
test "correctly deserializes PastTransaction":
let json = %*{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
"blockNumber":"0x22e",
"from":"0xe00b677c29ff8d8fe6068530e2bc36158c54dd34",
"gas":"0x4d4bb",
"gasPrice":"0x3b9aca07",
"hash":"0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790",
"input":"0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3",
"value":"0x0",
"type":"0x0",
"chainId":"0xc0de4",
"v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
}
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.transactionType == some TransactionType.Legacy
check tx.chainId == some 0xc0de4.u256
check tx.v == 0x181bec.u256
check tx.r == UInt256.fromBytesBE(hexToSeqByte("0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a"))
check tx.s == UInt256.fromBytesBE(hexToSeqByte("0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"))
test "PastTransaction serializes correctly":
let tx = PastTransaction(
blockHash: BlockHash(array[32, byte].fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922")),
blockNumber: 0x22e.u256,
sender: Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get,
gas: 0x4d4bb.u256,
gasPrice: 0x3b9aca07.u256,
hash: TransactionHash(array[32, byte].fromHex("0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790")),
input: hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"),
nonce: 0x3.u256,
to: Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e").get,
transactionIndex: 0x3.u256,
value: 0.u256,
v: 0x181bec.u256,
r: UInt256.fromBytesBE(hexToSeqByte("0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a")),
s: UInt256.fromBytesBE(hexToSeqByte("0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2")),
transactionType: some TransactionType.Legacy,
chainId: some 0xc0de4.u256
)
let expected = """
{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
"blockNumber":"0x22e",
"from":"0xe00b677c29ff8d8fe6068530e2bc36158c54dd34",
"gas":"0x4d4bb",
"gasPrice":"0x3b9aca07",
"hash":"0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790",
"input":"0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3",
"value":"0x0",
"v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2",
"type":"0x0",
"chainId":"0xc0de4"
}""".flatten
check $(%tx) == expected
test "correctly converts PastTransaction to Transaction":
let json = %*{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
"blockNumber":"0x22e",
"from":"0xe00b677c29ff8d8fe6068530e2bc36158c54dd34",
"gas":"0x52277",
"gasPrice":"0x3b9aca07",
"hash":"0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790",
"input":"0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3",
"value":"0x0",
"type":"0x0",
"chainId":"0xc0de4",
"v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
}
let past = PastTransaction.fromJson(json)
check %past.toTransaction == %*{
"to": !Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e"),
"data": hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"),
"from": !Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34"),
"nonce": 0x3.u256,
"chainId": 0xc0de4.u256,
"gasPrice": 0x3b9aca07.u256,
"gas": 0x52277.u256
}

View File

@ -96,5 +96,14 @@ suite "Testing helpers - contracts":
discard await provider.send("evm_revert", @[snapshot]) discard await provider.send("evm_revert", @[snapshot])
await provider.close() await provider.close()
test "revert works with provider": test "revert reason can be retrieved when transaction fails":
check await helpersContract.doRevert(revertReason).reverts(revertReason) let txResp = helpersContract.doRevert(
revertReason,
# override gasLimit to skip estimating gas
TransactionOverrides(gasLimit: some 10000000.u256)
)
check await txResp.confirm(1).reverts(revertReason)
test "revert reason can be retrieved when estimate gas fails":
let txResp = helpersContract.doRevert(revertReason)
check await txResp.reverts(revertReason)