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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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 = "("

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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