diff --git a/ethers.nimble b/ethers.nimble index f9b1c5c..5548718 100644 --- a/ethers.nimble +++ b/ethers.nimble @@ -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" diff --git a/ethers/contract.nim b/ethers/contract.nim index c590313..1bef12d 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -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 diff --git a/ethers/provider.nim b/ethers/provider.nim index 97f6fee..f46d948 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -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], diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 505944d..96941a8 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -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: diff --git a/ethers/providers/jsonrpc/conversions.nim b/ethers/providers/jsonrpc/conversions.nim index 1d93bf3..6f89542 100644 --- a/ethers/providers/jsonrpc/conversions.nim +++ b/ethers/providers/jsonrpc/conversions.nim @@ -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") + ) diff --git a/ethers/providers/jsonrpc/signatures.nim b/ethers/providers/jsonrpc/signatures.nim index 63bc395..668acbc 100644 --- a/ethers/providers/jsonrpc/signatures.nim +++ b/ethers/providers/jsonrpc/signatures.nim @@ -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 diff --git a/ethers/signer.nim b/ethers/signer.nim index 089600c..c86f182 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -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: diff --git a/ethers/transaction.nim b/ethers/transaction.nim index 92652af..0ee9d2a 100644 --- a/ethers/transaction.nim +++ b/ethers/transaction.nim @@ -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 &= ")" diff --git a/ethers/wallet.nim b/ethers/wallet.nim index 72177be..85305f6 100644 --- a/ethers/wallet.nim +++ b/ethers/wallet.nim @@ -8,6 +8,7 @@ import ./wallet/signing export keys export WalletError +export signing var rng {.threadvar.}: ref HmacDrbgContext diff --git a/ethers/wallet/signing.nim b/ethers/wallet/signing.nim index b56e406..2ea9fbf 100644 --- a/ethers/wallet/signing.nim +++ b/ethers/wallet/signing.nim @@ -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) diff --git a/testmodule/providers/jsonrpc/testConversions.nim b/testmodule/providers/jsonrpc/testConversions.nim index 065729f..37ffb2b 100644 --- a/testmodule/providers/jsonrpc/testConversions.nim +++ b/testmodule/providers/jsonrpc/testConversions.nim @@ -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 + } diff --git a/testmodule/testTesting.nim b/testmodule/testTesting.nim index ec1ba82..8fec151 100644 --- a/testmodule/testTesting.nim +++ b/testmodule/testTesting.nim @@ -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)