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"
|
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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 &= ")"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue