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:
parent
7eac8410af
commit
2428b756d6
|
@ -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"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import std/json
|
||||
import std/macros
|
||||
import std/sequtils
|
||||
import pkg/chronicles
|
||||
import pkg/chronos
|
||||
import pkg/contractabi
|
||||
import ./basics
|
||||
|
@ -13,6 +14,9 @@ export basics
|
|||
export provider
|
||||
export events
|
||||
|
||||
logScope:
|
||||
topics = "ethers contract"
|
||||
|
||||
type
|
||||
Contract* = ref object of RootObj
|
||||
provider: Provider
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import pkg/chronicles
|
||||
import pkg/stew/byteutils
|
||||
import ./basics
|
||||
import ./transaction
|
||||
import ./blocktag
|
||||
|
@ -47,7 +49,9 @@ 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]
|
||||
|
@ -55,10 +59,43 @@ 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
|
||||
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"
|
||||
|
||||
|
@ -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.} =
|
||||
|
@ -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.} =
|
||||
|
@ -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
|
||||
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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.} =
|
||||
|
@ -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:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import std/json
|
||||
import std/strformat
|
||||
import std/strutils
|
||||
import pkg/json_rpc/jsonmarshal
|
||||
import pkg/stew/byteutils
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -50,7 +50,8 @@ method getTransactionCount*(signer: Signer,
|
|||
return await signer.provider.getTransactionCount(address, blockTag)
|
||||
|
||||
method estimateGas*(signer: Signer,
|
||||
transaction: Transaction): Future[UInt256] {.base, async.} =
|
||||
transaction: Transaction,
|
||||
blockTag = BlockTag.latest): Future[UInt256] {.base, async.} =
|
||||
var transaction = transaction
|
||||
transaction.sender = some(await signer.getAddress)
|
||||
try:
|
||||
|
@ -106,7 +107,7 @@ method populateTransaction*(signer: Signer,
|
|||
populated.sender = some(await signer.getAddress())
|
||||
if transaction.chainId.isNone:
|
||||
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())
|
||||
|
||||
if transaction.nonce.isNone and transaction.gasLimit.isNone:
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
import pkg/stew/byteutils
|
||||
import ./basics
|
||||
|
||||
type Transaction* = object
|
||||
sender*: ?Address
|
||||
to*: Address
|
||||
data*: seq[byte]
|
||||
value*: UInt256
|
||||
nonce*: ?UInt256
|
||||
chainId*: ?UInt256
|
||||
gasPrice*: ?UInt256
|
||||
maxFee*: ?UInt256
|
||||
maxPriorityFee*: ?UInt256
|
||||
gasLimit*: ?UInt256
|
||||
type
|
||||
TransactionType* = enum
|
||||
Legacy = 0,
|
||||
AccessList = 1,
|
||||
Dynamic = 2
|
||||
Transaction* = object
|
||||
sender*: ?Address
|
||||
to*: Address
|
||||
data*: seq[byte]
|
||||
value*: UInt256
|
||||
nonce*: ?UInt256
|
||||
chainId*: ?UInt256
|
||||
gasPrice*: ?UInt256
|
||||
maxFee*: ?UInt256
|
||||
maxPriorityFee*: ?UInt256
|
||||
gasLimit*: ?UInt256
|
||||
transactionType*: ?TransactionType
|
||||
|
||||
func `$`*(transaction: Transaction): string =
|
||||
result = "("
|
||||
|
@ -19,7 +25,7 @@ func `$`*(transaction: Transaction): string =
|
|||
result &= "from: " & $sender & ", "
|
||||
result &= "to: " & $transaction.to & ", "
|
||||
result &= "value: " & $transaction.value & ", "
|
||||
result &= "data: 0x" & $transaction.data.toHex
|
||||
result &= "data: 0x" & $(transaction.data.toHex)
|
||||
if nonce =? transaction.nonce:
|
||||
result &= ", nonce: " & $nonce
|
||||
if chainId =? transaction.chainId:
|
||||
|
@ -28,4 +34,6 @@ func `$`*(transaction: Transaction): string =
|
|||
result &= ", gasPrice: " & $gasPrice
|
||||
if gasLimit =? transaction.gasLimit:
|
||||
result &= ", gasLimit: " & $gasLimit
|
||||
if txType =? transaction.transactionType:
|
||||
result &= ", type: " & $txType
|
||||
result &= ")"
|
||||
|
|
|
@ -8,6 +8,7 @@ import ./wallet/signing
|
|||
|
||||
export keys
|
||||
export WalletError
|
||||
export signing
|
||||
|
||||
var rng {.threadvar.}: ref HmacDrbgContext
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import pkg/eth/keys
|
||||
import pkg/eth/rlp
|
||||
import pkg/eth/common/transaction as eth
|
||||
import pkg/eth/common/eth_hash
|
||||
import ../basics
|
||||
import ../transaction as ethers
|
||||
import ../provider
|
||||
import ./error
|
||||
|
||||
type
|
||||
|
@ -62,3 +64,6 @@ func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] =
|
|||
|
||||
func sign*(key: PrivateKey, transaction: Transaction): seq[byte] =
|
||||
key.sign(transaction.toSignableTransaction())
|
||||
|
||||
func toTransactionHash*(bytes: seq[byte]): TransactionHash =
|
||||
TransactionHash(bytes.keccakHash.data)
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import std/strutils
|
||||
import std/unittest
|
||||
import pkg/ethers/provider
|
||||
import pkg/ethers/providers/jsonrpc/conversions
|
||||
import pkg/stew/byteutils
|
||||
|
||||
func flatten(s: string): string =
|
||||
s.replace(" ")
|
||||
.replace("\n")
|
||||
|
||||
suite "JSON Conversions":
|
||||
|
||||
|
@ -36,7 +42,7 @@ suite "JSON Conversions":
|
|||
|
||||
test "missing block number in TransactionReceipt isNone":
|
||||
var json = %*{
|
||||
"sender": newJNull(),
|
||||
"from": newJNull(),
|
||||
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
|
||||
"contractAddress": newJNull(),
|
||||
"transactionIndex": "0x0",
|
||||
|
@ -56,7 +62,9 @@ suite "JSON Conversions":
|
|||
],
|
||||
"blockNumber": newJNull(),
|
||||
"cumulativeGasUsed": "0x10db1",
|
||||
"status": "0000000000000001"
|
||||
"status": "0x1",
|
||||
"effectiveGasPrice": "0x3b9aca08",
|
||||
"type": "0x0"
|
||||
}
|
||||
|
||||
var receipt = TransactionReceipt.fromJson(json)
|
||||
|
@ -69,7 +77,7 @@ suite "JSON Conversions":
|
|||
|
||||
test "missing block hash in TransactionReceipt isNone":
|
||||
let json = %*{
|
||||
"sender": newJNull(),
|
||||
"from": newJNull(),
|
||||
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
|
||||
"contractAddress": newJNull(),
|
||||
"transactionIndex": "0x0",
|
||||
|
@ -89,8 +97,143 @@ suite "JSON Conversions":
|
|||
],
|
||||
"blockNumber": newJNull(),
|
||||
"cumulativeGasUsed": "0x10db1",
|
||||
"status": "0000000000000001"
|
||||
"status": "0x1",
|
||||
"effectiveGasPrice": "0x3b9aca08",
|
||||
"type": "0x0"
|
||||
}
|
||||
|
||||
let receipt = TransactionReceipt.fromJson(json)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -96,5 +96,14 @@ suite "Testing helpers - contracts":
|
|||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
test "revert works with provider":
|
||||
check await helpersContract.doRevert(revertReason).reverts(revertReason)
|
||||
test "revert reason can be retrieved when transaction fails":
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue