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
This commit is contained in:
Eric 2024-02-19 16:50:46 +11:00 committed by GitHub
parent fd16d71ea5
commit 43500c63d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 671 additions and 470 deletions

View File

@ -5,10 +5,11 @@ license = "MIT"
requires "nim >= 1.6.0" requires "nim >= 1.6.0"
requires "chronicles >= 0.10.3 & < 0.11.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 "contractabi >= 0.6.0 & < 0.7.0"
requires "questionable >= 0.10.2 & < 0.11.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 "stint"
requires "stew" requires "stew"
requires "eth" requires "eth"

View File

@ -76,5 +76,5 @@ method transferFrom*(token: Erc20Token,
spender: Address, spender: Address,
recipient: Address, recipient: Address,
amount: UInt256): ?TransactionResponse {.base, contract.} 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. ## mechanism. `amount` is then deducted from the caller's allowance.

View File

@ -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

View File

@ -1,4 +1,5 @@
import pkg/chronicles import pkg/chronicles
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ./basics import ./basics
import ./transaction import ./transaction
@ -14,15 +15,15 @@ type
Provider* = ref object of RootObj Provider* = ref object of RootObj
ProviderError* = object of EthersError ProviderError* = object of EthersError
Subscription* = ref object of RootObj Subscription* = ref object of RootObj
EventFilter* = ref object of RootObj EventFilter* {.serialize.} = ref object of RootObj
address*: Address address*: Address
topics*: seq[Topic] topics*: seq[Topic]
Filter* = ref object of EventFilter Filter* {.serialize.} = ref object of EventFilter
fromBlock*: BlockTag fromBlock*: BlockTag
toBlock*: BlockTag toBlock*: BlockTag
FilterByBlockHash* = ref object of EventFilter FilterByBlockHash* {.serialize.} = ref object of EventFilter
blockHash*: BlockHash blockHash*: BlockHash
Log* = object Log* {.serialize.} = object
blockNumber*: UInt256 blockNumber*: UInt256
data*: seq[byte] data*: seq[byte]
logIndex*: UInt256 logIndex*: UInt256
@ -36,9 +37,9 @@ type
Invalid = 2 Invalid = 2
TransactionResponse* = object TransactionResponse* = object
provider*: Provider provider*: Provider
hash*: TransactionHash hash* {.serialize.}: TransactionHash
TransactionReceipt* = object TransactionReceipt* {.serialize.} = object
sender*: ?Address sender* {.serialize("from"), deserialize("from").}: ?Address
to*: ?Address to*: ?Address
contractAddress*: ?Address contractAddress*: ?Address
transactionIndex*: UInt256 transactionIndex*: UInt256
@ -51,18 +52,18 @@ type
cumulativeGasUsed*: UInt256 cumulativeGasUsed*: UInt256
effectiveGasPrice*: ?UInt256 effectiveGasPrice*: ?UInt256
status*: TransactionStatus status*: TransactionStatus
transactionType*: TransactionType transactionType* {.serialize("type"), deserialize("type").}: TransactionType
LogHandler* = proc(log: Log) {.gcsafe, raises:[].} LogHandler* = proc(log: Log) {.gcsafe, raises:[].}
BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].}
Topic* = array[32, byte] Topic* = array[32, byte]
Block* = object Block* {.serialize.} = object
number*: ?UInt256 number*: ?UInt256
timestamp*: UInt256 timestamp*: UInt256
hash*: ?BlockHash hash*: ?BlockHash
PastTransaction* = object PastTransaction* {.serialize.} = object
blockHash*: BlockHash blockHash*: BlockHash
blockNumber*: UInt256 blockNumber*: UInt256
sender*: Address sender* {.serialize("from"), deserialize("from").}: Address
gas*: UInt256 gas*: UInt256
gasPrice*: UInt256 gasPrice*: UInt256
hash*: TransactionHash hash*: TransactionHash
@ -70,7 +71,7 @@ type
nonce*: UInt256 nonce*: UInt256
to*: Address to*: Address
transactionIndex*: UInt256 transactionIndex*: UInt256
transactionType*: ?TransactionType transactionType* {.serialize("type"), deserialize("type").}: ?TransactionType
chainId*: ?UInt256 chainId*: ?UInt256
value*: UInt256 value*: UInt256
v*, r*, s*: UInt256 v*, r*, s*: UInt256
@ -87,77 +88,104 @@ template raiseProviderError(msg: string) =
func toTransaction*(past: PastTransaction): Transaction = func toTransaction*(past: PastTransaction): Transaction =
Transaction( Transaction(
sender: some past.sender, sender: some past.sender,
gasPrice: some past.gasPrice,
data: past.input,
nonce: some past.nonce,
to: past.to, 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, 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" 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" doAssert false, "not implemented"
method call*(provider: Provider, method call*(
provider: Provider,
tx: Transaction, tx: Transaction,
blockTag = BlockTag.latest): Future[seq[byte]] {.base, gcsafe.} = blockTag = BlockTag.latest): Future[seq[byte]] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" 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" doAssert false, "not implemented"
method getTransactionCount*(provider: Provider, method getTransactionCount*(
provider: Provider,
address: Address, address: Address,
blockTag = BlockTag.latest): blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransaction*(provider: Provider, method getTransaction*(
txHash: TransactionHash): provider: Provider,
Future[?PastTransaction] {.base, gcsafe.} = txHash: TransactionHash): Future[?PastTransaction] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransactionReceipt*(provider: Provider, method getTransactionReceipt*(
txHash: TransactionHash): provider: Provider,
Future[?TransactionReceipt] {.base, gcsafe.} = txHash: TransactionHash): Future[?TransactionReceipt] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*(provider: Provider, method sendTransaction*(
rawTransaction: seq[byte]): provider: Provider,
Future[TransactionResponse] {.base, gcsafe.} = rawTransaction: seq[byte]): Future[TransactionResponse] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getLogs*(provider: Provider, method getLogs*(
filter: EventFilter): Future[seq[Log]] {.base, gcsafe.} = provider: Provider,
filter: EventFilter): Future[seq[Log]] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method estimateGas*(provider: Provider, method estimateGas*(
provider: Provider,
transaction: Transaction, transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, gcsafe.} = blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" 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" doAssert false, "not implemented"
method subscribe*(provider: Provider, method subscribe*(
provider: Provider,
filter: EventFilter, filter: EventFilter,
callback: LogHandler): callback: LogHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} =
Future[Subscription] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method subscribe*(provider: Provider, method subscribe*(
callback: BlockHandler): provider: Provider,
Future[Subscription] {.base, gcsafe.} = callback: BlockHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method unsubscribe*(subscription: Subscription) {.base, async.} = method unsubscribe*(
subscription: Subscription) {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" 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 # Replay transaction at block. Useful for fetching revert reasons, which will
# be present in the raised error message. The replayed block number should # 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 # 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*( method getRevertReason*(
provider: Provider, provider: Provider,
hash: TransactionHash, hash: TransactionHash,
blockNumber: UInt256 blockNumber: UInt256): Future[?string] {.base, async: (raises: [ProviderError]).} =
): Future[?string] {.base, async.} =
without pastTx =? await provider.getTransaction(hash): without pastTx =? await provider.getTransaction(hash):
return none string return none string
@ -186,8 +213,7 @@ method getRevertReason*(
method getRevertReason*( method getRevertReason*(
provider: Provider, provider: Provider,
receipt: TransactionReceipt receipt: TransactionReceipt): Future[?string] {.base, async: (raises: [ProviderError]).} =
): Future[?string] {.base, async.} =
if receipt.status != TransactionStatus.Failure: if receipt.status != TransactionStatus.Failure:
return none string return none string
@ -199,8 +225,7 @@ method getRevertReason*(
proc ensureSuccess( proc ensureSuccess(
provider: Provider, provider: Provider,
receipt: TransactionReceipt receipt: TransactionReceipt) {.async: (raises: [ProviderError]).} =
) {.async, raises: [ProviderError].} =
## If the receipt.status is Failed, the tx is replayed to obtain a revert ## 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. ## reason, after which a ProviderError with the revert reason is raised.
## If no revert reason was obtained ## If no revert reason was obtained
@ -219,11 +244,12 @@ proc ensureSuccess(
trace "transaction replay completed, no revert reason obtained" trace "transaction replay completed, no revert reason obtained"
raiseProviderError("Transaction reverted with unknown reason") raiseProviderError("Transaction reverted with unknown reason")
proc confirm*(tx: TransactionResponse, proc confirm*(
tx: TransactionResponse,
confirmations = EthersDefaultConfirmations, confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks): timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt]
Future[TransactionReceipt] {.async: (raises: [CancelledError, ProviderError, EthersError]).} =
{.async, raises: [ProviderError, EthersError].} =
## Waits for a transaction to be mined and for the specified number of blocks ## Waits for a transaction to be mined and for the specified number of blocks
## to pass since it was mined (confirmations). ## to pass since it was mined (confirmations).
## A timeout, in blocks, can be specified that will raise an error if too many ## A timeout, in blocks, can be specified that will raise an error if too many
@ -265,10 +291,10 @@ proc confirm*(tx: TransactionResponse,
await tx.provider.ensureSuccess(receipt) await tx.provider.ensureSuccess(receipt)
return receipt return receipt
proc confirm*(tx: Future[TransactionResponse], proc confirm*(
tx: Future[TransactionResponse],
confirmations: int = EthersDefaultConfirmations, confirmations: int = EthersDefaultConfirmations,
timeout: int = EthersReceiptTimeoutBlks): timeout: int = EthersReceiptTimeoutBlks): Future[TransactionReceipt] {.async.} =
Future[TransactionReceipt] {.async.} =
## Convenience method that allows wait to be chained to a sendTransaction ## Convenience method that allows wait to be chained to a sendTransaction
## call, eg: ## call, eg:
## `await signer.sendTransaction(populated).confirm(3)` ## `await signer.sendTransaction(populated).confirm(3)`
@ -276,5 +302,5 @@ proc confirm*(tx: Future[TransactionResponse],
let txResp = await tx let txResp = await tx
return await txResp.confirm(confirmations, timeout) return await txResp.confirm(confirmations, timeout)
method close*(provider: Provider) {.async, base.} = method close*(provider: Provider) {.base, async: (raises:[ProviderError]).} =
discard discard

View File

@ -1,10 +1,10 @@
import std/json
import std/tables import std/tables
import std/uri import std/uri
import pkg/chronicles import pkg/chronicles
import pkg/eth/common/eth_types_json_serialization import pkg/eth/common/eth_types_json_serialization
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/json_rpc/errors import pkg/json_rpc/errors
import pkg/serde
import ../basics import ../basics
import ../provider import ../provider
import ../signer import ../signer
@ -12,7 +12,6 @@ import ./jsonrpc/rpccalls
import ./jsonrpc/conversions import ./jsonrpc/conversions
import ./jsonrpc/subscriptions import ./jsonrpc/subscriptions
export json
export basics export basics
export provider export provider
export chronicles export chronicles
@ -26,20 +25,25 @@ type
JsonRpcProvider* = ref object of Provider JsonRpcProvider* = ref object of Provider
client: Future[RpcClient] client: Future[RpcClient]
subscriptions: Future[JsonRpcSubscriptions] subscriptions: Future[JsonRpcSubscriptions]
JsonRpcSigner* = ref object of Signer
provider: JsonRpcProvider
address: ?Address
JsonRpcProviderError* = object of ProviderError JsonRpcProviderError* = object of ProviderError
JsonRpcSubscription* = ref object of Subscription JsonRpcSubscription* = ref object of Subscription
subscriptions: JsonRpcSubscriptions subscriptions: JsonRpcSubscriptions
id: JsonNode 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 var message = message
try: if json =? JsonNode.fromJson(message):
message = parseJson(message){"message"}.getStr if "message" in json:
except Exception: message = json{"message"}.getStr
discard
raise newException(JsonRpcProviderError, message) raise newException(JsonRpcProviderError, message)
template convertError(body) = template convertError(body) =
@ -47,9 +51,7 @@ template convertError(body) =
body body
except JsonRpcError as error: except JsonRpcError as error:
raiseJsonRpcProviderError(error.msg) raiseJsonRpcProviderError(error.msg)
# Catch all ValueErrors for now, at least until JsonRpcError is actually except CatchableError as error:
# raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151
except ValueError as error:
raiseJsonRpcProviderError(error.msg) raiseJsonRpcProviderError(error.msg)
# Provider # Provider
@ -60,14 +62,17 @@ const defaultPollingInterval = 4.seconds
proc jsonHeaders: seq[(string, string)] = proc jsonHeaders: seq[(string, string)] =
@[("Content-Type", "application/json")] @[("Content-Type", "application/json")]
proc new*(_: type JsonRpcProvider, proc new*(
_: type JsonRpcProvider,
url=defaultUrl, url=defaultUrl,
pollingInterval=defaultPollingInterval): JsonRpcProvider = pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} =
var initialized: Future[void] var initialized: Future[void]
var client: RpcClient var client: RpcClient
var subscriptions: JsonRpcSubscriptions var subscriptions: JsonRpcSubscriptions
proc initialize {.async.} = proc initialize {.async: (raises:[JsonRpcProviderError]).} =
convertError:
case parseUri(url).scheme case parseUri(url).scheme
of "ws", "wss": of "ws", "wss":
let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders) let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders)
@ -80,28 +85,45 @@ proc new*(_: type JsonRpcProvider,
client = http client = http
subscriptions = JsonRpcSubscriptions.new(http, subscriptions = JsonRpcSubscriptions.new(http,
pollingInterval = pollingInterval) pollingInterval = pollingInterval)
subscriptions.start()
proc awaitClient: Future[RpcClient] {.async.} = proc awaitClient: Future[RpcClient] {.async:(raises:[JsonRpcProviderError]).} =
convertError: convertError:
await initialized await initialized
return client return client
proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async.} = proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async:(raises:[JsonRpcProviderError]).} =
convertError: convertError:
await initialized await initialized
return subscriptions return subscriptions
initialized = initialize() initialized = initialize()
JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions())
proc send*(provider: JsonRpcProvider, proc callImpl(
client: RpcClient,
call: string, call: string,
arguments: seq[JsonNode] = @[]): Future[JsonNode] {.async.} = 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]).} =
convertError: convertError:
let client = await provider.client 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: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_accounts() return await client.eth_accounts()
@ -112,54 +134,66 @@ proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner =
proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner = proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
JsonRpcSigner(provider: provider, address: some address) JsonRpcSigner(provider: provider, address: some address)
method getBlockNumber*(provider: JsonRpcProvider): Future[UInt256] {.async.} = method getBlockNumber*(
provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_blockNumber() return await client.eth_blockNumber()
method getBlock*(provider: JsonRpcProvider, method getBlock*(
tag: BlockTag): Future[?Block] {.async.} = provider: JsonRpcProvider,
tag: BlockTag): Future[?Block] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getBlockByNumber(tag, false) return await client.eth_getBlockByNumber(tag, false)
method call*(provider: JsonRpcProvider, method call*(
provider: JsonRpcProvider,
tx: Transaction, tx: Transaction,
blockTag = BlockTag.latest): Future[seq[byte]] {.async.} = blockTag = BlockTag.latest): Future[seq[byte]] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_call(tx, blockTag) return await client.eth_call(tx, blockTag)
method getGasPrice*(provider: JsonRpcProvider): Future[UInt256] {.async.} = method getGasPrice*(
provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_gasPrice() return await client.eth_gasPrice()
method getTransactionCount*(provider: JsonRpcProvider, method getTransactionCount*(
provider: JsonRpcProvider,
address: Address, address: Address,
blockTag = BlockTag.latest): blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} =
Future[UInt256] {.async.} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag) return await client.eth_getTransactionCount(address, blockTag)
method getTransaction*(provider: JsonRpcProvider, method getTransaction*(
txHash: TransactionHash): provider: JsonRpcProvider,
Future[?PastTransaction] {.async.} = txHash: TransactionHash): Future[?PastTransaction] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionByHash(txHash) return await client.eth_getTransactionByHash(txHash)
method getTransactionReceipt*(provider: JsonRpcProvider, method getTransactionReceipt*(
txHash: TransactionHash): provider: JsonRpcProvider,
Future[?TransactionReceipt] {.async.} = txHash: TransactionHash): Future[?TransactionReceipt] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionReceipt(txHash) return await client.eth_getTransactionReceipt(txHash)
method getLogs*(provider: JsonRpcProvider, method getLogs*(
filter: EventFilter): provider: JsonRpcProvider,
Future[seq[Log]] {.async.} = filter: EventFilter): Future[seq[Log]] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
let logsJson = if filter of Filter: let logsJson = if filter of Filter:
@ -171,19 +205,23 @@ method getLogs*(provider: JsonRpcProvider,
var logs: seq[Log] = @[] var logs: seq[Log] = @[]
for logJson in logsJson.getElems: for logJson in logsJson.getElems:
if log =? Log.fromJson(logJson).catch: if log =? Log.fromJson(logJson):
logs.add log logs.add log
return logs return logs
method estimateGas*(provider: JsonRpcProvider, method estimateGas*(
provider: JsonRpcProvider,
transaction: Transaction, transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.async.} = blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_estimateGas(transaction, blockTag) return await client.eth_estimateGas(transaction, blockTag)
method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} = method getChainId*(
provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
try: try:
@ -191,7 +229,11 @@ method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
except CatchableError: except CatchableError:
return parse(await client.net_version(), UInt256) 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: convertError:
let let
client = await provider.client client = await provider.client
@ -199,30 +241,36 @@ method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): F
return TransactionResponse(hash: hash, provider: provider) return TransactionResponse(hash: hash, provider: provider)
method subscribe*(provider: JsonRpcProvider, method subscribe*(
provider: JsonRpcProvider,
filter: EventFilter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler): Future[Subscription] {.async: (raises:[ProviderError]).} =
Future[Subscription] {.async.} =
convertError: convertError:
let subscriptions = await provider.subscriptions let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeLogs(filter, onLog) let id = await subscriptions.subscribeLogs(filter, onLog)
return JsonRpcSubscription(subscriptions: subscriptions, id: id) return JsonRpcSubscription(subscriptions: subscriptions, id: id)
method subscribe*(provider: JsonRpcProvider, method subscribe*(
onBlock: BlockHandler): provider: JsonRpcProvider,
Future[Subscription] {.async.} = onBlock: BlockHandler): Future[Subscription] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let subscriptions = await provider.subscriptions let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeBlocks(onBlock) let id = await subscriptions.subscribeBlocks(onBlock)
return JsonRpcSubscription(subscriptions: subscriptions, id: id) return JsonRpcSubscription(subscriptions: subscriptions, id: id)
method unsubscribe(subscription: JsonRpcSubscription) {.async.} = method unsubscribe*(
subscription: JsonRpcSubscription) {.async: (raises:[ProviderError]).} =
convertError: convertError:
let subscriptions = subscription.subscriptions let subscriptions = subscription.subscriptions
let id = subscription.id let id = subscription.id
await subscriptions.unsubscribe(id) await subscriptions.unsubscribe(id)
method close*(provider: JsonRpcProvider) {.async.} = method close*(
provider: JsonRpcProvider) {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
let subscriptions = await provider.subscriptions let subscriptions = await provider.subscriptions
@ -231,10 +279,32 @@ method close*(provider: JsonRpcProvider) {.async.} =
# Signer # 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 signer.provider
method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} = method getAddress*(
signer: JsonRpcSigner): Future[Address]
{.async: (raises:[ProviderError, SignerError]).} =
if address =? signer.address: if address =? signer.address:
return address return address
@ -242,18 +312,23 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
if accounts.len > 0: if accounts.len > 0:
return accounts[0] return accounts[0]
raiseJsonRpcProviderError "no address found" raiseJsonRpcSignerError "no address found"
method signMessage*(signer: JsonRpcSigner, method signMessage*(
message: seq[byte]): Future[seq[byte]] {.async.} = signer: JsonRpcSigner,
convertError: message: seq[byte]): Future[seq[byte]] {.async: (raises:[SignerError]).} =
convertSignerError:
let client = await signer.provider.client let client = await signer.provider.client
let address = await signer.getAddress() let address = await signer.getAddress()
return await client.eth_sign(address, message) return await client.eth_sign(address, message)
method sendTransaction*(signer: JsonRpcSigner, method sendTransaction*(
transaction: Transaction): Future[TransactionResponse] {.async.} = signer: JsonRpcSigner,
convertError: transaction: Transaction): Future[TransactionResponse]
{.async: (raises:[SignerError]).} =
convertSignerError:
if nonce =? transaction.nonce: if nonce =? transaction.nonce:
signer.updateNonce(nonce) signer.updateNonce(nonce)
let let

View File

@ -1,7 +1,9 @@
import std/json
import std/strformat import std/strformat
import std/strutils import std/strutils
import pkg/chronicles except fromJson, `%`, `%*`, toJson
import pkg/json_rpc/jsonmarshal import pkg/json_rpc/jsonmarshal
import pkg/questionable/results
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../basics import ../../basics
import ../../transaction import ../../transaction
@ -9,68 +11,57 @@ import ../../blocktag
import ../../provider import ../../provider
export jsonmarshal export jsonmarshal
export serde
export chronicles except fromJson, `%`, `%*`, toJson
type JsonSerializationError = object of EthersError {.push raises: [].}
template raiseSerializationError(message: string) = proc getOrRaise*[T, E](self: ?!T, exc: typedesc[E]): T {.raises: [E].} =
raise newException(JsonSerializationError, message) let val = self.valueOr:
raise newException(E, self.error.msg)
val
proc expectFields(json: JsonNode, expectedFields: varargs[string]) = template mapFailure*[T, V, E](
for fieldName in expectedFields: exp: Result[T, V],
if not json.hasKey(fieldName): exc: typedesc[E],
raiseSerializationError(fmt"'{fieldName}' field not found in ${json}") ): Result[T, ref CatchableError] =
## Convert `Result[T, E]` to `Result[E, ref CatchableError]`
##
func fromJson*(T: type, json: JsonNode, name = ""): T = exp.mapErr(proc (e: V): ref CatchableError = (ref exc)(msg: e.msg))
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)
# Address # Address
func `%`*(address: Address): JsonNode = func `%`*(address: Address): JsonNode =
%($address) %($address)
func fromJson*(json: JsonNode, name: string, result: var Address) = func fromJson(_: type Address, json: JsonNode): ?!Address =
if address =? Address.init(json.getStr()): expectJsonKind(Address, JString, json)
result = address without address =? Address.init(json.getStr), error:
else: return failure newException(SerializationError,
raise newException(ValueError, "\"" & name & "\"is not an Address") "Failed to convert '" & $json & "' to Address: " & error.msg)
success address
# UInt256 # UInt256
func `%`*(integer: UInt256): JsonNode = func `%`*(integer: UInt256): JsonNode =
%("0x" & toHex(integer)) %("0x" & toHex(integer))
func fromJson*(json: JsonNode, name: string, result: var UInt256) = func fromJson*(_: type UInt256, json: JsonNode): ?!UInt256 =
result = UInt256.fromHex(json.getStr()) without result =? UInt256.fromHex(json.getStr()).catch, error:
return UInt256.failure error.msg
# TransactionType success result
func fromJson*(json: JsonNode, name: string, result: var TransactionType) =
let val = fromHex[int](json.getStr)
result = TransactionType(val)
func `%`*(txType: TransactionType): JsonNode =
%("0x" & txType.int.toHex(1))
# Transaction # Transaction
# TODO: add option that ignores none Option[T]
# TODO: add name option (gasLimit => gas, sender => from)
func `%`*(transaction: Transaction): JsonNode = 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: if sender =? transaction.sender:
result["from"] = %sender result["from"] = %sender
if nonce =? transaction.nonce: if nonce =? transaction.nonce:
@ -84,105 +75,52 @@ func `%`*(transaction: Transaction): JsonNode =
# BlockTag # BlockTag
func `%`*(blockTag: BlockTag): JsonNode = func `%`*(tag: BlockTag): JsonNode =
%($blockTag) % $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) = case jsonVal:
if not (json.hasKey("data") and json.hasKey("topics")): of "earliest": return success BlockTag.earliest
raise newException(ValueError, "'data' and/or 'topics' fields not found") 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] # TransactionStatus | TransactionType
var topics: seq[Topic]
fromJson(json["data"], "data", data)
fromJson(json["topics"], "topics", topics)
result = Log(data: data, topics: topics)
# TransactionStatus func `%`*(e: TransactionStatus | TransactionType): JsonNode =
% ("0x" & e.int8.toHex(1))
func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) = proc fromJson*[E: TransactionStatus | TransactionType](
let val = fromHex[int](json.getStr) T: type E,
result = TransactionStatus(val) json: JsonNode
): ?!T =
expectJsonKind(string, JString, json)
let integer = ? fromHex[int](json.str).catch.mapFailure(SerializationError)
success T(integer)
func `%`*(status: TransactionStatus): JsonNode = ## Generic conversions to use nim-json instead of nim-json-serialization for
%("0x" & status.int.toHex(1)) ## 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) = writer.writeValue(%value)
# 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( proc readValue*[T: not JsonNode](
blockHash: BlockHash.fromJson(json["blockHash"], "blockHash"), r: var JsonReader[JrpcConv],
blockNumber: UInt256.fromJson(json["blockNumber"], "blockNumber"), result: var T) {.raises: [SerializationError, IOError].} =
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 = var json = r.readValue(JsonNode)
let json = %*{ result = T.fromJson(json).getOrRaise(SerializationError)
"blockHash": tx.blockHash,
"blockNumber": tx.blockNumber,
"from": tx.sender,
"gas": tx.gas,
"gasPrice": tx.gasPrice,
"hash": tx.hash,
"input": tx.input,
"nonce": tx.nonce,
"to": tx.to,
"transactionIndex": tx.transactionIndex,
"value": tx.value,
"v": tx.v,
"r": tx.r,
"s": tx.s
}
if txType =? tx.transactionType:
json["type"] = %txType
if chainId =? tx.chainId:
json["chainId"] = %chainId
return json
# TransactionReceipt
func fromJson*(json: JsonNode, name: string, result: var TransactionReceipt) =
# Deserializes a transaction receipt, eg eth_getTransactionReceipt.
# Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt
json.expectFields "transactionHash", "transactionIndex", "cumulativeGasUsed",
"effectiveGasPrice", "gasUsed", "logs", "logsBloom", "type",
"status"
result = TransactionReceipt(
transactionHash: fromJson(TransactionHash, json["transactionHash"], "transactionHash"),
transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"),
blockHash: fromJson(?BlockHash, json["blockHash"], "blockHash"),
blockNumber: fromJson(?UInt256, json["blockNumber"], "blockNumber"),
sender: fromJson(?Address, json["from"], "from"),
to: fromJson(?Address, json["to"], "to"),
cumulativeGasUsed: UInt256.fromJson(json["cumulativeGasUsed"], "cumulativeGasUsed"),
effectiveGasPrice: fromJson(?UInt256, json["effectiveGasPrice"], "effectiveGasPrice"),
gasUsed: UInt256.fromJson(json["gasUsed"], "gasUsed"),
contractAddress: fromJson(?Address, json["contractAddress"], "contractAddress"),
logs: seq[Log].fromJson(json["logs"], "logs"),
logsBloom: seq[byte].fromJson(json["logsBloom"], "logsBloom"),
transactionType: TransactionType.fromJson(json["type"], "type"),
status: TransactionStatus.fromJson(json["status"], "status")
)

View File

@ -2,5 +2,5 @@ template untilCancelled*(body) =
try: try:
while true: while true:
body body
except CancelledError: except CancelledError as e:
raise raise e

View File

@ -4,6 +4,7 @@ import pkg/chronos
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import ../../basics import ../../basics
import ../../provider import ../../provider
include ../../nimshims/hashes
import ./rpccalls import ./rpccalls
import ./conversions import ./conversions
import ./looping import ./looping
@ -12,8 +13,41 @@ type
JsonRpcSubscriptions* = ref object of RootObj JsonRpcSubscriptions* = ref object of RootObj
client: RpcClient client: RpcClient
callbacks: Table[JsonNode, SubscriptionCallback] callbacks: Table[JsonNode, SubscriptionCallback]
methodHandlers: Table[string, MethodHandler]
MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].}
SubscriptionCallback = proc(id, arguments: 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, method subscribeBlocks*(subscriptions: JsonRpcSubscriptions,
onBlock: BlockHandler): onBlock: BlockHandler):
Future[JsonNode] Future[JsonNode]
@ -40,11 +74,11 @@ method close*(subscriptions: JsonRpcSubscriptions) {.async, base.} =
proc getCallback(subscriptions: JsonRpcSubscriptions, proc getCallback(subscriptions: JsonRpcSubscriptions,
id: JsonNode): ?SubscriptionCallback = id: JsonNode): ?SubscriptionCallback =
try: try:
if subscriptions.callbacks.hasKey(id): if not id.isNil and id in subscriptions.callbacks:
subscriptions.callbacks[id].some subscriptions.callbacks[id].some
else: else:
SubscriptionCallback.none SubscriptionCallback.none
except Exception: except KeyError:
SubscriptionCallback.none SubscriptionCallback.none
# Web sockets # Web sockets
@ -54,20 +88,21 @@ type
proc new*(_: type JsonRpcSubscriptions, proc new*(_: type JsonRpcSubscriptions,
client: RpcWebSocketClient): JsonRpcSubscriptions = client: RpcWebSocketClient): JsonRpcSubscriptions =
let subscriptions = WebSocketSubscriptions(client: client) let subscriptions = WebSocketSubscriptions(client: client)
proc subscriptionHandler(arguments: JsonNode) {.raises:[].} = proc subscriptionHandler(arguments: JsonNode) {.raises:[].} =
if id =? arguments["subscription"].catch and let id = arguments{"subscription"} or newJString("")
callback =? subscriptions.getCallback(id): if callback =? subscriptions.getCallback(id):
callback(id, arguments) callback(id, arguments)
client.setMethodHandler("eth_subscription", subscriptionHandler) subscriptions.setMethodHandler("eth_subscription", subscriptionHandler)
subscriptions subscriptions
method subscribeBlocks(subscriptions: WebSocketSubscriptions, method subscribeBlocks(subscriptions: WebSocketSubscriptions,
onBlock: BlockHandler): onBlock: BlockHandler):
Future[JsonNode] Future[JsonNode]
{.async.} = {.async.} =
proc callback(id, arguments: JsonNode) = proc callback(id, arguments: JsonNode) {.raises: [].} =
if blck =? Block.fromJson(arguments["result"]).catch: if blck =? Block.fromJson(arguments{"result"}):
onBlock(blck) onBlock(blck)
let id = await subscriptions.client.eth_subscribe("newHeads") let id = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[id] = callback subscriptions.callbacks[id] = callback
@ -79,13 +114,13 @@ method subscribeLogs(subscriptions: WebSocketSubscriptions,
Future[JsonNode] Future[JsonNode]
{.async.} = {.async.} =
proc callback(id, arguments: JsonNode) = proc callback(id, arguments: JsonNode) =
if log =? Log.fromJson(arguments["result"]).catch: if log =? Log.fromJson(arguments{"result"}):
onLog(log) onLog(log)
let id = await subscriptions.client.eth_subscribe("logs", filter) let id = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.callbacks[id] = callback subscriptions.callbacks[id] = callback
return id return id
method unsubscribe(subscriptions: WebSocketSubscriptions, method unsubscribe*(subscriptions: WebSocketSubscriptions,
id: JsonNode) id: JsonNode)
{.async.} = {.async.} =
subscriptions.callbacks.del(id) subscriptions.callbacks.del(id)
@ -140,7 +175,7 @@ method subscribeBlocks(subscriptions: PollingSubscriptions,
discard discard
proc callback(id, change: JsonNode) = proc callback(id, change: JsonNode) =
if hash =? BlockHash.fromJson(change).catch: if hash =? BlockHash.fromJson(change):
asyncSpawn getBlock(hash) asyncSpawn getBlock(hash)
let id = await subscriptions.client.eth_newBlockFilter() let id = await subscriptions.client.eth_newBlockFilter()
@ -154,14 +189,14 @@ method subscribeLogs(subscriptions: PollingSubscriptions,
{.async.} = {.async.} =
proc callback(id, change: JsonNode) = proc callback(id, change: JsonNode) =
if log =? Log.fromJson(change).catch: if log =? Log.fromJson(change):
onLog(log) onLog(log)
let id = await subscriptions.client.eth_newFilter(filter) let id = await subscriptions.client.eth_newFilter(filter)
subscriptions.callbacks[id] = callback subscriptions.callbacks[id] = callback
return id return id
method unsubscribe(subscriptions: PollingSubscriptions, method unsubscribe*(subscriptions: PollingSubscriptions,
id: JsonNode) id: JsonNode)
{.async.} = {.async.} =
subscriptions.callbacks.del(id) subscriptions.callbacks.del(id)

View File

@ -1,14 +1,15 @@
import pkg/questionable
import ./basics import ./basics
import ./provider import ./provider
export basics export basics
{.push raises: [].}
type type
Signer* = ref object of RootObj Signer* = ref object of RootObj
lastSeenNonce: ?UInt256 lastSeenNonce: ?UInt256
populateLock: AsyncLock populateLock: AsyncLock
type
SignerError* = object of EthersError SignerError* = object of EthersError
EstimateGasError* = object of SignerError EstimateGasError* = object of SignerError
transaction*: Transaction transaction*: Transaction
@ -19,50 +20,87 @@ template raiseSignerError(message: string, parent: ref ProviderError = nil) =
proc raiseEstimateGasError( proc raiseEstimateGasError(
transaction: Transaction, transaction: Transaction,
parent: ref ProviderError = nil parent: ref ProviderError = nil
) = ) {.raises: [EstimateGasError] .} =
let e = (ref EstimateGasError)( let e = (ref EstimateGasError)(
msg: "Estimate gas failed", msg: "Estimate gas failed",
transaction: transaction, transaction: transaction,
parent: parent) parent: parent)
raise e 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" 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" doAssert false, "not implemented"
method signMessage*(signer: Signer, method signMessage*(
message: seq[byte]): Future[seq[byte]] {.base, async.} = signer: Signer,
message: seq[byte]): Future[seq[byte]]
{.base, async: (raises: [SignerError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*(signer: Signer, method sendTransaction*(
transaction: Transaction): Future[TransactionResponse] {.base, async.} = signer: Signer,
transaction: Transaction): Future[TransactionResponse]
{.base, async: (raises:[SignerError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getGasPrice*(signer: Signer): Future[UInt256] {.base, gcsafe.} = method getGasPrice*(
signer.provider.getGasPrice() signer: Signer): Future[UInt256]
{.base, async: (raises: [ProviderError, SignerError]).} =
method getTransactionCount*(signer: Signer, return await signer.provider.getGasPrice()
blockTag = BlockTag.latest):
Future[UInt256] {.base, async.} = method getTransactionCount*(
signer: Signer,
blockTag = BlockTag.latest): Future[UInt256]
{.base, async: (raises:[SignerError]).} =
convertError:
let address = await signer.getAddress() let address = await signer.getAddress()
return await signer.provider.getTransactionCount(address, blockTag) return await signer.provider.getTransactionCount(address, blockTag)
method estimateGas*(signer: Signer, method estimateGas*(
signer: Signer,
transaction: Transaction, transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, async.} = blockTag = BlockTag.latest): Future[UInt256]
{.base, async: (raises:[SignerError]).} =
var transaction = transaction var transaction = transaction
transaction.sender = some(await signer.getAddress) var address: Address
convertError:
address = await signer.getAddress
transaction.sender = some(address)
try: try:
return await signer.provider.estimateGas(transaction) return await signer.provider.estimateGas(transaction)
except ProviderError as e: except ProviderError as e:
raiseEstimateGasError transaction, e raiseEstimateGasError transaction, e
method getChainId*(signer: Signer): Future[UInt256] {.base, gcsafe.} = method getChainId*(
signer.provider.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) var nonce = await signer.getTransactionCount(BlockTag.pending)
if lastSeen =? signer.lastSeenNonce and lastSeen >= nonce: if lastSeen =? signer.lastSeenNonce and lastSeen >= nonce:
@ -87,11 +125,16 @@ method decreaseNonce*(signer: Signer) {.base, gcsafe.} =
if lastSeen =? signer.lastSeenNonce and lastSeen > 0: if lastSeen =? signer.lastSeenNonce and lastSeen > 0:
signer.lastSeenNonce = some lastSeen - 1 signer.lastSeenNonce = some lastSeen - 1
method populateTransaction*(signer: Signer, method populateTransaction*(
transaction: Transaction): signer: Signer,
Future[Transaction] {.base, async.} = 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") raiseSignerError("from address mismatch")
if chainId =? transaction.chainId and chainId != await signer.getChainId(): if chainId =? transaction.chainId and chainId != await signer.getChainId():
raiseSignerError("chain id mismatch") raiseSignerError("chain id mismatch")
@ -105,7 +148,7 @@ method populateTransaction*(signer: Signer,
try: try:
if transaction.sender.isNone: if transaction.sender.isNone:
populated.sender = some(await signer.getAddress()) populated.sender = some(address)
if transaction.chainId.isNone: if transaction.chainId.isNone:
populated.chainId = some(await signer.getChainId()) populated.chainId = some(await signer.getChainId())
if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone): 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()) populated.nonce = some(await signer.getNonce())
try: try:
populated.gasLimit = some(await signer.estimateGas(populated)) populated.gasLimit = some(await signer.estimateGas(populated))
except ProviderError, EstimateGasError: except EstimateGasError as e:
let e = getCurrentException()
signer.decreaseNonce() signer.decreaseNonce()
raise e raise e
except ProviderError as e:
signer.decreaseNonce()
raiseSignerError(e.msg)
else: else:
if transaction.nonce.isNone: if transaction.nonce.isNone:
@ -138,7 +183,7 @@ method populateTransaction*(signer: Signer,
method cancelTransaction*( method cancelTransaction*(
signer: Signer, signer: Signer,
tx: Transaction tx: Transaction
): Future[TransactionResponse] {.async, base.} = ): Future[TransactionResponse] {.base, async: (raises: [SignerError]).} =
# cancels a transaction by sending with a 0-valued transaction to ourselves # cancels a transaction by sending with a 0-valued transaction to ourselves
# with the failed tx's nonce # with the failed tx's nonce
@ -148,5 +193,6 @@ method cancelTransaction*(
raiseSignerError "transaction must have nonce" raiseSignerError "transaction must have nonce"
var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce) var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce)
convertError:
cancelTx = await signer.populateTransaction(cancelTx) cancelTx = await signer.populateTransaction(cancelTx)
return await signer.sendTransaction(cancelTx) return await signer.sendTransaction(cancelTx)

View File

@ -0,0 +1,3 @@
import ../providers/jsonrpc
export provider, getAddress, signMessage, sendTransaction

92
ethers/signers/wallet.nim Normal file
View File

@ -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)

View File

@ -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)

View File

@ -2,9 +2,9 @@ import pkg/eth/keys
import pkg/eth/rlp import pkg/eth/rlp
import pkg/eth/common/transaction as eth import pkg/eth/common/transaction as eth
import pkg/eth/common/eth_hash import pkg/eth/common/eth_hash
import ../basics import ../../basics
import ../transaction as ethers import ../../transaction as ethers
import ../provider import ../../provider
import ./error import ./error
type type

View File

@ -27,7 +27,7 @@ proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
else: else:
discard await call discard await call
return false return false
except ProviderError, EstimateGasError: except ProviderError, SignerError, EstimateGasError:
return true return true
proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} = 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: else:
discard await call discard await call
return false return false
except ProviderError, EstimateGasError: except ProviderError, SignerError, EstimateGasError:
let e = getCurrentException() let e = getCurrentException()
var passed = reason == (ref EthersError)(e).revertReason var passed = reason == (ref EthersError)(e).revertReason
if not passed and if not passed and

View File

@ -1,3 +1,4 @@
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ./basics import ./basics
@ -6,8 +7,8 @@ type
Legacy = 0, Legacy = 0,
AccessList = 1, AccessList = 1,
Dynamic = 2 Dynamic = 2
Transaction* = object Transaction* {.serialize.} = object
sender*: ?Address sender* {.serialize("from").}: ?Address
to*: Address to*: Address
data*: seq[byte] data*: seq[byte]
value*: UInt256 value*: UInt256
@ -17,7 +18,7 @@ type
maxFee*: ?UInt256 maxFee*: ?UInt256
maxPriorityFee*: ?UInt256 maxPriorityFee*: ?UInt256
gasLimit*: ?UInt256 gasLimit*: ?UInt256
transactionType*: ?TransactionType transactionType* {.serialize("type").}: ?TransactionType
func `$`*(transaction: Transaction): string = func `$`*(transaction: Transaction): string =
result = "(" result = "("

View File

@ -1,76 +1,3 @@
import eth/keys import ./signers/wallet
import ./basics
import ./provider
import ./transaction
import ./signer
import ./wallet/error
import ./wallet/signing
export keys export wallet
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)

View File

@ -1,7 +0,0 @@
import ../basics
type
WalletError* = object of EthersError
func raiseWalletError*(message: string) =
raise newException(WalletError, message)

View File

@ -11,10 +11,15 @@ func new*(_: type MockSigner, provider: Provider): MockSigner =
method provider*(signer: MockSigner): Provider = method provider*(signer: MockSigner): Provider =
signer.provider signer.provider
method getAddress*(signer: MockSigner): Future[Address] {.async.} = method getAddress*(
signer: MockSigner): Future[Address]
{.async: (raises:[ProviderError, SignerError]).} =
return signer.address return signer.address
method sendTransaction*(signer: MockSigner, method sendTransaction*(
transaction: Transaction): signer: MockSigner,
Future[TransactionResponse] {.async.} = transaction: Transaction): Future[TransactionResponse]
{.async: (raises:[SignerError]).} =
signer.transactions.add(transaction) signer.transactions.add(transaction)

View File

@ -2,6 +2,9 @@ import std/strutils
import std/unittest import std/unittest
import pkg/ethers/provider import pkg/ethers/provider
import pkg/ethers/providers/jsonrpc/conversions import pkg/ethers/providers/jsonrpc/conversions
import pkg/questionable
import pkg/questionable/results
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
func flatten(s: string): string = func flatten(s: string): string =
@ -17,14 +20,14 @@ suite "JSON Conversions":
"timestamp":"0x6285c293" "timestamp":"0x6285c293"
} }
var blk = Block.fromJson(json) let blk1 = !Block.fromJson(json)
check blk.number.isNone check blk1.number.isNone
json["number"] = newJString("") json["number"] = newJString("")
blk = Block.fromJson(json) let blk2 = !Block.fromJson(json)
check blk.number.isSome check blk2.number.isSome
check blk.number.get.isZero check blk2.number.get.isZero
test "missing block hash in Block isNone": 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 check blk.hash.isNone
test "missing block number in TransactionReceipt isNone": test "missing block number in TransactionReceipt isNone":
@ -67,13 +71,15 @@ suite "JSON Conversions":
"type": "0x0" "type": "0x0"
} }
var receipt = TransactionReceipt.fromJson(json) without receipt1 =? TransactionReceipt.fromJson(json):
check receipt.blockNumber.isNone fail
check receipt1.blockNumber.isNone
json["blockNumber"] = newJString("") json["blockNumber"] = newJString("")
receipt = TransactionReceipt.fromJson(json) without receipt2 =? TransactionReceipt.fromJson(json):
check receipt.blockNumber.isSome fail
check receipt.blockNumber.get.isZero check receipt2.blockNumber.isSome
check receipt2.blockNumber.get.isZero
test "missing block hash in TransactionReceipt isNone": test "missing block hash in TransactionReceipt isNone":
let json = %*{ let json = %*{
@ -102,33 +108,10 @@ suite "JSON Conversions":
"type": "0x0" "type": "0x0"
} }
let receipt = TransactionReceipt.fromJson(json) without receipt =? TransactionReceipt.fromJson(json):
fail
check receipt.blockHash.isNone check receipt.blockHash.isNone
test "newHeads subcription raises exception when deserializing to Log":
let json = """{
"parentHash":"0xd68d4d0f29307df51e1284fc8a13595ae700ef0f1128830a69e6854381363d42",
"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"miner":"0x0000000000000000000000000000000000000000",
"stateRoot":"0x1f6f2d05de35bbfd50213be96ddf960d62b978b472c55d6ac223cd648cbbbbb0",
"transactionsRoot":"0xb9bb8a26abe091bb628ab2b6585c5af151aeb3984f4ba47a3c65d438283e069d",
"receiptsRoot":"0x33f229b7133e1ba3fb524b8af22d8184ca10b2da5bb170092a219c61ca023c1d",
"logsBloom":"0x00000000000000000000000000000000000000000020000000000002000000000000000000000000000000000000000000000000000008080000100200200000000000000000000000000008000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000010000040000000000100000000000800000000000000000000000000000000020000000000020000000000000000000000000000040000008000000000000000000020000000000002000000000000000000000000000000000000000000000000000001000010000000000000000020002000000020000000000000008002000000000000",
"difficulty":"0x2",
"number":"0x21d",
"gasLimit":"0x1c1b59a7",
"gasUsed":"0xda41b",
"timestamp":"0x6509410e",
"extraData":"0xd883010b05846765746888676f312e32302e32856c696e7578000000000000007102a27d75709b90ca9eb23cdaaccf4fc2d571d710f3bc5a7dc874f43af116a93ff832576a53c16f0d0aa1cd9e9a1dc0a60126c4d420f72b0866fc96ba6664f601",
"mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x0000000000000000",
"baseFeePerGas":"0x7",
"withdrawalsRoot":null,
"hash":"0x64066c7150c660e5357c4b6b02d836c10353dfa8edb32c805fca9367fd29c6e7"
}"""
expect ValueError:
discard Log.fromJson(parseJson(json))
test "correctly deserializes PastTransaction": test "correctly deserializes PastTransaction":
let json = %*{ let json = %*{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922", "blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
@ -149,7 +132,8 @@ suite "JSON Conversions":
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
} }
let tx = PastTransaction.fromJson(json) without tx =? PastTransaction.fromJson(json):
fail
check tx.blockHash == BlockHash(array[32, byte].fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922")) check tx.blockHash == BlockHash(array[32, byte].fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922"))
check tx.blockNumber == 0x22e.u256 check tx.blockNumber == 0x22e.u256
check tx.sender == Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get check tx.sender == Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get
@ -198,12 +182,12 @@ suite "JSON Conversions":
"nonce":"0x3", "nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e", "to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3", "transactionIndex":"0x3",
"type":"0x0",
"chainId":"0xc0de4",
"value":"0x0", "value":"0x0",
"v":"0x181bec", "v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a", "r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2", "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
"type":"0x0",
"chainId":"0xc0de4"
}""".flatten }""".flatten
check $(%tx) == expected check $(%tx) == expected
@ -227,10 +211,12 @@ suite "JSON Conversions":
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2" "s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
} }
let past = PastTransaction.fromJson(json) without past =? PastTransaction.fromJson(json):
fail
check %past.toTransaction == %*{ check %past.toTransaction == %*{
"to": !Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e"), "to": !Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e"),
"data": hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"), "data": hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"),
"value": "0x0",
"from": !Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34"), "from": !Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34"),
"nonce": 0x3.u256, "nonce": 0x3.u256,
"chainId": 0xc0de4.u256, "chainId": 0xc0de4.u256,

View File

@ -1,4 +1,3 @@
import std/json
import pkg/asynctest import pkg/asynctest
import pkg/chronos import pkg/chronos
import pkg/ethers import pkg/ethers
@ -97,5 +96,5 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
discard await provider.getBlock(BlockTag.latest) discard await provider.getBlock(BlockTag.latest)
expect JsonRpcProviderError: expect JsonRpcProviderError:
discard await provider.subscribe(proc(_: Block) = discard) discard await provider.subscribe(proc(_: Block) = discard)
expect JsonRpcProviderError: expect JsonRpcSignerError:
discard await provider.getSigner().sendTransaction(Transaction.example) discard await provider.getSigner().sendTransaction(Transaction.example)

View File

@ -64,6 +64,7 @@ suite "Web socket subscriptions":
client = newRpcWebSocketClient() client = newRpcWebSocketClient()
await client.connect("ws://localhost:8545") await client.connect("ws://localhost:8545")
subscriptions = JsonRpcSubscriptions.new(client) subscriptions = JsonRpcSubscriptions.new(client)
subscriptions.start()
teardown: teardown:
await subscriptions.close() await subscriptions.close()
@ -81,6 +82,7 @@ suite "HTTP polling subscriptions":
await client.connect("http://localhost:8545") await client.connect("http://localhost:8545")
subscriptions = JsonRpcSubscriptions.new(client, subscriptions = JsonRpcSubscriptions.new(client,
pollingInterval = 100.millis) pollingInterval = 100.millis)
subscriptions.start()
teardown: teardown:
await subscriptions.close() await subscriptions.close()

View File

@ -1,5 +1,6 @@
import pkg/asynctest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/serde
import ./hardhat import ./hardhat
type type

View File

@ -1,5 +1,6 @@
import pkg/asynctest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/serde
import ./hardhat import ./hardhat
type type

View File

@ -3,6 +3,7 @@ import pkg/asynctest
import pkg/chronos import pkg/chronos
import pkg/ethers import pkg/ethers
import pkg/ethers/testing import pkg/ethers/testing
import pkg/serde
import ./helpers import ./helpers
suite "Testing helpers": suite "Testing helpers":
@ -31,12 +32,15 @@ suite "Testing helpers":
test "reverts only checks ProviderErrors, EstimateGasErrors": test "reverts only checks ProviderErrors, EstimateGasErrors":
proc callProviderError() {.async.} = proc callProviderError() {.async.} =
raise newException(ProviderError, "test") raise newException(ProviderError, "test")
proc callSignerError() {.async.} =
raise newException(SignerError, "test")
proc callEstimateGasError() {.async.} = proc callEstimateGasError() {.async.} =
raise newException(EstimateGasError, "test") raise newException(EstimateGasError, "test")
proc callEthersError() {.async.} = proc callEthersError() {.async.} =
raise newException(EthersError, "test") raise newException(EthersError, "test")
check await callProviderError().reverts() check await callProviderError().reverts()
check await callSignerError().reverts()
check await callEstimateGasError().reverts() check await callEstimateGasError().reverts()
expect EthersError: expect EthersError:
check await callEthersError().reverts() check await callEthersError().reverts()
@ -44,12 +48,15 @@ suite "Testing helpers":
test "reverts with reason only checks ProviderErrors, EstimateGasErrors": test "reverts with reason only checks ProviderErrors, EstimateGasErrors":
proc callProviderError() {.async.} = proc callProviderError() {.async.} =
raise newException(ProviderError, revertReason) raise newException(ProviderError, revertReason)
proc callSignerError() {.async.} =
raise newException(SignerError, revertReason)
proc callEstimateGasError() {.async.} = proc callEstimateGasError() {.async.} =
raise newException(EstimateGasError, revertReason) raise newException(EstimateGasError, revertReason)
proc callEthersError() {.async.} = proc callEthersError() {.async.} =
raise newException(EthersError, revertReason) raise newException(EthersError, revertReason)
check await callProviderError().reverts(revertReason) check await callProviderError().reverts(revertReason)
check await callSignerError().reverts(revertReason)
check await callEstimateGasError().reverts(revertReason) check await callEstimateGasError().reverts(revertReason)
expect EthersError: expect EthersError:
check await callEthersError().reverts(revertReason) check await callEthersError().reverts(revertReason)

View File

@ -1,4 +1,5 @@
import pkg/asynctest import pkg/asynctest
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ../ethers import ../ethers