From 43500c63d785537d60865f96b26148353de1eee5 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:50:46 +1100 Subject: [PATCH] Upgrade to `nim-json-rpc` v0.4.2 and chronos v4 (#64) * Add json de/serialization lib from codex to handle conversions json-rpc now requires nim-json-serialization to convert types to/from json. Use the nim-json-serialization signatures to call the json serialization lib from nim-codex (should be moved to its own lib) * Add ethers implementation for setMethodHandler Was removed in json-rpc * More json conversion updates * Fix json_rpc.call returning JsonString instead of JsonNode * Update exceptions Use {.async: (raises: [...].} where needed Annotate provider with {.push raises:[].} Format signatures * Start fixing tests (mainly conversion fixes) * rename sender to `from`, update json error logging, add more conversions * Refactor exceptions for providers and signers, fix more tests - signer procs raise SignerError, provider procs raise ProviderError - WalletError now inherits from SignerError - move wallet module under signers - create jsonrpo moudle under signers - bump nim-json-rpc for null-handling fixes - All jsonrpc provider tests passing, still need to fix others * remove raises from async annotation for dynamic dispatch - removes async: raises from getAddress and signTransaction because derived JsonRpcSigner methods were not being used when dynamically dispatched. Once `raises` was removed from the async annotation, the dynamic dispatch worked again. This is only the case for getAddress and signTransaction. - add gcsafe annotation to wallet.provider so that it matches the base method * Catch EstimateGasError before ProviderError EstimateGasError is now a ProviderError (it is a SignerError, and SignerError is a ProviderError), so EstimateGasErrors were not being caught * clean up - all tests passing * support nim 2.0 * lock in chronos version * Add serde options to the json util, along with tests next step is to: 1. change back any ethers var names that were changed for serialization purposes, eg `from` and `type` 2. move the json util to its own lib * bump json-rpc to 0.4.0 and fix test * fix: specify raises for getAddress and sendTransaction Fixes issue where getAddress and sendTransaction could not be found for MockSigner in tests. The problem was that the async: raises update had not been applied to the MockSigner. * handle exceptions during jsonrpc init There are too many exceptions to catch individually, including chronos raising CatchableError exceptions in await expansion. There are also many other errors captured inside of the new proc with CatchableError. Instead of making it more complicated and harder to read, I think sticking with excepting CatchableError inside of convertError is a sensible solution * cleanup * deserialize key defaults to serialize key * Add more tests for OptIn/OptOut/Strict modes, fix logic * use nim-serde instead of json util Allows aliasing of de/serialized fields, so revert changes of sender to `from` and transactionType to `type` * Move hash* shim to its own module * address PR feedback - add comments to hashes shim - remove .catch from callback condition - derive SignerError from EthersError instead of ProviderError. This allows Providers and Signers to be separate, as Ledger does it, to isolate functionality. Some signer functions now raise both ProviderError and SignerError - Update reverts to check for SignerError - Update ERC-20 method comment * rename subscriptions.init > subscriptions.start --- ethers.nimble | 5 +- ethers/erc20.nim | 2 +- ethers/nimshims/hashes.nim | 49 ++++ ethers/provider.nim | 160 +++++++----- ethers/providers/jsonrpc.nim | 237 ++++++++++++------ ethers/providers/jsonrpc/conversions.nim | 208 ++++++--------- ethers/providers/jsonrpc/looping.nim | 4 +- ethers/providers/jsonrpc/subscriptions.nim | 59 ++++- ethers/signer.nim | 112 ++++++--- ethers/signers/jsonrpc.nim | 3 + ethers/signers/wallet.nim | 92 +++++++ ethers/signers/wallet/error.nim | 13 + ethers/{ => signers}/wallet/signing.nim | 6 +- ethers/testing.nim | 4 +- ethers/transaction.nim | 7 +- ethers/wallet.nim | 77 +----- ethers/wallet/error.nim | 7 - testmodule/mocks.nim | 13 +- .../providers/jsonrpc/testConversions.nim | 68 ++--- .../providers/jsonrpc/testJsonRpcProvider.nim | 3 +- .../jsonrpc/testJsonRpcSubscriptions.nim | 2 + testmodule/testEnums.nim | 1 + testmodule/testReturns.nim | 1 + testmodule/testTesting.nim | 7 + testmodule/testWallet.nim | 1 + 25 files changed, 671 insertions(+), 470 deletions(-) create mode 100644 ethers/nimshims/hashes.nim create mode 100644 ethers/signers/jsonrpc.nim create mode 100644 ethers/signers/wallet.nim create mode 100644 ethers/signers/wallet/error.nim rename ethers/{ => signers}/wallet/signing.nim (96%) delete mode 100644 ethers/wallet/error.nim diff --git a/ethers.nimble b/ethers.nimble index 14d96e2..e841f74 100644 --- a/ethers.nimble +++ b/ethers.nimble @@ -5,10 +5,11 @@ license = "MIT" requires "nim >= 1.6.0" requires "chronicles >= 0.10.3 & < 0.11.0" -requires "chronos >= 3.0.0 & < 4.0.0" +requires "chronos >= 4.0.0 & < 4.1.0" requires "contractabi >= 0.6.0 & < 0.7.0" requires "questionable >= 0.10.2 & < 0.11.0" -requires "json_rpc" +requires "json_rpc >= 0.4.0 & < 0.5.0" +requires "serde >= 0.1.1 & < 0.2.0" requires "stint" requires "stew" requires "eth" diff --git a/ethers/erc20.nim b/ethers/erc20.nim index 57f0cbf..5b49275 100644 --- a/ethers/erc20.nim +++ b/ethers/erc20.nim @@ -76,5 +76,5 @@ method transferFrom*(token: Erc20Token, spender: Address, recipient: Address, amount: UInt256): ?TransactionResponse {.base, contract.} - ## Moves `amount` tokens from `from` to `to` using the allowance + ## Moves `amount` tokens from `spender` to `recipient` using the allowance ## mechanism. `amount` is then deducted from the caller's allowance. diff --git a/ethers/nimshims/hashes.nim b/ethers/nimshims/hashes.nim new file mode 100644 index 0000000..c691d9e --- /dev/null +++ b/ethers/nimshims/hashes.nim @@ -0,0 +1,49 @@ +## Fixes an underlying Exception caused by missing forward declarations for +## `std/json.JsonNode.hash`, eg when using `JsonNode` as a `Table` key. Adds +## {.raises: [].} for proper exception tracking. Copied from the std/json module + +import std/json +import std/hashes + +{.push raises:[].} + +when (NimMajor) >= 2: + proc hash*[A](x: openArray[A]): Hash = + ## Efficient hashing of arrays and sequences. + ## There must be a `hash` proc defined for the element type `A`. + when A is byte: + result = murmurHash(x) + elif A is char: + when nimvm: + result = hashVmImplChar(x, 0, x.high) + else: + result = murmurHash(toOpenArrayByte(x, 0, x.high)) + else: + for a in x: + result = result !& hash(a) + result = !$result + +func hash*(n: OrderedTable[string, JsonNode]): Hash + +func hash*(n: JsonNode): Hash = + ## Compute the hash for a JSON node + case n.kind + of JArray: + result = hash(n.elems) + of JObject: + result = hash(n.fields) + of JInt: + result = hash(n.num) + of JFloat: + result = hash(n.fnum) + of JBool: + result = hash(n.bval.int) + of JString: + result = hash(n.str) + of JNull: + result = Hash(0) + +func hash*(n: OrderedTable[string, JsonNode]): Hash = + for key, val in n: + result = result xor (hash(key) !& hash(val)) + result = !$result \ No newline at end of file diff --git a/ethers/provider.nim b/ethers/provider.nim index 6cc3058..1bf2363 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -1,4 +1,5 @@ import pkg/chronicles +import pkg/serde import pkg/stew/byteutils import ./basics import ./transaction @@ -14,15 +15,15 @@ type Provider* = ref object of RootObj ProviderError* = object of EthersError Subscription* = ref object of RootObj - EventFilter* = ref object of RootObj + EventFilter* {.serialize.} = ref object of RootObj address*: Address topics*: seq[Topic] - Filter* = ref object of EventFilter + Filter* {.serialize.} = ref object of EventFilter fromBlock*: BlockTag toBlock*: BlockTag - FilterByBlockHash* = ref object of EventFilter + FilterByBlockHash* {.serialize.} = ref object of EventFilter blockHash*: BlockHash - Log* = object + Log* {.serialize.} = object blockNumber*: UInt256 data*: seq[byte] logIndex*: UInt256 @@ -36,9 +37,9 @@ type Invalid = 2 TransactionResponse* = object provider*: Provider - hash*: TransactionHash - TransactionReceipt* = object - sender*: ?Address + hash* {.serialize.}: TransactionHash + TransactionReceipt* {.serialize.} = object + sender* {.serialize("from"), deserialize("from").}: ?Address to*: ?Address contractAddress*: ?Address transactionIndex*: UInt256 @@ -51,18 +52,18 @@ type cumulativeGasUsed*: UInt256 effectiveGasPrice*: ?UInt256 status*: TransactionStatus - transactionType*: TransactionType + transactionType* {.serialize("type"), deserialize("type").}: TransactionType LogHandler* = proc(log: Log) {.gcsafe, raises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].} Topic* = array[32, byte] - Block* = object + Block* {.serialize.} = object number*: ?UInt256 timestamp*: UInt256 hash*: ?BlockHash - PastTransaction* = object + PastTransaction* {.serialize.} = object blockHash*: BlockHash blockNumber*: UInt256 - sender*: Address + sender* {.serialize("from"), deserialize("from").}: Address gas*: UInt256 gasPrice*: UInt256 hash*: TransactionHash @@ -70,7 +71,7 @@ type nonce*: UInt256 to*: Address transactionIndex*: UInt256 - transactionType*: ?TransactionType + transactionType* {.serialize("type"), deserialize("type").}: ?TransactionType chainId*: ?UInt256 value*: UInt256 v*, r*, s*: UInt256 @@ -87,77 +88,104 @@ template raiseProviderError(msg: string) = 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, + data: past.input, + value: past.value, + nonce: some past.nonce, + chainId: past.chainId, + gasPrice: some past.gasPrice, gasLimit: some past.gas, - chainId: past.chainId + transactionType: past.transactionType ) -method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} = +method getBlockNumber*( + provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getBlock*(provider: Provider, tag: BlockTag): Future[?Block] {.base, gcsafe.} = +method getBlock*( + provider: Provider, + tag: BlockTag): Future[?Block] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method call*(provider: Provider, - tx: Transaction, - blockTag = BlockTag.latest): Future[seq[byte]] {.base, gcsafe.} = +method call*( + provider: Provider, + tx: Transaction, + blockTag = BlockTag.latest): Future[seq[byte]] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getGasPrice*(provider: Provider): Future[UInt256] {.base, gcsafe.} = +method getGasPrice*( + provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getTransactionCount*(provider: Provider, - address: Address, - blockTag = BlockTag.latest): - Future[UInt256] {.base, gcsafe.} = +method getTransactionCount*( + provider: Provider, + address: Address, + blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getTransaction*(provider: Provider, - txHash: TransactionHash): - Future[?PastTransaction] {.base, gcsafe.} = +method getTransaction*( + provider: Provider, + txHash: TransactionHash): Future[?PastTransaction] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getTransactionReceipt*(provider: Provider, - txHash: TransactionHash): - Future[?TransactionReceipt] {.base, gcsafe.} = +method getTransactionReceipt*( + provider: Provider, + txHash: TransactionHash): Future[?TransactionReceipt] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method sendTransaction*(provider: Provider, - rawTransaction: seq[byte]): - Future[TransactionResponse] {.base, gcsafe.} = +method sendTransaction*( + provider: Provider, + rawTransaction: seq[byte]): Future[TransactionResponse] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getLogs*(provider: Provider, - filter: EventFilter): Future[seq[Log]] {.base, gcsafe.} = +method getLogs*( + provider: Provider, + filter: EventFilter): Future[seq[Log]] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method estimateGas*(provider: Provider, - transaction: Transaction, - blockTag = BlockTag.latest): Future[UInt256] {.base, gcsafe.} = +method estimateGas*( + provider: Provider, + transaction: Transaction, + blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method getChainId*(provider: Provider): Future[UInt256] {.base, gcsafe.} = +method getChainId*( + provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method subscribe*(provider: Provider, - filter: EventFilter, - callback: LogHandler): - Future[Subscription] {.base, gcsafe.} = +method subscribe*( + provider: Provider, + filter: EventFilter, + callback: LogHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method subscribe*(provider: Provider, - callback: BlockHandler): - Future[Subscription] {.base, gcsafe.} = +method subscribe*( + provider: Provider, + callback: BlockHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -method unsubscribe*(subscription: Subscription) {.base, async.} = +method unsubscribe*( + subscription: Subscription) {.base, async: (raises:[ProviderError]).} = + doAssert false, "not implemented" -proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async.} = +proc replay*( + provider: Provider, + tx: Transaction, + blockNumber: UInt256) {.async: (raises:[ProviderError]).} = # 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 @@ -171,8 +199,7 @@ proc replay*(provider: Provider, tx: Transaction, blockNumber: UInt256) {.async. method getRevertReason*( provider: Provider, hash: TransactionHash, - blockNumber: UInt256 -): Future[?string] {.base, async.} = + blockNumber: UInt256): Future[?string] {.base, async: (raises: [ProviderError]).} = without pastTx =? await provider.getTransaction(hash): return none string @@ -186,8 +213,7 @@ method getRevertReason*( method getRevertReason*( provider: Provider, - receipt: TransactionReceipt -): Future[?string] {.base, async.} = + receipt: TransactionReceipt): Future[?string] {.base, async: (raises: [ProviderError]).} = if receipt.status != TransactionStatus.Failure: return none string @@ -199,8 +225,7 @@ method getRevertReason*( proc ensureSuccess( provider: Provider, - receipt: TransactionReceipt -) {.async, raises: [ProviderError].} = + receipt: TransactionReceipt) {.async: (raises: [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 @@ -219,11 +244,12 @@ proc ensureSuccess( 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, raises: [ProviderError, EthersError].} = +proc confirm*( + tx: TransactionResponse, + confirmations = EthersDefaultConfirmations, + timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt] + {.async: (raises: [CancelledError, 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 @@ -265,10 +291,10 @@ proc confirm*(tx: TransactionResponse, await tx.provider.ensureSuccess(receipt) return receipt -proc confirm*(tx: Future[TransactionResponse], - confirmations: int = EthersDefaultConfirmations, - timeout: int = EthersReceiptTimeoutBlks): - Future[TransactionReceipt] {.async.} = +proc confirm*( + tx: Future[TransactionResponse], + confirmations: int = EthersDefaultConfirmations, + timeout: int = EthersReceiptTimeoutBlks): Future[TransactionReceipt] {.async.} = ## Convenience method that allows wait to be chained to a sendTransaction ## call, eg: ## `await signer.sendTransaction(populated).confirm(3)` @@ -276,5 +302,5 @@ proc confirm*(tx: Future[TransactionResponse], let txResp = await tx return await txResp.confirm(confirmations, timeout) -method close*(provider: Provider) {.async, base.} = +method close*(provider: Provider) {.base, async: (raises:[ProviderError]).} = discard diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 1784da0..729bec5 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -1,10 +1,10 @@ -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 pkg/serde import ../basics import ../provider import ../signer @@ -12,7 +12,6 @@ import ./jsonrpc/rpccalls import ./jsonrpc/conversions import ./jsonrpc/subscriptions -export json export basics export provider export chronicles @@ -26,20 +25,25 @@ type JsonRpcProvider* = ref object of Provider client: Future[RpcClient] subscriptions: Future[JsonRpcSubscriptions] - JsonRpcSigner* = ref object of Signer - provider: JsonRpcProvider - address: ?Address + JsonRpcProviderError* = object of ProviderError JsonRpcSubscription* = ref object of Subscription subscriptions: JsonRpcSubscriptions id: JsonNode -proc raiseJsonRpcProviderError(message: string) {.raises: [JsonRpcProviderError].} = + # Signer + JsonRpcSigner* = ref object of Signer + provider: JsonRpcProvider + address: ?Address + JsonRpcSignerError* = object of SignerError + +proc raiseJsonRpcProviderError( + message: string) {.raises: [JsonRpcProviderError].} = + var message = message - try: - message = parseJson(message){"message"}.getStr - except Exception: - discard + if json =? JsonNode.fromJson(message): + if "message" in json: + message = json{"message"}.getStr raise newException(JsonRpcProviderError, message) template convertError(body) = @@ -47,9 +51,7 @@ template convertError(body) = body except JsonRpcError as error: raiseJsonRpcProviderError(error.msg) - # Catch all ValueErrors for now, at least until JsonRpcError is actually - # raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151 - except ValueError as error: + except CatchableError as error: raiseJsonRpcProviderError(error.msg) # Provider @@ -60,48 +62,68 @@ const defaultPollingInterval = 4.seconds proc jsonHeaders: seq[(string, string)] = @[("Content-Type", "application/json")] -proc new*(_: type JsonRpcProvider, - url=defaultUrl, - pollingInterval=defaultPollingInterval): JsonRpcProvider = +proc new*( + _: type JsonRpcProvider, + url=defaultUrl, + pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} = + var initialized: Future[void] var client: RpcClient var subscriptions: JsonRpcSubscriptions - proc initialize {.async.} = - case parseUri(url).scheme - of "ws", "wss": - let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders) - await websocket.connect(url) - client = websocket - subscriptions = JsonRpcSubscriptions.new(websocket) - else: - let http = newRpcHttpClient(getHeaders = jsonHeaders) - await http.connect(url) - client = http - subscriptions = JsonRpcSubscriptions.new(http, - pollingInterval = pollingInterval) + proc initialize {.async: (raises:[JsonRpcProviderError]).} = + convertError: + case parseUri(url).scheme + of "ws", "wss": + let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders) + await websocket.connect(url) + client = websocket + subscriptions = JsonRpcSubscriptions.new(websocket) + else: + let http = newRpcHttpClient(getHeaders = jsonHeaders) + await http.connect(url) + client = http + subscriptions = JsonRpcSubscriptions.new(http, + pollingInterval = pollingInterval) + subscriptions.start() - proc awaitClient: Future[RpcClient] {.async.} = + proc awaitClient: Future[RpcClient] {.async:(raises:[JsonRpcProviderError]).} = convertError: await initialized return client - proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async.} = + proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async:(raises:[JsonRpcProviderError]).} = convertError: await initialized return subscriptions initialized = initialize() - JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) + return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) + +proc callImpl( + client: RpcClient, + call: string, + args: JsonNode): Future[JsonNode] {.async: (raises: [JsonRpcProviderError]).} = + + without response =? (await client.call(call, %args)).catch, error: + raiseJsonRpcProviderError error.msg + without json =? JsonNode.fromJson(response.string), error: + raiseJsonRpcProviderError "Failed to parse response: " & error.msg + json + +proc send*( + provider: JsonRpcProvider, + call: string, + arguments: seq[JsonNode] = @[]): Future[JsonNode] + {.async: (raises: [JsonRpcProviderError]).} = -proc send*(provider: JsonRpcProvider, - call: string, - arguments: seq[JsonNode] = @[]): Future[JsonNode] {.async.} = convertError: let client = await provider.client - return await client.call(call, %arguments) + return await client.callImpl(call, %arguments) + +proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] + {.async: (raises: [JsonRpcProviderError]).} = -proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] {.async.} = convertError: let client = await provider.client return await client.eth_accounts() @@ -112,54 +134,66 @@ proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner = proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner = JsonRpcSigner(provider: provider, address: some address) -method getBlockNumber*(provider: JsonRpcProvider): Future[UInt256] {.async.} = +method getBlockNumber*( + provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_blockNumber() -method getBlock*(provider: JsonRpcProvider, - tag: BlockTag): Future[?Block] {.async.} = +method getBlock*( + provider: JsonRpcProvider, + tag: BlockTag): Future[?Block] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getBlockByNumber(tag, false) -method call*(provider: JsonRpcProvider, - tx: Transaction, - blockTag = BlockTag.latest): Future[seq[byte]] {.async.} = +method call*( + provider: JsonRpcProvider, + tx: Transaction, + blockTag = BlockTag.latest): Future[seq[byte]] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_call(tx, blockTag) -method getGasPrice*(provider: JsonRpcProvider): Future[UInt256] {.async.} = +method getGasPrice*( + provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_gasPrice() -method getTransactionCount*(provider: JsonRpcProvider, - address: Address, - blockTag = BlockTag.latest): - Future[UInt256] {.async.} = +method getTransactionCount*( + provider: JsonRpcProvider, + address: Address, + blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getTransactionCount(address, blockTag) -method getTransaction*(provider: JsonRpcProvider, - txHash: TransactionHash): - Future[?PastTransaction] {.async.} = +method getTransaction*( + provider: JsonRpcProvider, + txHash: TransactionHash): Future[?PastTransaction] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getTransactionByHash(txHash) -method getTransactionReceipt*(provider: JsonRpcProvider, - txHash: TransactionHash): - Future[?TransactionReceipt] {.async.} = +method getTransactionReceipt*( + provider: JsonRpcProvider, + txHash: TransactionHash): Future[?TransactionReceipt] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_getTransactionReceipt(txHash) -method getLogs*(provider: JsonRpcProvider, - filter: EventFilter): - Future[seq[Log]] {.async.} = +method getLogs*( + provider: JsonRpcProvider, + filter: EventFilter): Future[seq[Log]] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client let logsJson = if filter of Filter: @@ -171,19 +205,23 @@ method getLogs*(provider: JsonRpcProvider, var logs: seq[Log] = @[] for logJson in logsJson.getElems: - if log =? Log.fromJson(logJson).catch: + if log =? Log.fromJson(logJson): logs.add log return logs -method estimateGas*(provider: JsonRpcProvider, - transaction: Transaction, - blockTag = BlockTag.latest): Future[UInt256] {.async.} = +method estimateGas*( + provider: JsonRpcProvider, + transaction: Transaction, + blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client return await client.eth_estimateGas(transaction, blockTag) -method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} = +method getChainId*( + provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client try: @@ -191,7 +229,11 @@ method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} = except CatchableError: return parse(await client.net_version(), UInt256) -method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): Future[TransactionResponse] {.async.} = +method sendTransaction*( + provider: JsonRpcProvider, + rawTransaction: seq[byte]): Future[TransactionResponse] + {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client @@ -199,30 +241,36 @@ method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): F return TransactionResponse(hash: hash, provider: provider) -method subscribe*(provider: JsonRpcProvider, - filter: EventFilter, - onLog: LogHandler): - Future[Subscription] {.async.} = +method subscribe*( + provider: JsonRpcProvider, + filter: EventFilter, + onLog: LogHandler): Future[Subscription] {.async: (raises:[ProviderError]).} = + convertError: let subscriptions = await provider.subscriptions let id = await subscriptions.subscribeLogs(filter, onLog) return JsonRpcSubscription(subscriptions: subscriptions, id: id) -method subscribe*(provider: JsonRpcProvider, - onBlock: BlockHandler): - Future[Subscription] {.async.} = +method subscribe*( + provider: JsonRpcProvider, + onBlock: BlockHandler): Future[Subscription] {.async: (raises:[ProviderError]).} = + convertError: let subscriptions = await provider.subscriptions let id = await subscriptions.subscribeBlocks(onBlock) return JsonRpcSubscription(subscriptions: subscriptions, id: id) -method unsubscribe(subscription: JsonRpcSubscription) {.async.} = +method unsubscribe*( + subscription: JsonRpcSubscription) {.async: (raises:[ProviderError]).} = + convertError: let subscriptions = subscription.subscriptions let id = subscription.id await subscriptions.unsubscribe(id) -method close*(provider: JsonRpcProvider) {.async.} = +method close*( + provider: JsonRpcProvider) {.async: (raises:[ProviderError]).} = + convertError: let client = await provider.client let subscriptions = await provider.subscriptions @@ -231,10 +279,32 @@ method close*(provider: JsonRpcProvider) {.async.} = # Signer -method provider*(signer: JsonRpcSigner): Provider = +proc raiseJsonRpcSignerError( + message: string) {.raises: [JsonRpcSignerError].} = + + var message = message + if json =? JsonNode.fromJson(message): + if "message" in json: + message = json{"message"}.getStr + raise newException(JsonRpcSignerError, message) + +template convertSignerError(body) = + try: + body + except JsonRpcError as error: + raiseJsonRpcSignerError(error.msg) + except CatchableError as error: + raise newException(JsonRpcSignerError, error.msg) + +method provider*(signer: JsonRpcSigner): Provider + {.gcsafe, raises: [SignerError].} = + signer.provider -method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} = +method getAddress*( + signer: JsonRpcSigner): Future[Address] + {.async: (raises:[ProviderError, SignerError]).} = + if address =? signer.address: return address @@ -242,18 +312,23 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} = if accounts.len > 0: return accounts[0] - raiseJsonRpcProviderError "no address found" + raiseJsonRpcSignerError "no address found" -method signMessage*(signer: JsonRpcSigner, - message: seq[byte]): Future[seq[byte]] {.async.} = - convertError: +method signMessage*( + signer: JsonRpcSigner, + message: seq[byte]): Future[seq[byte]] {.async: (raises:[SignerError]).} = + + convertSignerError: let client = await signer.provider.client let address = await signer.getAddress() return await client.eth_sign(address, message) -method sendTransaction*(signer: JsonRpcSigner, - transaction: Transaction): Future[TransactionResponse] {.async.} = - convertError: +method sendTransaction*( + signer: JsonRpcSigner, + transaction: Transaction): Future[TransactionResponse] + {.async: (raises:[SignerError]).} = + + convertSignerError: if nonce =? transaction.nonce: signer.updateNonce(nonce) let diff --git a/ethers/providers/jsonrpc/conversions.nim b/ethers/providers/jsonrpc/conversions.nim index 6f89542..4abd3da 100644 --- a/ethers/providers/jsonrpc/conversions.nim +++ b/ethers/providers/jsonrpc/conversions.nim @@ -1,7 +1,9 @@ -import std/json import std/strformat import std/strutils +import pkg/chronicles except fromJson, `%`, `%*`, toJson import pkg/json_rpc/jsonmarshal +import pkg/questionable/results +import pkg/serde import pkg/stew/byteutils import ../../basics import ../../transaction @@ -9,68 +11,57 @@ import ../../blocktag import ../../provider export jsonmarshal +export serde +export chronicles except fromJson, `%`, `%*`, toJson -type JsonSerializationError = object of EthersError +{.push raises: [].} -template raiseSerializationError(message: string) = - raise newException(JsonSerializationError, message) +proc getOrRaise*[T, E](self: ?!T, exc: typedesc[E]): T {.raises: [E].} = + let val = self.valueOr: + raise newException(E, self.error.msg) + val -proc expectFields(json: JsonNode, expectedFields: varargs[string]) = - for fieldName in expectedFields: - if not json.hasKey(fieldName): - raiseSerializationError(fmt"'{fieldName}' field not found in ${json}") +template mapFailure*[T, V, E]( + exp: Result[T, V], + exc: typedesc[E], +): Result[T, ref CatchableError] = + ## Convert `Result[T, E]` to `Result[E, ref CatchableError]` + ## -func fromJson*(T: type, json: JsonNode, name = ""): T = - fromJson(json, name, result) - -# byte sequence - -func `%`*(bytes: seq[byte]): JsonNode = - %("0x" & bytes.toHex) - -func fromJson*(json: JsonNode, name: string, result: var seq[byte]) = - result = hexToSeqByte(json.getStr()) - -# byte arrays - -func `%`*[N](bytes: array[N, byte]): JsonNode = - %("0x" & bytes.toHex) - -func fromJson*[N](json: JsonNode, name: string, result: var array[N, byte]) = - hexToByteArray(json.getStr(), result) + exp.mapErr(proc (e: V): ref CatchableError = (ref exc)(msg: e.msg)) # Address func `%`*(address: Address): JsonNode = %($address) -func fromJson*(json: JsonNode, name: string, result: var Address) = - if address =? Address.init(json.getStr()): - result = address - else: - raise newException(ValueError, "\"" & name & "\"is not an Address") +func fromJson(_: type Address, json: JsonNode): ?!Address = + expectJsonKind(Address, JString, json) + without address =? Address.init(json.getStr), error: + return failure newException(SerializationError, + "Failed to convert '" & $json & "' to Address: " & error.msg) + success address # UInt256 func `%`*(integer: UInt256): JsonNode = %("0x" & toHex(integer)) -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)) +func fromJson*(_: type UInt256, json: JsonNode): ?!UInt256 = + without result =? UInt256.fromHex(json.getStr()).catch, error: + return UInt256.failure error.msg + success result # Transaction +# TODO: add option that ignores none Option[T] +# TODO: add name option (gasLimit => gas, sender => from) func `%`*(transaction: Transaction): JsonNode = - result = %{ "to": %transaction.to, "data": %transaction.data } + result = %*{ + "to": transaction.to, + "data": %transaction.data, + "value": %transaction.value + } if sender =? transaction.sender: result["from"] = %sender if nonce =? transaction.nonce: @@ -84,105 +75,52 @@ func `%`*(transaction: Transaction): JsonNode = # BlockTag -func `%`*(blockTag: BlockTag): JsonNode = - %($blockTag) +func `%`*(tag: BlockTag): JsonNode = + % $tag -# Log +func fromJson*(_: type BlockTag, json: JsonNode): ?!BlockTag = + expectJsonKind(BlockTag, JString, json) + let jsonVal = json.getStr + if jsonVal[0..1].toLowerAscii == "0x": + without blkNum =? UInt256.fromHex(jsonVal).catch, error: + return BlockTag.failure error.msg + return success BlockTag.init(blkNum) -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") + case jsonVal: + of "earliest": return success BlockTag.earliest + of "latest": return success BlockTag.latest + of "pending": return success BlockTag.pending + else: return failure newException(SerializationError, + "Failed to convert '" & $json & + "' to BlockTag: must be one of 'earliest', 'latest', 'pending'") - var data: seq[byte] - var topics: seq[Topic] - fromJson(json["data"], "data", data) - fromJson(json["topics"], "topics", topics) - result = Log(data: data, topics: topics) +# TransactionStatus | TransactionType -# TransactionStatus +func `%`*(e: TransactionStatus | TransactionType): JsonNode = + % ("0x" & e.int8.toHex(1)) -func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) = - let val = fromHex[int](json.getStr) - result = TransactionStatus(val) +proc fromJson*[E: TransactionStatus | TransactionType]( + T: type E, + json: JsonNode +): ?!T = + expectJsonKind(string, JString, json) + let integer = ? fromHex[int](json.str).catch.mapFailure(SerializationError) + success T(integer) -func `%`*(status: TransactionStatus): JsonNode = - %("0x" & status.int.toHex(1)) +## Generic conversions to use nim-json instead of nim-json-serialization for +## json rpc serialization purposes +## writeValue => `%` +## readValue => fromJson -# PastTransaction +proc writeValue*[T: not JsonNode]( + writer: var JsonWriter[JrpcConv], + value: T) {.raises:[IOError].} = -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" + writer.writeValue(%value) - 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") +proc readValue*[T: not JsonNode]( + r: var JsonReader[JrpcConv], + result: var T) {.raises: [SerializationError, IOError].} = -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") - ) + var json = r.readValue(JsonNode) + result = T.fromJson(json).getOrRaise(SerializationError) diff --git a/ethers/providers/jsonrpc/looping.nim b/ethers/providers/jsonrpc/looping.nim index f0bb7a8..74ed075 100644 --- a/ethers/providers/jsonrpc/looping.nim +++ b/ethers/providers/jsonrpc/looping.nim @@ -2,5 +2,5 @@ template untilCancelled*(body) = try: while true: body - except CancelledError: - raise + except CancelledError as e: + raise e diff --git a/ethers/providers/jsonrpc/subscriptions.nim b/ethers/providers/jsonrpc/subscriptions.nim index 142c9cc..a3f0810 100644 --- a/ethers/providers/jsonrpc/subscriptions.nim +++ b/ethers/providers/jsonrpc/subscriptions.nim @@ -4,6 +4,7 @@ import pkg/chronos import pkg/json_rpc/rpcclient import ../../basics import ../../provider +include ../../nimshims/hashes import ./rpccalls import ./conversions import ./looping @@ -12,8 +13,41 @@ type JsonRpcSubscriptions* = ref object of RootObj client: RpcClient callbacks: Table[JsonNode, SubscriptionCallback] + methodHandlers: Table[string, MethodHandler] + MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].} SubscriptionCallback = proc(id, arguments: JsonNode) {.gcsafe, raises:[].} +{.push raises:[].} + +template `or`(a: JsonNode, b: typed): JsonNode = + if a.isNil: b else: a + +func start*(subscriptions: JsonRpcSubscriptions) = + subscriptions.client.onProcessMessage = + proc(client: RpcClient, + line: string): Result[bool, string] {.gcsafe, raises: [].} = + if json =? JsonNode.fromJson(line): + if "method" in json: + let methodName = json{"method"}.getStr() + if methodName in subscriptions.methodHandlers: + let handler = subscriptions.methodHandlers.getOrDefault(methodName) + if not handler.isNil: + handler(json{"params"} or newJArray()) + # false = do not continue processing message using json_rpc's + # default processing handler + return ok false + + # true = continue processing message using json_rpc's default message handler + return ok true + + +proc setMethodHandler( + subscriptions: JsonRpcSubscriptions, + `method`: string, + handler: MethodHandler +) = + subscriptions.methodHandlers[`method`] = handler + method subscribeBlocks*(subscriptions: JsonRpcSubscriptions, onBlock: BlockHandler): Future[JsonNode] @@ -40,11 +74,11 @@ method close*(subscriptions: JsonRpcSubscriptions) {.async, base.} = proc getCallback(subscriptions: JsonRpcSubscriptions, id: JsonNode): ?SubscriptionCallback = try: - if subscriptions.callbacks.hasKey(id): + if not id.isNil and id in subscriptions.callbacks: subscriptions.callbacks[id].some else: SubscriptionCallback.none - except Exception: + except KeyError: SubscriptionCallback.none # Web sockets @@ -54,20 +88,21 @@ type proc new*(_: type JsonRpcSubscriptions, client: RpcWebSocketClient): JsonRpcSubscriptions = + let subscriptions = WebSocketSubscriptions(client: client) proc subscriptionHandler(arguments: JsonNode) {.raises:[].} = - if id =? arguments["subscription"].catch and - callback =? subscriptions.getCallback(id): + let id = arguments{"subscription"} or newJString("") + if callback =? subscriptions.getCallback(id): callback(id, arguments) - client.setMethodHandler("eth_subscription", subscriptionHandler) + subscriptions.setMethodHandler("eth_subscription", subscriptionHandler) subscriptions method subscribeBlocks(subscriptions: WebSocketSubscriptions, onBlock: BlockHandler): Future[JsonNode] {.async.} = - proc callback(id, arguments: JsonNode) = - if blck =? Block.fromJson(arguments["result"]).catch: + proc callback(id, arguments: JsonNode) {.raises: [].} = + if blck =? Block.fromJson(arguments{"result"}): onBlock(blck) let id = await subscriptions.client.eth_subscribe("newHeads") subscriptions.callbacks[id] = callback @@ -79,13 +114,13 @@ method subscribeLogs(subscriptions: WebSocketSubscriptions, Future[JsonNode] {.async.} = proc callback(id, arguments: JsonNode) = - if log =? Log.fromJson(arguments["result"]).catch: + if log =? Log.fromJson(arguments{"result"}): onLog(log) let id = await subscriptions.client.eth_subscribe("logs", filter) subscriptions.callbacks[id] = callback return id -method unsubscribe(subscriptions: WebSocketSubscriptions, +method unsubscribe*(subscriptions: WebSocketSubscriptions, id: JsonNode) {.async.} = subscriptions.callbacks.del(id) @@ -140,7 +175,7 @@ method subscribeBlocks(subscriptions: PollingSubscriptions, discard proc callback(id, change: JsonNode) = - if hash =? BlockHash.fromJson(change).catch: + if hash =? BlockHash.fromJson(change): asyncSpawn getBlock(hash) let id = await subscriptions.client.eth_newBlockFilter() @@ -154,14 +189,14 @@ method subscribeLogs(subscriptions: PollingSubscriptions, {.async.} = proc callback(id, change: JsonNode) = - if log =? Log.fromJson(change).catch: + if log =? Log.fromJson(change): onLog(log) let id = await subscriptions.client.eth_newFilter(filter) subscriptions.callbacks[id] = callback return id -method unsubscribe(subscriptions: PollingSubscriptions, +method unsubscribe*(subscriptions: PollingSubscriptions, id: JsonNode) {.async.} = subscriptions.callbacks.del(id) diff --git a/ethers/signer.nim b/ethers/signer.nim index eaca26c..cba120c 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -1,14 +1,15 @@ +import pkg/questionable import ./basics import ./provider export basics +{.push raises: [].} + type Signer* = ref object of RootObj lastSeenNonce: ?UInt256 populateLock: AsyncLock - -type SignerError* = object of EthersError EstimateGasError* = object of SignerError transaction*: Transaction @@ -19,50 +20,87 @@ template raiseSignerError(message: string, parent: ref ProviderError = nil) = proc raiseEstimateGasError( transaction: Transaction, parent: ref ProviderError = nil -) = +) {.raises: [EstimateGasError] .} = let e = (ref EstimateGasError)( msg: "Estimate gas failed", transaction: transaction, parent: parent) raise e -method provider*(signer: Signer): Provider {.base, gcsafe.} = +template convertError(body) = + try: + body + except EthersError as error: + raiseSignerError(error.msg) + except CatchableError as error: + raiseSignerError(error.msg) + +method provider*( + signer: Signer): Provider {.base, gcsafe, raises: [SignerError].} = doAssert false, "not implemented" -method getAddress*(signer: Signer): Future[Address] {.base, gcsafe.} = +method getAddress*( + signer: Signer): Future[Address] + {.base, async: (raises:[ProviderError, SignerError]).} = + doAssert false, "not implemented" -method signMessage*(signer: Signer, - message: seq[byte]): Future[seq[byte]] {.base, async.} = +method signMessage*( + signer: Signer, + message: seq[byte]): Future[seq[byte]] + {.base, async: (raises: [SignerError]).} = + doAssert false, "not implemented" -method sendTransaction*(signer: Signer, - transaction: Transaction): Future[TransactionResponse] {.base, async.} = +method sendTransaction*( + signer: Signer, + transaction: Transaction): Future[TransactionResponse] + {.base, async: (raises:[SignerError]).} = + doAssert false, "not implemented" -method getGasPrice*(signer: Signer): Future[UInt256] {.base, gcsafe.} = - signer.provider.getGasPrice() +method getGasPrice*( + signer: Signer): Future[UInt256] + {.base, async: (raises: [ProviderError, SignerError]).} = -method getTransactionCount*(signer: Signer, - blockTag = BlockTag.latest): - Future[UInt256] {.base, async.} = - let address = await signer.getAddress() - return await signer.provider.getTransactionCount(address, blockTag) + return await signer.provider.getGasPrice() + +method getTransactionCount*( + signer: Signer, + blockTag = BlockTag.latest): Future[UInt256] + {.base, async: (raises:[SignerError]).} = + + convertError: + let address = await signer.getAddress() + return await signer.provider.getTransactionCount(address, blockTag) + +method estimateGas*( + signer: Signer, + transaction: Transaction, + blockTag = BlockTag.latest): Future[UInt256] + {.base, async: (raises:[SignerError]).} = -method estimateGas*(signer: Signer, - transaction: Transaction, - blockTag = BlockTag.latest): Future[UInt256] {.base, async.} = var transaction = transaction - transaction.sender = some(await signer.getAddress) + var address: Address + + convertError: + address = await signer.getAddress + + transaction.sender = some(address) try: return await signer.provider.estimateGas(transaction) except ProviderError as e: raiseEstimateGasError transaction, e -method getChainId*(signer: Signer): Future[UInt256] {.base, gcsafe.} = - signer.provider.getChainId() +method getChainId*( + signer: Signer): Future[UInt256] + {.base, async: (raises: [ProviderError, SignerError]).} = + + return await signer.provider.getChainId() + +method getNonce( + signer: Signer): Future[UInt256] {.base, async: (raises: [SignerError]).} = -method getNonce(signer: Signer): Future[UInt256] {.base, gcsafe, async.} = var nonce = await signer.getTransactionCount(BlockTag.pending) if lastSeen =? signer.lastSeenNonce and lastSeen >= nonce: @@ -87,11 +125,16 @@ method decreaseNonce*(signer: Signer) {.base, gcsafe.} = if lastSeen =? signer.lastSeenNonce and lastSeen > 0: signer.lastSeenNonce = some lastSeen - 1 -method populateTransaction*(signer: Signer, - transaction: Transaction): - Future[Transaction] {.base, async.} = +method populateTransaction*( + signer: Signer, + transaction: Transaction): Future[Transaction] + {.base, async: (raises: [CancelledError, AsyncLockError, ProviderError, SignerError]).} = - if sender =? transaction.sender and sender != await signer.getAddress(): + var address: Address + convertError: + address = await signer.getAddress() + + if sender =? transaction.sender and sender != address: raiseSignerError("from address mismatch") if chainId =? transaction.chainId and chainId != await signer.getChainId(): raiseSignerError("chain id mismatch") @@ -105,7 +148,7 @@ method populateTransaction*(signer: Signer, try: if transaction.sender.isNone: - populated.sender = some(await signer.getAddress()) + populated.sender = some(address) if transaction.chainId.isNone: populated.chainId = some(await signer.getChainId()) if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone): @@ -119,10 +162,12 @@ method populateTransaction*(signer: Signer, populated.nonce = some(await signer.getNonce()) try: populated.gasLimit = some(await signer.estimateGas(populated)) - except ProviderError, EstimateGasError: - let e = getCurrentException() + except EstimateGasError as e: signer.decreaseNonce() raise e + except ProviderError as e: + signer.decreaseNonce() + raiseSignerError(e.msg) else: if transaction.nonce.isNone: @@ -138,7 +183,7 @@ method populateTransaction*(signer: Signer, method cancelTransaction*( signer: Signer, tx: Transaction -): Future[TransactionResponse] {.async, base.} = +): Future[TransactionResponse] {.base, async: (raises: [SignerError]).} = # cancels a transaction by sending with a 0-valued transaction to ourselves # with the failed tx's nonce @@ -148,5 +193,6 @@ method cancelTransaction*( raiseSignerError "transaction must have nonce" var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce) - cancelTx = await signer.populateTransaction(cancelTx) - return await signer.sendTransaction(cancelTx) + convertError: + cancelTx = await signer.populateTransaction(cancelTx) + return await signer.sendTransaction(cancelTx) diff --git a/ethers/signers/jsonrpc.nim b/ethers/signers/jsonrpc.nim new file mode 100644 index 0000000..cd17b31 --- /dev/null +++ b/ethers/signers/jsonrpc.nim @@ -0,0 +1,3 @@ +import ../providers/jsonrpc + +export provider, getAddress, signMessage, sendTransaction \ No newline at end of file diff --git a/ethers/signers/wallet.nim b/ethers/signers/wallet.nim new file mode 100644 index 0000000..18a92cd --- /dev/null +++ b/ethers/signers/wallet.nim @@ -0,0 +1,92 @@ +import eth/keys +import ../basics +import ../provider +import ../transaction +import ../signer +import ./wallet/error +import ./wallet/signing + +export keys +export WalletError +export signing + +{.push raises: [].} + +var rng {.threadvar.}: ref HmacDrbgContext + +proc getRng: ref HmacDrbgContext = + if rng.isNil: + rng = newRng() + rng + +type Wallet* = ref object of Signer + privateKey*: PrivateKey + publicKey*: PublicKey + address*: Address + provider*: ?Provider + +proc new*(_: type Wallet, privateKey: PrivateKey): Wallet = + let publicKey = privateKey.toPublicKey() + let address = Address.init(publicKey.toCanonicalAddress()) + Wallet(privateKey: privateKey, publicKey: publicKey, address: address) + +proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet = + let wallet = Wallet.new(privateKey) + wallet.provider = some provider + wallet + +proc new*(_: type Wallet, privateKey: string): ?!Wallet = + let keyResult = PrivateKey.fromHex(privateKey) + if keyResult.isErr: + return failure newException(WalletError, "invalid key: " & $keyResult.error) + success Wallet.new(keyResult.get()) + +proc new*(_: type Wallet, privateKey: string, provider: Provider): ?!Wallet = + let wallet = ? Wallet.new(privateKey) + wallet.provider = some provider + success wallet + +proc connect*(wallet: Wallet, provider: Provider) = + wallet.provider = some provider + +proc createRandom*(_: type Wallet): Wallet = + result = Wallet() + result.privateKey = PrivateKey.random(getRng()[]) + result.publicKey = result.privateKey.toPublicKey() + result.address = Address.init(result.publicKey.toCanonicalAddress()) + +proc createRandom*(_: type Wallet, provider: Provider): Wallet = + result = Wallet() + result.privateKey = PrivateKey.random(getRng()[]) + result.publicKey = result.privateKey.toPublicKey() + result.address = Address.init(result.publicKey.toCanonicalAddress()) + result.provider = some provider + +method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} = + without provider =? wallet.provider: + raiseWalletError "Wallet has no provider" + provider + +method getAddress*( + wallet: Wallet): Future[Address] + {.async: (raises:[ProviderError, SignerError]).} = + + return wallet.address + +proc signTransaction*(wallet: Wallet, + transaction: Transaction): Future[seq[byte]] {.async: (raises:[WalletError]).} = + if sender =? transaction.sender and sender != wallet.address: + raiseWalletError "from address mismatch" + + return wallet.privateKey.sign(transaction) + +method sendTransaction*( + wallet: Wallet, + transaction: Transaction): Future[TransactionResponse] + {.async: (raises:[SignerError]).} = + + convertError: + let signed = await signTransaction(wallet, transaction) + if nonce =? transaction.nonce: + wallet.updateNonce(nonce) + return await provider(wallet).sendTransaction(signed) diff --git a/ethers/signers/wallet/error.nim b/ethers/signers/wallet/error.nim new file mode 100644 index 0000000..5bc593d --- /dev/null +++ b/ethers/signers/wallet/error.nim @@ -0,0 +1,13 @@ +import ../../signer + +type + WalletError* = object of SignerError + +func raiseWalletError*(message: string) {.raises: [WalletError].}= + raise newException(WalletError, message) + +template convertError*(body) = + try: + body + except CatchableError as error: + raiseWalletError(error.msg) diff --git a/ethers/wallet/signing.nim b/ethers/signers/wallet/signing.nim similarity index 96% rename from ethers/wallet/signing.nim rename to ethers/signers/wallet/signing.nim index 2ea9fbf..8533d77 100644 --- a/ethers/wallet/signing.nim +++ b/ethers/signers/wallet/signing.nim @@ -2,9 +2,9 @@ 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 ../../basics +import ../../transaction as ethers +import ../../provider import ./error type diff --git a/ethers/testing.nim b/ethers/testing.nim index 8162ab6..2b9ec4a 100644 --- a/ethers/testing.nim +++ b/ethers/testing.nim @@ -27,7 +27,7 @@ proc reverts*[T](call: Future[T]): Future[bool] {.async.} = else: discard await call return false - except ProviderError, EstimateGasError: + except ProviderError, SignerError, EstimateGasError: return true proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} = @@ -37,7 +37,7 @@ proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} = else: discard await call return false - except ProviderError, EstimateGasError: + except ProviderError, SignerError, EstimateGasError: let e = getCurrentException() var passed = reason == (ref EthersError)(e).revertReason if not passed and diff --git a/ethers/transaction.nim b/ethers/transaction.nim index 0ee9d2a..1ff2e1b 100644 --- a/ethers/transaction.nim +++ b/ethers/transaction.nim @@ -1,3 +1,4 @@ +import pkg/serde import pkg/stew/byteutils import ./basics @@ -6,8 +7,8 @@ type Legacy = 0, AccessList = 1, Dynamic = 2 - Transaction* = object - sender*: ?Address + Transaction* {.serialize.} = object + sender* {.serialize("from").}: ?Address to*: Address data*: seq[byte] value*: UInt256 @@ -17,7 +18,7 @@ type maxFee*: ?UInt256 maxPriorityFee*: ?UInt256 gasLimit*: ?UInt256 - transactionType*: ?TransactionType + transactionType* {.serialize("type").}: ?TransactionType func `$`*(transaction: Transaction): string = result = "(" diff --git a/ethers/wallet.nim b/ethers/wallet.nim index 85305f6..59490ca 100644 --- a/ethers/wallet.nim +++ b/ethers/wallet.nim @@ -1,76 +1,3 @@ -import eth/keys -import ./basics -import ./provider -import ./transaction -import ./signer -import ./wallet/error -import ./wallet/signing +import ./signers/wallet -export keys -export WalletError -export signing - -var rng {.threadvar.}: ref HmacDrbgContext - -proc getRng: ref HmacDrbgContext = - if rng.isNil: - rng = newRng() - rng - -type Wallet* = ref object of Signer - privateKey*: PrivateKey - publicKey*: PublicKey - address*: Address - provider*: ?Provider - -proc new*(_: type Wallet, privateKey: PrivateKey): Wallet = - let publicKey = privateKey.toPublicKey() - let address = Address.init(publicKey.toCanonicalAddress()) - Wallet(privateKey: privateKey, publicKey: publicKey, address: address) -proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet = - let wallet = Wallet.new(privateKey) - wallet.provider = some provider - wallet -proc new*(_: type Wallet, privateKey: string): ?!Wallet = - let keyResult = PrivateKey.fromHex(privateKey) - if keyResult.isErr: - return failure newException(WalletError, "invalid key: " & $keyResult.error) - success Wallet.new(keyResult.get()) -proc new*(_: type Wallet, privateKey: string, provider: Provider): ?!Wallet = - let wallet = ? Wallet.new(privateKey) - wallet.provider = some provider - success wallet -proc connect*(wallet: Wallet, provider: Provider) = - wallet.provider = some provider -proc createRandom*(_: type Wallet): Wallet = - result = Wallet() - result.privateKey = PrivateKey.random(getRng()[]) - result.publicKey = result.privateKey.toPublicKey() - result.address = Address.init(result.publicKey.toCanonicalAddress()) -proc createRandom*(_: type Wallet, provider: Provider): Wallet = - result = Wallet() - result.privateKey = PrivateKey.random(getRng()[]) - result.publicKey = result.privateKey.toPublicKey() - result.address = Address.init(result.publicKey.toCanonicalAddress()) - result.provider = some provider - -method provider*(wallet: Wallet): Provider = - without provider =? wallet.provider: - raiseWalletError "Wallet has no provider" - provider - -method getAddress(wallet: Wallet): Future[Address] {.async.} = - return wallet.address - -proc signTransaction*(wallet: Wallet, - transaction: Transaction): Future[seq[byte]] {.async.} = - if sender =? transaction.sender and sender != wallet.address: - raiseWalletError "from address mismatch" - - return wallet.privateKey.sign(transaction) - -method sendTransaction*(wallet: Wallet, transaction: Transaction): Future[TransactionResponse] {.async.} = - let signed = await signTransaction(wallet, transaction) - if nonce =? transaction.nonce: - wallet.updateNonce(nonce) - return await provider(wallet).sendTransaction(signed) +export wallet \ No newline at end of file diff --git a/ethers/wallet/error.nim b/ethers/wallet/error.nim deleted file mode 100644 index dc4ab4e..0000000 --- a/ethers/wallet/error.nim +++ /dev/null @@ -1,7 +0,0 @@ -import ../basics - -type - WalletError* = object of EthersError - -func raiseWalletError*(message: string) = - raise newException(WalletError, message) diff --git a/testmodule/mocks.nim b/testmodule/mocks.nim index 30584e1..eed2c23 100644 --- a/testmodule/mocks.nim +++ b/testmodule/mocks.nim @@ -11,10 +11,15 @@ func new*(_: type MockSigner, provider: Provider): MockSigner = method provider*(signer: MockSigner): Provider = signer.provider -method getAddress*(signer: MockSigner): Future[Address] {.async.} = +method getAddress*( + signer: MockSigner): Future[Address] + {.async: (raises:[ProviderError, SignerError]).} = + return signer.address -method sendTransaction*(signer: MockSigner, - transaction: Transaction): - Future[TransactionResponse] {.async.} = +method sendTransaction*( + signer: MockSigner, + transaction: Transaction): Future[TransactionResponse] + {.async: (raises:[SignerError]).} = + signer.transactions.add(transaction) diff --git a/testmodule/providers/jsonrpc/testConversions.nim b/testmodule/providers/jsonrpc/testConversions.nim index 37ffb2b..c29810a 100644 --- a/testmodule/providers/jsonrpc/testConversions.nim +++ b/testmodule/providers/jsonrpc/testConversions.nim @@ -2,6 +2,9 @@ import std/strutils import std/unittest import pkg/ethers/provider import pkg/ethers/providers/jsonrpc/conversions +import pkg/questionable +import pkg/questionable/results +import pkg/serde import pkg/stew/byteutils func flatten(s: string): string = @@ -17,14 +20,14 @@ suite "JSON Conversions": "timestamp":"0x6285c293" } - var blk = Block.fromJson(json) - check blk.number.isNone + let blk1 = !Block.fromJson(json) + check blk1.number.isNone json["number"] = newJString("") - blk = Block.fromJson(json) - check blk.number.isSome - check blk.number.get.isZero + let blk2 = !Block.fromJson(json) + check blk2.number.isSome + check blk2.number.get.isZero test "missing block hash in Block isNone": @@ -37,7 +40,8 @@ suite "JSON Conversions": } } - var blk = Block.fromJson(blkJson["result"]) + without blk =? Block.fromJson(blkJson["result"]): + fail check blk.hash.isNone test "missing block number in TransactionReceipt isNone": @@ -67,13 +71,15 @@ suite "JSON Conversions": "type": "0x0" } - var receipt = TransactionReceipt.fromJson(json) - check receipt.blockNumber.isNone + without receipt1 =? TransactionReceipt.fromJson(json): + fail + check receipt1.blockNumber.isNone json["blockNumber"] = newJString("") - receipt = TransactionReceipt.fromJson(json) - check receipt.blockNumber.isSome - check receipt.blockNumber.get.isZero + without receipt2 =? TransactionReceipt.fromJson(json): + fail + check receipt2.blockNumber.isSome + check receipt2.blockNumber.get.isZero test "missing block hash in TransactionReceipt isNone": let json = %*{ @@ -102,33 +108,10 @@ suite "JSON Conversions": "type": "0x0" } - let receipt = TransactionReceipt.fromJson(json) + without receipt =? TransactionReceipt.fromJson(json): + fail 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", @@ -149,7 +132,8 @@ suite "JSON Conversions": "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" } - let tx = PastTransaction.fromJson(json) + without tx =? PastTransaction.fromJson(json): + fail check tx.blockHash == BlockHash(array[32, byte].fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922")) check tx.blockNumber == 0x22e.u256 check tx.sender == Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get @@ -198,12 +182,12 @@ suite "JSON Conversions": "nonce":"0x3", "to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e", "transactionIndex":"0x3", + "type":"0x0", + "chainId":"0xc0de4", "value":"0x0", "v":"0x181bec", "r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a", - "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2", - "type":"0x0", - "chainId":"0xc0de4" + "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" }""".flatten check $(%tx) == expected @@ -227,10 +211,12 @@ suite "JSON Conversions": "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" } - let past = PastTransaction.fromJson(json) + without past =? PastTransaction.fromJson(json): + fail check %past.toTransaction == %*{ "to": !Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e"), "data": hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"), + "value": "0x0", "from": !Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34"), "nonce": 0x3.u256, "chainId": 0xc0de4.u256, diff --git a/testmodule/providers/jsonrpc/testJsonRpcProvider.nim b/testmodule/providers/jsonrpc/testJsonRpcProvider.nim index ee00acc..ccfc5e7 100644 --- a/testmodule/providers/jsonrpc/testJsonRpcProvider.nim +++ b/testmodule/providers/jsonrpc/testJsonRpcProvider.nim @@ -1,4 +1,3 @@ -import std/json import pkg/asynctest import pkg/chronos import pkg/ethers @@ -97,5 +96,5 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]: discard await provider.getBlock(BlockTag.latest) expect JsonRpcProviderError: discard await provider.subscribe(proc(_: Block) = discard) - expect JsonRpcProviderError: + expect JsonRpcSignerError: discard await provider.getSigner().sendTransaction(Transaction.example) diff --git a/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim b/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim index 8671e7a..a402774 100644 --- a/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim +++ b/testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim @@ -64,6 +64,7 @@ suite "Web socket subscriptions": client = newRpcWebSocketClient() await client.connect("ws://localhost:8545") subscriptions = JsonRpcSubscriptions.new(client) + subscriptions.start() teardown: await subscriptions.close() @@ -81,6 +82,7 @@ suite "HTTP polling subscriptions": await client.connect("http://localhost:8545") subscriptions = JsonRpcSubscriptions.new(client, pollingInterval = 100.millis) + subscriptions.start() teardown: await subscriptions.close() diff --git a/testmodule/testEnums.nim b/testmodule/testEnums.nim index bc7ec42..3686b93 100644 --- a/testmodule/testEnums.nim +++ b/testmodule/testEnums.nim @@ -1,5 +1,6 @@ import pkg/asynctest import pkg/ethers +import pkg/serde import ./hardhat type diff --git a/testmodule/testReturns.nim b/testmodule/testReturns.nim index 5cdf03b..09d4a60 100644 --- a/testmodule/testReturns.nim +++ b/testmodule/testReturns.nim @@ -1,5 +1,6 @@ import pkg/asynctest import pkg/ethers +import pkg/serde import ./hardhat type diff --git a/testmodule/testTesting.nim b/testmodule/testTesting.nim index 8fec151..b5d80bd 100644 --- a/testmodule/testTesting.nim +++ b/testmodule/testTesting.nim @@ -3,6 +3,7 @@ import pkg/asynctest import pkg/chronos import pkg/ethers import pkg/ethers/testing +import pkg/serde import ./helpers suite "Testing helpers": @@ -31,12 +32,15 @@ suite "Testing helpers": test "reverts only checks ProviderErrors, EstimateGasErrors": proc callProviderError() {.async.} = raise newException(ProviderError, "test") + proc callSignerError() {.async.} = + raise newException(SignerError, "test") proc callEstimateGasError() {.async.} = raise newException(EstimateGasError, "test") proc callEthersError() {.async.} = raise newException(EthersError, "test") check await callProviderError().reverts() + check await callSignerError().reverts() check await callEstimateGasError().reverts() expect EthersError: check await callEthersError().reverts() @@ -44,12 +48,15 @@ suite "Testing helpers": test "reverts with reason only checks ProviderErrors, EstimateGasErrors": proc callProviderError() {.async.} = raise newException(ProviderError, revertReason) + proc callSignerError() {.async.} = + raise newException(SignerError, revertReason) proc callEstimateGasError() {.async.} = raise newException(EstimateGasError, revertReason) proc callEthersError() {.async.} = raise newException(EthersError, revertReason) check await callProviderError().reverts(revertReason) + check await callSignerError().reverts(revertReason) check await callEstimateGasError().reverts(revertReason) expect EthersError: check await callEthersError().reverts(revertReason) diff --git a/testmodule/testWallet.nim b/testmodule/testWallet.nim index 703ccf5..a3cdf2d 100644 --- a/testmodule/testWallet.nim +++ b/testmodule/testWallet.nim @@ -1,4 +1,5 @@ import pkg/asynctest +import pkg/serde import pkg/stew/byteutils import ../ethers