Merge 86b9a02054833816d704f39cc4f5e3fcd073481f into 965b8cd752544df96b5effecbbd27a8f56a25d62

This commit is contained in:
markspanbroek 2025-12-13 07:32:50 +01:00 committed by GitHub
commit cc05b1ee31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 413 additions and 956 deletions

View File

@ -33,7 +33,7 @@ JSON-RPC provider is supported:
import ethers
import chronos
let provider = JsonRpcProvider.new("ws://localhost:8545")
let provider = await JsonRpcProvider.connect("ws://localhost:8545")
let accounts = await provider.listAccounts()
```
@ -137,11 +137,8 @@ You can now subscribe to Transfer events by calling `subscribe` on the contract
instance.
```nim
proc handleTransfer(transferResult: ?!Transfer) =
if transferResult.isOk:
echo "received transfer: ", transferResult.value
else:
echo "error during transfer: ", transferResult.error.msg
proc handleTransfer(transferResult: Transfer) =
echo "received transfer: ", transferResult.value
let subscription = await token.subscribe(Transfer, handleTransfer)
```
@ -204,13 +201,6 @@ This library ships with some optional modules that provides convenience utilitie
- `ethers/erc20` module provides you with ERC20 token implementation and its events
Hardhat websockets workaround
---------
If you're working with Hardhat, you might encounter an issue where [websocket subscriptions stop working after 5 minutes](https://github.com/NomicFoundation/hardhat/issues/2053).
This library provides a workaround using the compile time `ws_resubscribe` symbol. When this symbol is defined and set to a value greater than 0, websocket subscriptions will automatically resubscribe after the amount of time (in seconds) specified. The recommended value is 240 seconds (4 minutes), eg `--define:ws_resubscribe=240`.
Contribution
------------

View File

@ -1,10 +1,2 @@
--styleCheck:usages
--styleCheck:error
# begin Nimble config (version 1)
when fileExists("nimble.paths"):
include "nimble.paths"
# end Nimble config
when (NimMajor, NimMinor) >= (2, 0):
--mm:refc

View File

@ -6,7 +6,7 @@ import ./contract
import ./events
import ./fields
type EventHandler*[E: Event] = proc(event: ?!E) {.gcsafe, raises:[].}
type EventHandler*[E: Event] = proc(event: E) {.gcsafe, raises:[].}
proc subscribe*[E: Event](contract: Contract,
_: type E,
@ -16,13 +16,9 @@ proc subscribe*[E: Event](contract: Contract,
let topic = topic($E, E.fieldTypes).toArray
let filter = EventFilter(address: contract.address, topics: @[topic])
proc logHandler(logResult: ?!Log) {.raises: [].} =
without log =? logResult, error:
handler(failure(E, error))
return
proc logHandler(log: Log) {.raises: [].} =
if event =? E.decode(log.data, log.topics):
handler(success(event))
handler(event)
contract.provider.subscribe(filter, logHandler)

View File

@ -4,7 +4,6 @@ type
SolidityError* = object of EthersError
ContractError* = object of EthersError
SignerError* = object of EthersError
SubscriptionError* = object of EthersError
ProviderError* = object of EthersError
data*: ?seq[byte]

View File

@ -27,10 +27,11 @@ type
FilterByBlockHash* {.serialize.} = ref object of EventFilter
blockHash*: BlockHash
Log* {.serialize.} = object
blockNumber*: UInt256
blockNumber*: BlockNumber
data*: seq[byte]
logIndex*: UInt256
removed*: bool
address*: Address
topics*: seq[Topic]
TransactionHash* = array[32, byte]
BlockHash* = array[32, byte]
@ -51,22 +52,24 @@ type
blockHash*: ?BlockHash
transactionHash*: TransactionHash
logs*: seq[Log]
blockNumber*: ?UInt256
blockNumber*: ?BlockNumber
cumulativeGasUsed*: UInt256
effectiveGasPrice*: ?UInt256
status*: TransactionStatus
transactionType* {.serialize("type"), deserialize("type").}: TransactionType
LogHandler* = proc(log: ?!Log) {.gcsafe, raises:[].}
BlockHandler* = proc(blck: ?!Block) {.gcsafe, raises:[].}
LogHandler* = proc(log: Log) {.gcsafe, raises:[].}
BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].}
Topic* = array[32, byte]
Block* {.serialize.} = object
number*: ?UInt256
number*: ?BlockNumber
timestamp*: UInt256
hash*: ?BlockHash
baseFeePerGas* : ?UInt256
logsBloom*: ?StUint[2048]
BlockNumber* = UInt256
PastTransaction* {.serialize.} = object
blockHash*: BlockHash
blockNumber*: UInt256
blockNumber*: BlockNumber
sender* {.serialize("from"), deserialize("from").}: Address
gas*: UInt256
gasPrice*: UInt256
@ -222,7 +225,7 @@ proc confirm*(
tx: TransactionResponse,
confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt]
{.async: (raises: [CancelledError, ProviderError, SubscriptionError, EthersError]).} =
{.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). The number of confirmations
@ -232,69 +235,40 @@ proc confirm*(
assert confirmations > 0
var blockNumber: UInt256
## We need initialized succesfull Result, because the first iteration of the `while` loop
## bellow is triggered "manually" by calling `await updateBlockNumber` and not by block
## subscription. If left uninitialized then the Result is in error state and error is raised.
## This result is not used for block value, but for block subscription errors.
var blockSubscriptionResult: ?!Block = success(Block(number: UInt256.none, timestamp: 0.u256, hash: BlockHash.none))
var blockNumber = await tx.provider.getBlockNumber()
let blockEvent = newAsyncEvent()
blockEvent.fire()
proc updateBlockNumber {.async: (raises: []).} =
try:
let number = await tx.provider.getBlockNumber()
if number > blockNumber:
blockNumber = number
blockEvent.fire()
except ProviderError, CancelledError:
# there's nothing we can do here
discard
proc onBlock(blckResult: ?!Block) =
blockSubscriptionResult = blckResult
if blckResult.isErr:
proc onBlock(blck: Block) =
if number =? blck.number:
blockNumber = number
blockEvent.fire()
return
# ignore block parameter; hardhat may call this with pending blocks
asyncSpawn updateBlockNumber()
await updateBlockNumber()
let subscription = await tx.provider.subscribe(onBlock)
let finish = blockNumber + timeout.u256
var receipt: ?TransactionReceipt
while true:
await blockEvent.wait()
blockEvent.clear()
try:
var receipt: ?TransactionReceipt
if blockSubscriptionResult.isErr:
let error = blockSubscriptionResult.error()
while true:
await blockEvent.wait()
blockEvent.clear()
if error of SubscriptionError:
raise (ref SubscriptionError)(error)
elif error of CancelledError:
raise (ref CancelledError)(error)
else:
raise error.toErr(ProviderError)
if blockNumber >= finish:
raise newException(EthersError, "tx not mined before timeout")
if blockNumber >= finish:
await subscription.unsubscribe()
raise newException(EthersError, "tx not mined before timeout")
if receipt.?blockNumber.isNone:
receipt = await tx.provider.getTransactionReceipt(tx.hash)
if receipt.?blockNumber.isNone:
receipt = await tx.provider.getTransactionReceipt(tx.hash)
without receipt =? receipt and txBlockNumber =? receipt.blockNumber:
continue
without receipt =? receipt and txBlockNumber =? receipt.blockNumber:
continue
if txBlockNumber + confirmations.u256 <= blockNumber + 1:
await subscription.unsubscribe()
await tx.provider.ensureSuccess(receipt)
return receipt
if txBlockNumber + confirmations.u256 <= blockNumber + 1:
await tx.provider.ensureSuccess(receipt)
return receipt
finally:
await subscription.unsubscribe()
proc confirm*(
tx: Future[TransactionResponse],

View File

@ -7,40 +7,27 @@ import pkg/json_rpc/errors
import pkg/serde
import ../basics
import ../provider
import ../subscriptions
import ../signer
import ./jsonrpc/rpccalls
import ./jsonrpc/conversions
import ./jsonrpc/subscriptions
import ./jsonrpc/errors
import ./jsonrpc/websocket
export basics
export provider
export chronicles
export errors.JsonRpcProviderError
export subscriptions
{.push raises: [].}
logScope:
topics = "ethers jsonrpc"
type
JsonRpcProvider* = ref object of Provider
client: Future[RpcClient]
subscriptions: Future[JsonRpcSubscriptions]
maxPriorityFeePerGas: UInt256
JsonRpcSubscription* = ref object of Subscription
subscriptions: JsonRpcSubscriptions
id: JsonNode
# Signer
JsonRpcSigner* = ref object of Signer
provider: JsonRpcProvider
address: ?Address
JsonRpcSignerError* = object of SignerError
# Provider
type JsonRpcProvider* = ref object of Provider
client: RpcClient
subscriptions: Subscriptions
maxPriorityFeePerGas: UInt256
const defaultUrl = "http://localhost:8545"
const defaultPollingInterval = 4.seconds
@ -49,48 +36,27 @@ const defaultMaxPriorityFeePerGas = 1_000_000_000.u256
proc jsonHeaders: seq[(string, string)] =
@[("Content-Type", "application/json")]
proc new*(
proc connect*(
_: type JsonRpcProvider,
url=defaultUrl,
pollingInterval=defaultPollingInterval,
maxPriorityFeePerGas=defaultMaxPriorityFeePerGas): JsonRpcProvider {.raises: [JsonRpcProviderError].} =
var initialized: Future[void]
var client: RpcClient
var subscriptions: JsonRpcSubscriptions
proc initialize() {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
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: (raises: [JsonRpcProviderError, CancelledError])
.} =
convertError:
await initialized
return client
proc awaitSubscriptions(): Future[JsonRpcSubscriptions] {.
async: (raises: [JsonRpcProviderError, CancelledError])
.} =
convertError:
await initialized
return subscriptions
initialized = initialize()
return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions(), maxPriorityFeePerGas: maxPriorityFeePerGas)
maxPriorityFeePerGas=defaultMaxPriorityFeePerGas
): Future[JsonRpcProvider] {.async:(raises: [JsonRpcProviderError, CancelledError]).} =
convertError:
let provider = JsonRpcProvider(maxPriorityFeePerGas: maxPriorityFeePerGas)
case parseUri(url).scheme
of "ws", "wss":
let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders)
await websocket.connect(url)
provider.client = websocket
provider.subscriptions = Subscriptions.new(provider, pollingInterval)
await provider.subscriptions.useWebsocketUpdates(websocket)
else:
let http = newRpcHttpClient(getHeaders = jsonHeaders)
await http.connect(url)
provider.client = http
provider.subscriptions = Subscriptions.new(provider, pollingInterval)
return provider
proc callImpl(
client: RpcClient, call: string, args: JsonNode
@ -98,8 +64,9 @@ proc callImpl(
try:
let response = await client.call(call, %args)
without json =? JsonNode.fromJson(response.string), error:
raiseJsonRpcProviderError "Failed to parse response '" & response.string & "': " &
error.msg
raiseJsonRpcProviderError(
"Failed to parse response '" & response.string & "': " & error.msg
)
return json
except CancelledError as error:
raise error
@ -110,57 +77,44 @@ proc send*(
provider: JsonRpcProvider, call: string, arguments: seq[JsonNode] = @[]
): Future[JsonNode] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.callImpl(call, %arguments)
return await provider.client.callImpl(call, %arguments)
proc listAccounts*(
provider: JsonRpcProvider
): Future[seq[Address]] {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_accounts()
proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner =
JsonRpcSigner(provider: provider)
proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
JsonRpcSigner(provider: provider, address: some address)
return await provider.client.eth_accounts()
method getBlockNumber*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_blockNumber()
return await provider.client.eth_blockNumber()
method getBlock*(
provider: JsonRpcProvider, tag: BlockTag
): Future[?Block] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_getBlockByNumber(tag, false)
return await provider.client.eth_getBlockByNumber(tag, false)
method call*(
provider: JsonRpcProvider, tx: Transaction, blockTag = BlockTag.latest
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_call(tx, blockTag)
return await provider.client.eth_call(tx, blockTag)
method getGasPrice*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_gasPrice()
return await provider.client.eth_gasPrice()
method getMaxPriorityFeePerGas*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [CancelledError]).} =
try:
convertError:
let client = await provider.client
return await client.eth_maxPriorityFeePerGas()
return await provider.client.eth_maxPriorityFeePerGas()
except JsonRpcProviderError:
# If the provider does not provide the implementation
# let's just remove the manual value
@ -170,35 +124,31 @@ method getTransactionCount*(
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag)
return await provider.client.eth_getTransactionCount(address, blockTag)
method getTransaction*(
provider: JsonRpcProvider, txHash: TransactionHash
): Future[?PastTransaction] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_getTransactionByHash(txHash)
return await provider.client.eth_getTransactionByHash(txHash)
method getTransactionReceipt*(
provider: JsonRpcProvider, txHash: TransactionHash
): Future[?TransactionReceipt] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
return await client.eth_getTransactionReceipt(txHash)
return await provider.client.eth_getTransactionReceipt(txHash)
method getLogs*(
provider: JsonRpcProvider, filter: EventFilter
): Future[seq[Log]] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
let logsJson =
if filter of Filter:
await client.eth_getLogs(Filter(filter))
await provider.client.eth_getLogs(Filter(filter))
elif filter of FilterByBlockHash:
await client.eth_getLogs(FilterByBlockHash(filter))
await provider.client.eth_getLogs(FilterByBlockHash(filter))
else:
await client.eth_getLogs(filter)
await provider.client.eth_getLogs(filter)
var logs: seq[Log] = @[]
for logJson in logsJson.getElems:
@ -214,8 +164,7 @@ method estimateGas*(
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
try:
convertError:
let client = await provider.client
return await client.eth_estimateGas(transaction, blockTag)
return await provider.client.eth_estimateGas(transaction, blockTag)
except ProviderError as error:
raise (ref EstimateGasError)(
msg: "Estimate gas failed: " & error.msg,
@ -228,47 +177,29 @@ method getChainId*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
try:
return await client.eth_chainId()
return await provider.client.eth_chainId()
except CancelledError as error:
raise error
except CatchableError:
return parse(await client.net_version(), UInt256)
return parse(await provider.client.net_version(), UInt256)
method sendTransaction*(
provider: JsonRpcProvider, rawTransaction: seq[byte]
): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let
client = await provider.client
hash = await client.eth_sendRawTransaction(rawTransaction)
let hash = await provider.client.eth_sendRawTransaction(rawTransaction)
return TransactionResponse(hash: hash, provider: provider)
method subscribe*(
provider: JsonRpcProvider, filter: EventFilter, onLog: LogHandler
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeLogs(filter, onLog)
return JsonRpcSubscription(subscriptions: subscriptions, id: id)
await provider.subscriptions.subscribe(filter, onLog)
method subscribe*(
provider: JsonRpcProvider, onBlock: BlockHandler
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeBlocks(onBlock)
return JsonRpcSubscription(subscriptions: subscriptions, id: id)
method unsubscribe*(
subscription: JsonRpcSubscription
) {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let subscriptions = subscription.subscriptions
let id = subscription.id
await subscriptions.unsubscribe(id)
await provider.subscriptions.subscribe(onBlock)
method isSyncing*(
provider: JsonRpcProvider
@ -282,12 +213,14 @@ method close*(
provider: JsonRpcProvider
) {.async: (raises: [ProviderError, CancelledError]).} =
convertError:
let client = await provider.client
let subscriptions = await provider.subscriptions
await subscriptions.close()
await client.close()
await provider.subscriptions.close()
await provider.client.close()
# Signer
type
JsonRpcSigner* = ref object of Signer
provider: JsonRpcProvider
address: ?Address
JsonRpcSignerError* = object of SignerError
proc raiseJsonRpcSignerError(
message: string) {.raises: [JsonRpcSignerError].} =
@ -308,6 +241,12 @@ template convertSignerError(body) =
except CatchableError as error:
raise newException(JsonRpcSignerError, error.msg)
proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner =
JsonRpcSigner(provider: provider)
proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
JsonRpcSigner(provider: provider, address: some address)
method provider*(signer: JsonRpcSigner): Provider
{.gcsafe, raises: [SignerError].} =
@ -329,9 +268,8 @@ method signMessage*(
signer: JsonRpcSigner, message: seq[byte]
): Future[seq[byte]] {.async: (raises: [SignerError, CancelledError]).} =
convertSignerError:
let client = await signer.provider.client
let address = await signer.getAddress()
return await client.personal_sign(message, address)
return await signer.provider.client.personal_sign(message, address)
method sendTransaction*(
signer: JsonRpcSigner, transaction: Transaction
@ -339,8 +277,5 @@ method sendTransaction*(
async: (raises: [SignerError, ProviderError, CancelledError])
.} =
convertError:
let
client = await signer.provider.client
hash = await client.eth_sendTransaction(transaction)
let hash = await signer.provider.client.eth_sendTransaction(transaction)
return TransactionResponse(hash: hash, provider: signer.provider)

View File

@ -1,379 +0,0 @@
import std/tables
import std/sequtils
import std/strutils
import pkg/chronos
import pkg/questionable
import pkg/json_rpc/rpcclient
import pkg/serde
import ../../basics
import ../../errors
import ../../provider
include ../../nimshims/hashes
import ./rpccalls
import ./conversions
export serde
type
JsonRpcSubscriptions* = ref object of RootObj
client: RpcClient
callbacks: Table[JsonNode, SubscriptionCallback]
methodHandlers: Table[string, MethodHandler]
# Used by both PollingSubscriptions and WebsocketSubscriptions to store
# subscription filters so the subscriptions can be recreated. With
# PollingSubscriptions, the RPC node might prune/forget about them, and with
# WebsocketSubscriptions, when using hardhat, subscriptions are dropped after 5
# minutes.
logFilters: Table[JsonNode, EventFilter]
MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].}
SubscriptionCallback = proc(id: JsonNode, arguments: ?!JsonNode) {.gcsafe, raises:[].}
{.push raises:[].}
template convertErrorsToSubscriptionError(body) =
try:
body
except CancelledError as error:
raise error
except CatchableError as error:
raise error.toErr(SubscriptionError)
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]
{.async: (raises: [SubscriptionError, CancelledError]), base,.} =
raiseAssert "not implemented"
method subscribeLogs*(subscriptions: JsonRpcSubscriptions,
filter: EventFilter,
onLog: LogHandler):
Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]), base.} =
raiseAssert "not implemented"
method unsubscribe*(subscriptions: JsonRpcSubscriptions,
id: JsonNode)
{.async: (raises: [CancelledError]), base.} =
raiseAssert "not implemented "
method close*(subscriptions: JsonRpcSubscriptions) {.async: (raises: []), base.} =
let ids = toSeq subscriptions.callbacks.keys
for id in ids:
try:
await subscriptions.unsubscribe(id)
except CatchableError as e:
error "JsonRpc unsubscription failed", error = e.msg, id = id
proc getCallback(subscriptions: JsonRpcSubscriptions,
id: JsonNode): ?SubscriptionCallback {. raises:[].} =
try:
if not id.isNil and id in subscriptions.callbacks:
return subscriptions.callbacks[id].some
except: discard
# Web sockets
# Default re-subscription period is seconds
const WsResubscribe {.intdefine.}: int = 0
type
WebSocketSubscriptions = ref object of JsonRpcSubscriptions
logFiltersLock: AsyncLock
resubscribeFut: Future[void]
resubscribeInterval: int
template withLock*(subscriptions: WebSocketSubscriptions, body: untyped) =
if subscriptions.logFiltersLock.isNil:
subscriptions.logFiltersLock = newAsyncLock()
await subscriptions.logFiltersLock.acquire()
try:
body
finally:
subscriptions.logFiltersLock.release()
# This is a workaround to manage the 5 minutes limit due to hardhat.
# See https://github.com/NomicFoundation/hardhat/issues/2053#issuecomment-1061374064
proc resubscribeWebsocketEventsOnTimeout*(subscriptions: WebsocketSubscriptions) {.async: (raises: [CancelledError]).} =
while true:
await sleepAsync(subscriptions.resubscribeInterval.seconds)
try:
withLock(subscriptions):
for id, callback in subscriptions.callbacks:
var newId: JsonNode
if id in subscriptions.logFilters:
let filter = subscriptions.logFilters[id]
newId = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.logFilters[newId] = filter
subscriptions.logFilters.del(id)
else:
newId = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[newId] = callback
subscriptions.callbacks.del(id)
discard await subscriptions.client.eth_unsubscribe(id)
except CancelledError as e:
raise e
except CatchableError as e:
error "WS resubscription failed" , error = e.msg
proc new*(_: type JsonRpcSubscriptions,
client: RpcWebSocketClient,
resubscribeInterval = WsResubscribe): JsonRpcSubscriptions =
let subscriptions = WebSocketSubscriptions(client: client, resubscribeInterval: resubscribeInterval)
proc subscriptionHandler(arguments: JsonNode) {.raises:[].} =
let id = arguments{"subscription"} or newJString("")
if callback =? subscriptions.getCallback(id):
callback(id, success(arguments))
subscriptions.setMethodHandler("eth_subscription", subscriptionHandler)
if resubscribeInterval > 0:
if resubscribeInterval >= 300:
warn "Resubscription interval greater than 300 seconds is useless for hardhat workaround", resubscribeInterval = resubscribeInterval
subscriptions.resubscribeFut = resubscribeWebsocketEventsOnTimeout(subscriptions)
subscriptions
method subscribeBlocks(subscriptions: WebSocketSubscriptions,
onBlock: BlockHandler):
Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} =
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) {.raises: [].} =
without arguments =? argumentsResult, error:
onBlock(failure(Block, error.toErr(SubscriptionError)))
return
let res = Block.fromJson(arguments{"result"}).mapFailure(SubscriptionError)
onBlock(res)
convertErrorsToSubscriptionError:
withLock(subscriptions):
let id = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[id] = callback
return id
method subscribeLogs(subscriptions: WebSocketSubscriptions,
filter: EventFilter,
onLog: LogHandler):
Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} =
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) =
without arguments =? argumentsResult, error:
onLog(failure(Log, error.toErr(SubscriptionError)))
return
let res = Log.fromJson(arguments{"result"}).mapFailure(SubscriptionError)
onLog(res)
convertErrorsToSubscriptionError:
withLock(subscriptions):
let id = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
return id
method unsubscribe*(subscriptions: WebSocketSubscriptions,
id: JsonNode)
{.async: (raises: [CancelledError]).} =
try:
withLock(subscriptions):
subscriptions.callbacks.del(id)
discard await subscriptions.client.eth_unsubscribe(id)
except CancelledError as e:
raise e
except CatchableError:
# Ignore if uninstallation of the subscribiton fails.
discard
method close*(subscriptions: WebSocketSubscriptions) {.async: (raises: []).} =
await procCall JsonRpcSubscriptions(subscriptions).close()
if not subscriptions.resubscribeFut.isNil:
await subscriptions.resubscribeFut.cancelAndWait()
# Polling
type
PollingSubscriptions* = ref object of JsonRpcSubscriptions
polling: Future[void]
# Used when filters are recreated to translate from the id that user
# originally got returned to new filter id
subscriptionMapping: Table[JsonNode, JsonNode]
proc new*(_: type JsonRpcSubscriptions,
client: RpcHttpClient,
pollingInterval = 4.seconds): JsonRpcSubscriptions =
let subscriptions = PollingSubscriptions(client: client)
proc resubscribe(id: JsonNode): Future[?!void] {.async: (raises: [CancelledError]).} =
try:
var newId: JsonNode
# Log filters are stored in logFilters, block filters are not persisted
# there is they do not need any specific data for their recreation.
# We use this to determine if the filter was log or block filter here.
if subscriptions.logFilters.hasKey(id):
let filter = subscriptions.logFilters[id]
newId = await subscriptions.client.eth_newFilter(filter)
else:
newId = await subscriptions.client.eth_newBlockFilter()
subscriptions.subscriptionMapping[id] = newId
except CancelledError as e:
raise e
except CatchableError as e:
return failure(void, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
return success()
proc getChanges(id: JsonNode): Future[?!JsonNode] {.async: (raises: [CancelledError]).} =
if mappedId =? subscriptions.subscriptionMapping.?[id]:
try:
let changes = await subscriptions.client.eth_getFilterChanges(mappedId)
if changes.kind == JArray:
return success(changes)
except JsonRpcError as e:
if error =? (await resubscribe(id)).errorOption:
return failure(JsonNode, error)
# TODO: we could still miss some events between losing the subscription
# and resubscribing. We should probably adopt a strategy like ethers.js,
# whereby we keep track of the latest block number that we've seen
# filter changes for:
# https://github.com/ethers-io/ethers.js/blob/f97b92bbb1bde22fcc44100af78d7f31602863ab/packages/providers/src.ts/base-provider.ts#L977
if not ("filter not found" in e.msg):
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
except CancelledError as e:
raise e
except SubscriptionError as e:
return failure(JsonNode, e)
except CatchableError as e:
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
return success(newJArray())
proc poll(id: JsonNode) {.async: (raises: [CancelledError]).} =
without callback =? subscriptions.getCallback(id):
return
without changes =? (await getChanges(id)), error:
callback(id, failure(JsonNode, error))
return
for change in changes:
callback(id, success(change))
proc poll {.async: (raises: []).} =
try:
while true:
for id in toSeq subscriptions.callbacks.keys:
await poll(id)
await sleepAsync(pollingInterval)
except CancelledError:
discard
subscriptions.polling = poll()
asyncSpawn subscriptions.polling
subscriptions
method close*(subscriptions: PollingSubscriptions) {.async: (raises: []).} =
await subscriptions.polling.cancelAndWait()
await procCall JsonRpcSubscriptions(subscriptions).close()
method subscribeBlocks(subscriptions: PollingSubscriptions,
onBlock: BlockHandler):
Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} =
proc getBlock(hash: BlockHash) {.async: (raises:[]).} =
try:
if blck =? (await subscriptions.client.eth_getBlockByHash(hash, false)):
onBlock(success(blck))
except CancelledError:
discard
except CatchableError as e:
let error = e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription's block: " & e.msg)
onBlock(failure(Block, error))
proc callback(id: JsonNode, changeResult: ?!JsonNode) {.raises:[].} =
without change =? changeResult, e:
onBlock(failure(Block, e.toErr(SubscriptionError)))
return
if hash =? BlockHash.fromJson(change):
asyncSpawn getBlock(hash)
convertErrorsToSubscriptionError:
let id = await subscriptions.client.eth_newBlockFilter()
subscriptions.callbacks[id] = callback
subscriptions.subscriptionMapping[id] = id
return id
method subscribeLogs(subscriptions: PollingSubscriptions,
filter: EventFilter,
onLog: LogHandler):
Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} =
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) =
without arguments =? argumentsResult, error:
onLog(failure(Log, error.toErr(SubscriptionError)))
return
let res = Log.fromJson(arguments).mapFailure(SubscriptionError)
onLog(res)
convertErrorsToSubscriptionError:
let id = await subscriptions.client.eth_newFilter(filter)
subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
subscriptions.subscriptionMapping[id] = id
return id
method unsubscribe*(subscriptions: PollingSubscriptions,
id: JsonNode)
{.async: (raises: [CancelledError]).} =
try:
subscriptions.logFilters.del(id)
subscriptions.callbacks.del(id)
if sub =? subscriptions.subscriptionMapping.?[id]:
subscriptions.subscriptionMapping.del(id)
discard await subscriptions.client.eth_uninstallFilter(sub)
except CancelledError as e:
raise e
except CatchableError:
# Ignore if uninstallation of the filter fails. If it's the last step in our
# cleanup, then filter changes for this filter will no longer be polled so
# if the filter continues to live on in geth for whatever reason then it
# doesn't matter.
discard

View File

@ -0,0 +1,34 @@
import std/json
import pkg/json_rpc/rpcclient
import ../../basics
import ../../subscriptions
import ./rpccalls
import ./errors
proc useWebsocketUpdates*(
subscriptions: Subscriptions,
websocket: RpcWebSocketClient
) {.async:(raises:[JsonRpcProviderError, CancelledError]).} =
var rpcSubscriptionId: JsonNode
proc processMessage(client: RpcClient, message: string): Result[bool, string] =
without message =? parseJson(message).catch:
return ok true
without rpcMethod =? message{"method"}:
return ok true
if rpcMethod.getStr() != "eth_subscription":
return ok true
without rpcParameter =? message{"params"}{"subscription"}:
return ok true
if rpcParameter != rpcSubscriptionId:
return ok true
subscriptions.update()
ok false # do not process further using json-rpc default handler
assert websocket.onProcessMessage.isNil
websocket.onProcessMessage = processMessage
convertError:
rpcSubscriptionId = await websocket.eth_subscribe("newHeads")

View File

@ -58,7 +58,7 @@ method getGasPrice*(
method getMaxPriorityFeePerGas*(
signer: Signer
): Future[UInt256] {.async: (raises: [SignerError, CancelledError]).} =
): Future[UInt256] {.base, async: (raises: [SignerError, CancelledError]).} =
return await signer.provider.getMaxPriorityFeePerGas()
method getTransactionCount*(

127
ethers/subscriptions.nim Normal file
View File

@ -0,0 +1,127 @@
import std/tables
import std/sequtils
import ./basics
import ./provider
import ./subscriptions/blocksubscriber
import ./subscriptions/logsbloom
type
Subscriptions* = ref object
provider: Provider
blockSubscriber: BlockSubscriber
blockSubscriptions: Table[SubscriptionId, BlockHandler]
logSubscriptions: Table[SubscriptionId, (EventFilter, LogHandler)]
nextSubscriptionId: int
LocalSubscription* = ref object of Subscription
subscriptions: Subscriptions
id: SubscriptionId
SubscriptionId = int
func len*(subscriptions: Subscriptions): int =
subscriptions.blockSubscriptions.len + subscriptions.logSubscriptions.len
proc subscribe*(
subscriptions: Subscriptions,
onBlock: BlockHandler
): Future[Subscription] {.async:(raises:[ProviderError, CancelledError]).} =
let id = subscriptions.nextSubscriptionId
inc subscriptions.nextSubscriptionId
subscriptions.blockSubscriptions[id] = onBlock
await subscriptions.blockSubscriber.start()
LocalSubscription(subscriptions: subscriptions, id: id)
proc subscribe*(
subscriptions: Subscriptions,
filter: EventFilter,
onLog: LogHandler
): Future[Subscription] {.async:(raises:[ProviderError, CancelledError]).} =
let id = subscriptions.nextSubscriptionId
inc subscriptions.nextSubscriptionId
subscriptions.logSubscriptions[id] = (filter, onLog)
await subscriptions.blockSubscriber.start()
LocalSubscription(subscriptions: subscriptions, id: id)
method unsubscribe*(
subscription: LocalSubscription
) {.async:(raises:[ProviderError, CancelledError]).} =
let subscriptions = subscription.subscriptions
let id = subscription.id
subscriptions.logSubscriptions.del(id)
subscriptions.blockSubscriptions.del(id)
if subscriptions.len == 0:
await subscriptions.blockSubscriber.stop()
proc getLogs(
subscriptions: Subscriptions,
filter: EventFilter,
blockTag: BlockTag
): Future[seq[Log]] {.async:(raises:[ProviderError, CancelledError]).} =
let logFilter = Filter()
logFilter.address = filter.address
logFilter.topics = filter.topics
logFilter.fromBlock = blockTag
logFilter.toBlock = blockTag
await subscriptions.provider.getLogs(logFilter)
proc getLogs(
subscriptions: Subscriptions,
blck: Block
): Future[Table[SubscriptionId, seq[Log]]] {.
async:(raises:[ProviderError, CancelledError])
.} =
without blockNumber =? blck.number:
return
let blockTag = BlockTag.init(blockNumber)
let ids = toSeq(subscriptions.logSubscriptions.keys)
for id in ids:
without (filter, _) =? subscriptions.logSubscriptions.?[id]:
continue
if filter notin blck:
continue
result[id] = await subscriptions.getLogs(filter, blockTag)
proc processBlock(
subscriptions: Subscriptions,
blockNumber: BlockNumber
): Future[bool] {.async:(raises:[CancelledError]).} =
try:
let blockTag = BlockTag.init(blockNumber)
without blck =? await subscriptions.provider.getBlock(blockTag):
return false
if blck.logsBloom.isNone:
return false
let logs = await subscriptions.getLogs(blck)
for handler in subscriptions.blockSubscriptions.values:
handler(blck)
for (id, logs) in logs.pairs:
if (_, handler) =? subscriptions.logSubscriptions.?[id]:
for log in logs:
handler(log)
return true
except ProviderError:
return false
func new*(
_: type Subscriptions,
provider: Provider,
pollingInterval: Duration
): Subscriptions =
let subscriptions = Subscriptions()
proc processBlock(
blockNumber: BlockNumber
): Future[bool] {.async:(raises:[CancelledError]).} =
await subscriptions.processBlock(blockNumber)
let blockSubscriber = BlockSubscriber.new(
provider,
processBlock,
pollingInterval
)
subscriptions.provider = provider
subscriptions.blockSubscriber = blockSubscriber
subscriptions
proc close*(subscriptions: Subscriptions) {.async:(raises:[]).} =
await subscriptions.blockSubscriber.stop()
proc update*(subscriptions: Subscriptions) =
subscriptions.blockSubscriber.update()

View File

@ -0,0 +1,64 @@
import ../basics
import ../provider
type
BlockSubscriber* = ref object
provider: Provider
processor: ProcessBlock
pollingInterval: Duration
lastSeen: BlockNumber
lastProcessed: BlockNumber
wake: AsyncEvent
looping: Future[void].Raising([])
ProcessBlock* =
proc(number: BlockNumber): Future[bool] {.async:(raises:[CancelledError]).}
func new*(
_: type BlockSubscriber,
provider: Provider,
processor: ProcessBlock,
pollingInterval: Duration
): BlockSubscriber =
BlockSubscriber(
provider: provider,
processor: processor,
pollingInterval: pollingInterval,
wake: newAsyncEvent()
)
proc sleep(subscriber: BlockSubscriber) {.async:(raises:[CancelledError]).} =
discard await subscriber.wake.wait().withTimeout(subscriber.pollingInterval)
subscriber.wake.clear()
proc loop(subscriber: BlockSubscriber) {.async:(raises:[]).} =
try:
while true:
try:
await subscriber.sleep()
subscriber.lastSeen = await subscriber.provider.getBlockNumber()
for number in (subscriber.lastProcessed + 1)..subscriber.lastSeen:
if await subscriber.processor(number):
subscriber.lastProcessed = number
else:
break
except ProviderError:
discard
except CancelledError:
discard
proc start*(
subscriber: BlockSubscriber
) {.async:(raises:[ProviderError, CancelledError]).} =
if subscriber.looping.isNil:
subscriber.lastSeen = await subscriber.provider.getBlockNumber()
subscriber.lastProcessed = subscriber.lastSeen
subscriber.wake.clear()
subscriber.looping = subscriber.loop()
proc stop*(subscriber: BlockSubscriber) {.async:(raises:[]).} =
if looping =? subscriber.looping:
subscriber.looping = nil
await looping.cancelAndWait()
proc update*(subscriber: BlockSubscriber) =
subscriber.wake.fire()

View File

@ -0,0 +1,14 @@
import pkg/eth/bloom
import ../basics
import ../provider
func contains*(blck: Block, filter: EventFilter): bool =
without logsBloom =? blck.logsBloom:
return false
let bloomFilter = BloomFilter(value: logsBloom)
if filter.address.toArray notin bloomFilter:
return false
for topic in filter.topics:
if topic notin bloomFilter:
return false
return true

View File

@ -1,7 +1,2 @@
switch("path", "..")
when (NimMajor, NimMinor) >= (1, 4):
switch("hint", "XCannotRaiseY:off")
when (NimMajor, NimMinor, NimPatch) >= (1, 6, 11):
switch("warning", "BareExcept:off")
--path:".."
--define:"chronicles_enabled:off"

View File

@ -1,56 +0,0 @@
import ../../examples
import ../../../ethers/provider
import ../../../ethers/providers/jsonrpc/conversions
import std/sequtils
import pkg/stew/byteutils
import pkg/json_rpc/rpcserver except `%`, `%*`
import pkg/json_rpc/errors
type MockRpcHttpServer* = ref object
filters*: seq[string]
nextGetChangesReturnsError*: bool
srv: RpcHttpServer
proc new*(_: type MockRpcHttpServer): MockRpcHttpServer =
let srv = newRpcHttpServer(["127.0.0.1:0"])
MockRpcHttpServer(filters: @[], srv: srv, nextGetChangesReturnsError: false)
proc invalidateFilter*(server: MockRpcHttpServer, jsonId: JsonNode) =
server.filters.keepItIf it != jsonId.getStr
proc start*(server: MockRpcHttpServer) =
server.srv.router.rpc("eth_newFilter") do(filter: EventFilter) -> string:
let filterId = "0x" & (array[16, byte].example).toHex
server.filters.add filterId
return filterId
server.srv.router.rpc("eth_newBlockFilter") do() -> string:
let filterId = "0x" & (array[16, byte].example).toHex
server.filters.add filterId
return filterId
server.srv.router.rpc("eth_getFilterChanges") do(id: string) -> seq[string]:
if server.nextGetChangesReturnsError:
raise (ref ApplicationError)(code: -32000, msg: "unknown error")
if id notin server.filters:
raise (ref ApplicationError)(code: -32000, msg: "filter not found")
return @[]
server.srv.router.rpc("eth_uninstallFilter") do(id: string) -> bool:
if id notin server.filters:
raise (ref ApplicationError)(code: -32000, msg: "filter not found")
server.invalidateFilter(%id)
return true
server.srv.start()
proc stop*(server: MockRpcHttpServer) {.async.} =
await server.srv.stop()
await server.srv.closeWait()
proc localAddress*(server: MockRpcHttpServer): seq[TransportAddress] =
return server.srv.localAddress()

View File

@ -15,14 +15,14 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
var provider: JsonRpcProvider
setup:
provider = JsonRpcProvider.new(url, pollingInterval = 100.millis)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
teardown:
await provider.close()
test "can be instantiated with a default URL":
discard JsonRpcProvider.new()
let provider = await JsonRpcProvider.connect()
await provider.close()
test "lists all accounts":
let accounts = await provider.listAccounts()
@ -49,7 +49,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
let oldBlock = !await provider.getBlock(BlockTag.latest)
discard await provider.send("evm_mine")
var newBlock: Block
let blockHandler = proc(blck: ?!Block) {.raises:[].}= newBlock = blck.value
let blockHandler = proc(blck: Block) = newBlock = blck
let subscription = await provider.subscribe(blockHandler)
discard await provider.send("evm_mine")
check eventually newBlock.number.isSome
@ -87,20 +87,9 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
expect EthersError:
discard await confirming
test "raises JsonRpcProviderError when something goes wrong":
let provider = JsonRpcProvider.new("http://invalid.")
test "raises JsonRpcProviderError when connect fails":
expect JsonRpcProviderError:
discard await provider.listAccounts()
expect JsonRpcProviderError:
discard await provider.send("evm_mine")
expect JsonRpcProviderError:
discard await provider.getBlockNumber()
expect JsonRpcProviderError:
discard await provider.getBlock(BlockTag.latest)
expect JsonRpcProviderError:
discard await provider.subscribe(proc(_: ?!Block) = discard)
expect JsonRpcProviderError:
discard await provider.getSigner().sendTransaction(Transaction.example)
discard await JsonRpcProvider.connect("http://invalid.")
test "syncing":
let isSyncing = await provider.isSyncing()

View File

@ -8,10 +8,10 @@ suite "JsonRpcSigner":
var provider: JsonRpcProvider
var accounts: seq[Address]
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
accounts = await provider.listAccounts()
teardown:
@ -75,7 +75,7 @@ suite "JsonRpcSigner":
let blk = !(await signer.provider.getBlock(BlockTag.latest))
transaction.maxFeePerGas = some(!blk.baseFeePerGas * 2.u256 + !populated.maxPriorityFeePerGas)
check populated == transaction
test "populate fails when sender does not match signer address":

View File

@ -1,218 +1,63 @@
import std/os
import std/importutils
import pkg/asynctest/chronos/unittest
import pkg/serde
import pkg/json_rpc/rpcclient
import pkg/json_rpc/rpcserver
import ethers/provider
import ethers/providers/jsonrpc/subscriptions
import ethers/providers/jsonrpc
import ../../examples
import ./rpc_mock
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
suite "JsonRpcSubscriptions":
suite "JSON-RPC Subscriptions (" & url & ")":
test "can be instantiated with an http client":
let client = newRpcHttpClient()
let subscriptions = JsonRpcSubscriptions.new(client)
check not isNil subscriptions
var provider: JsonRpcProvider
test "can be instantiated with a websocket client":
let client = newRpcWebSocketClient()
let subscriptions = JsonRpcSubscriptions.new(client)
check not isNil subscriptions
setup:
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
template subscriptionTests(subscriptions, client) =
teardown:
await provider.close()
test "subscribes to new blocks":
var latestBlock: Block
proc callback(blck: ?!Block) =
latestBlock = blck.value
let subscription = await subscriptions.subscribeBlocks(callback)
discard await client.call("evm_mine", newJArray())
check eventually latestBlock.number.isSome
check latestBlock.hash.isSome
check latestBlock.timestamp > 0.u256
await subscriptions.unsubscribe(subscription)
test "subscribes to new blocks":
var latestBlock: Block
proc callback(blck: Block) =
latestBlock = blck
let subscription = await provider.subscribe(callback)
discard await provider.send("evm_mine")
check eventually latestBlock.number.isSome
check latestBlock.hash.isSome
check latestBlock.timestamp > 0.u256
await subscription.unsubscribe()
test "stops listening to new blocks when unsubscribed":
var count = 0
proc callback(blck: ?!Block) =
if blck.isOk:
test "stops listening to new blocks when unsubscribed":
var count = 0
proc callback(blck: Block) =
inc count
let subscription = await subscriptions.subscribeBlocks(callback)
discard await client.call("evm_mine", newJArray())
check eventually count > 0
await subscriptions.unsubscribe(subscription)
count = 0
discard await client.call("evm_mine", newJArray())
await sleepAsync(100.millis)
check count == 0
let subscription = await provider.subscribe(callback)
discard await provider.send("evm_mine")
check eventually count > 0
await subscription.unsubscribe()
count = 0
discard await provider.send("evm_mine")
await sleepAsync(200.millis)
check count == 0
test "unsubscribing from a non-existent subscription does not do any harm":
await subscriptions.unsubscribe(newJInt(0))
test "duplicate unsubscribe is harmless":
proc callback(blck: Block) = discard
let subscription = await provider.subscribe(callback)
await subscription.unsubscribe()
await subscription.unsubscribe()
test "duplicate unsubscribe is harmless":
proc callback(blck: ?!Block) = discard
let subscription = await subscriptions.subscribeBlocks(callback)
await subscriptions.unsubscribe(subscription)
await subscriptions.unsubscribe(subscription)
test "stops listening to new blocks when provider is closed":
var count = 0
proc callback(blck: ?!Block) =
if blck.isOk:
test "stops listening to new blocks when provider is closed":
var count = 0
proc callback(blck: Block) =
inc count
discard await subscriptions.subscribeBlocks(callback)
discard await client.call("evm_mine", newJArray())
check eventually count > 0
await subscriptions.close()
count = 0
discard await client.call("evm_mine", newJArray())
await sleepAsync(100.millis)
check count == 0
discard await provider.subscribe(callback)
discard await provider.send("evm_mine")
check eventually count > 0
await provider.close()
count = 0
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
discard await provider.send("evm_mine")
await sleepAsync(200.millis)
check count == 0
suite "Web socket subscriptions":
var subscriptions: JsonRpcSubscriptions
var client: RpcWebSocketClient
setup:
client = newRpcWebSocketClient()
await client.connect("ws://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545"))
subscriptions = JsonRpcSubscriptions.new(client)
subscriptions.start()
teardown:
await subscriptions.close()
await client.close()
subscriptionTests(subscriptions, client)
suite "HTTP polling subscriptions":
var subscriptions: JsonRpcSubscriptions
var client: RpcHttpClient
setup:
client = newRpcHttpClient()
await client.connect("http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545"))
subscriptions = JsonRpcSubscriptions.new(client,
pollingInterval = 100.millis)
subscriptions.start()
teardown:
await subscriptions.close()
await client.close()
subscriptionTests(subscriptions, client)
suite "HTTP polling subscriptions - mock tests":
var subscriptions: PollingSubscriptions
var client: RpcHttpClient
var mockServer: MockRpcHttpServer
privateAccess(PollingSubscriptions)
privateAccess(JsonRpcSubscriptions)
proc startServer() {.async.} =
mockServer = MockRpcHttpServer.new()
mockServer.start()
await client.connect("http://" & $mockServer.localAddress()[0])
proc stopServer() {.async.} =
await mockServer.stop()
setup:
client = newRpcHttpClient()
await startServer()
subscriptions = PollingSubscriptions(
JsonRpcSubscriptions.new(
client,
pollingInterval = 1.millis))
subscriptions.start()
teardown:
await subscriptions.close()
await client.close()
await mockServer.stop()
test "filter not found error recreates log filter":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
check subscriptions.logFilters.len == 0
check subscriptions.subscriptionMapping.len == 0
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
check subscriptions.logFilters[id] == filter
check subscriptions.subscriptionMapping[id] == id
check subscriptions.logFilters.len == 1
check subscriptions.subscriptionMapping.len == 1
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
test "recreated log filter can be still unsubscribed using the original id":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
await subscriptions.unsubscribe(id)
check not subscriptions.logFilters.hasKey id
check not subscriptions.subscriptionMapping.hasKey id
test "filter not found error recreates block filter":
let emptyHandler = proc(blck: ?!Block) = discard
check subscriptions.subscriptionMapping.len == 0
let id = await subscriptions.subscribeBlocks(emptyHandler)
check subscriptions.subscriptionMapping[id] == id
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
test "recreated block filter can be still unsubscribed using the original id":
let emptyHandler = proc(blck: ?!Block) = discard
let id = await subscriptions.subscribeBlocks(emptyHandler)
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
await subscriptions.unsubscribe(id)
check not subscriptions.subscriptionMapping.hasKey id
test "polling continues with new filter after temporary error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
await stopServer()
mockServer.invalidateFilter(id)
await sleepAsync(50.milliseconds)
await startServer()
check eventually subscriptions.subscriptionMapping[id] != id
test "calls callback with failed result on error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
var failedResultReceived = false
proc handler(log: ?!Log) =
if log.isErr:
failedResultReceived = true
let id = await subscriptions.subscribeLogs(filter, handler)
await sleepAsync(50.milliseconds)
mockServer.nextGetChangesReturnsError = true
check eventually failedResultReceived

View File

@ -1,56 +0,0 @@
import std/os
import std/importutils
import pkg/asynctest/chronos/unittest
import pkg/json_rpc/rpcclient
import ethers/provider
import ethers/providers/jsonrpc/subscriptions
import ../../examples
suite "Websocket re-subscriptions":
privateAccess(JsonRpcSubscriptions)
var subscriptions: JsonRpcSubscriptions
var client: RpcWebSocketClient
var resubscribeInterval: int
setup:
resubscribeInterval = 3
client = newRpcWebSocketClient()
await client.connect("ws://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545"))
subscriptions = JsonRpcSubscriptions.new(client, resubscribeInterval = resubscribeInterval)
subscriptions.start()
teardown:
await subscriptions.close()
await client.close()
test "unsubscribing from a log filter while subscriptions are being resubscribed does not cause a concurrency error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
for i in 1..10:
discard await subscriptions.subscribeLogs(filter, emptyHandler)
# Wait until the re-subscription starts
await sleepAsync(resubscribeInterval.seconds)
# Attempt to modify callbacks while its being iterated
discard await subscriptions.subscribeLogs(filter, emptyHandler)
test "resubscribe events take effect with new subscription IDs in the log filters":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
check id in subscriptions.logFilters
check subscriptions.logFilters.len == 1
# Make sure the subscription is done
await sleepAsync((resubscribeInterval + 1).seconds)
# The previous subscription should not be in the log filters
check id notin subscriptions.logFilters
# There is still one subscription which is the new one
check subscriptions.logFilters.len == 1

View File

@ -1,7 +1,6 @@
import ./jsonrpc/testJsonRpcProvider
import ./jsonrpc/testJsonRpcSigner
import ./jsonrpc/testJsonRpcSubscriptions
import ./jsonrpc/testWsResubscription
import ./jsonrpc/testConversions
import ./jsonrpc/testErrors

View File

@ -28,7 +28,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
var accounts: seq[Address]
setup:
provider = JsonRpcProvider.new(url, pollingInterval = 100.millis)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
accounts = await provider.listAccounts()
let deployment = readDeployment()
@ -157,10 +157,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
test "receives events when subscribed":
var transfers: seq[Transfer]
proc handleTransfer(transferRes: ?!Transfer) =
without transfer =? transferRes, error:
echo error.msg
proc handleTransfer(transfer: Transfer) =
transfers.add(transfer)
let signer0 = provider.getSigner(accounts[0])
@ -182,9 +179,8 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
test "stops receiving events when unsubscribed":
var transfers: seq[Transfer]
proc handleTransfer(transferRes: ?!Transfer) =
if transfer =? transferRes:
transfers.add(transfer)
proc handleTransfer(transfer: Transfer) =
transfers.add(transfer)
let signer0 = provider.getSigner(accounts[0])

View File

@ -23,10 +23,10 @@ suite "Contract custom errors":
var contract: TestCustomErrors
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment()
let address = !deployment.address(TestCustomErrors)

View File

@ -15,10 +15,10 @@ suite "Contract enum parameters and return values":
var contract: TestEnums
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment()
contract = TestEnums.new(!deployment.address(TestEnums), provider)

View File

@ -24,7 +24,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
var accounts: seq[Address]
setup:
provider = JsonRpcProvider.new(url, pollingInterval = 100.millis)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
accounts = await provider.listAccounts()
let deployment = readDeployment()

View File

@ -15,10 +15,10 @@ suite "gas estimation":
var contract: TestGasEstimation
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment()
let signer = provider.getSigner()

View File

@ -14,10 +14,10 @@ suite "Contract return values":
var contract: TestReturns
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment()
contract = TestReturns.new(!deployment.address(TestReturns), provider)

View File

@ -93,10 +93,10 @@ suite "Testing helpers - contracts":
var snapshot: JsonNode
var accounts: seq[Address]
let revertReason = "revert reason"
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "ws://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("ws://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
accounts = await provider.listAccounts()
helpersContract = TestHelpers.new(provider.getSigner())

View File

@ -13,10 +13,10 @@ proc transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.contract.}
suite "Wallet":
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
let url = "http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
provider = await JsonRpcProvider.connect(url, pollingInterval = 100.millis)
snapshot = await provider.send("evm_snapshot")
teardown:
@ -31,13 +31,12 @@ suite "Wallet":
check isSuccess Wallet.new("0x" & pk1)
test "Can create Wallet with provider":
let provider = JsonRpcProvider.new()
check isSuccess Wallet.new(pk1, provider)
discard Wallet.new(PrivateKey.fromHex(pk1).get, provider)
test "Cannot create wallet with invalid key string":
check isFailure Wallet.new("0xInvalidKey")
check isFailure Wallet.new("0xInvalidKey", JsonRpcProvider.new())
check isFailure Wallet.new("0xInvalidKey", provider)
test "Can connect Wallet to provider":
let wallet = !Wallet.new(pk1)