diff --git a/ethers.nimble b/ethers.nimble index 5df4668..b317c5e 100644 --- a/ethers.nimble +++ b/ethers.nimble @@ -13,6 +13,8 @@ requires "serde >= 1.2.1 & < 1.3.0" requires "stint >= 0.8.1 & < 0.9.0" requires "stew >= 0.2.0 & < 0.3.0" requires "eth >= 0.5.0 & < 0.6.0" +requires "websock >= 0.2.0 & < 0.3.0" +requires "httputils >= 0.2.0" task test, "Run the test suite": # exec "nimble install -d -y" diff --git a/ethers/errors.nim b/ethers/errors.nim index 064a2ae..eff7996 100644 --- a/ethers/errors.nim +++ b/ethers/errors.nim @@ -1,3 +1,5 @@ +import pkg/websock/websock +import pkg/json_rpc/errors import ./basics type @@ -7,20 +9,53 @@ type SubscriptionError* = object of EthersError ProviderError* = object of EthersError data*: ?seq[byte] + RpcNetworkError* = object of EthersError RpcHttpErrorResponse* = object of RpcNetworkError HttpRequestLimitError* = object of RpcHttpErrorResponse HttpRequestTimeoutError* = object of RpcHttpErrorResponse + WebsocketConnectionError* = object of RpcNetworkError -{.push raises:[].} +{.push raises: [].} + +template convertErrorsTo*(newErrorType: type, body) = + try: + body + except CancelledError as error: + raise error + except RpcNetworkError as error: + raise error + except RpcPostError as error: + raiseNetworkError(error) + except FailedHttpResponse as error: + raiseNetworkError(error) + except HttpError as error: # from websock.common + # eg Timeout expired while receiving headers + # eg Unable to connect to host on any address! + # eg No connection to host! + raise newException(WebsocketConnectionError, error.msg, error) + except WSClosedError as error: + raise newException(WebsocketConnectionError, error.msg, error) + except ErrorResponse as error: + if error.status == 429: + raise newException(HttpRequestLimitError, error.msg, error) + elif error.status == 408: + raise newException(HttpRequestTimeoutError, error.msg, error) + else: + raise newException(newErrorType, error.msg, error) + except JsonRpcError as error: + var message = error.msg + if jsn =? JsonNode.fromJson(message): + if "message" in jsn: + message = jsn{"message"}.getStr + raise newException(newErrorType, message, error) + except CatchableError as error: + raise newException(newErrorType, error.msg, error) proc toErr*[E1: ref CatchableError, E2: EthersError]( - e1: E1, - _: type E2, - msg: string = e1.msg): ref E2 = - + e1: E1, _: type E2, msg: string = e1.msg +): ref E2 = return newException(E2, msg, e1) -proc raiseNetworkError*( - error: ref CatchableError) {.raises: [RpcNetworkError].} = +proc raiseNetworkError*(error: ref CatchableError) {.raises: [RpcNetworkError].} = raise newException(RpcNetworkError, error.msg, error) diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index cfd55a9..05306a2 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -16,7 +16,7 @@ import ./jsonrpc/errors export basics export provider export chronicles -export errors.JsonRpcProviderError +export errors export subscriptions {.push raises: [].} @@ -37,6 +37,7 @@ type JsonRpcSigner* = ref object of Signer provider: JsonRpcProvider address: ?Address + JsonRpcSignerError* = object of SignerError # Provider @@ -44,19 +45,19 @@ type const defaultUrl = "http://localhost:8545" const defaultPollingInterval = 4.seconds -proc jsonHeaders: seq[(string, string)] = +proc jsonHeaders(): seq[(string, string)] = @[("Content-Type", "application/json")] proc new*( - _: type JsonRpcProvider, - url=defaultUrl, - pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} = - + _: type JsonRpcProvider, url = defaultUrl, pollingInterval = defaultPollingInterval +): JsonRpcProvider {.raises: [JsonRpcProviderError].} = var initialized: Future[void] var client: RpcClient var subscriptions: JsonRpcSubscriptions - proc initialize() {.async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]).} = + proc initialize() {. + async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]) + .} = convertError: case parseUri(url).scheme of "ws", "wss": @@ -68,8 +69,8 @@ proc new*( let http = newRpcHttpClient() await http.connect(url) client = http - subscriptions = JsonRpcSubscriptions.new(http, - pollingInterval = pollingInterval) + subscriptions = + JsonRpcSubscriptions.new(http, pollingInterval = pollingInterval) subscriptions.start() proc awaitClient(): Future[RpcClient] {. @@ -91,30 +92,30 @@ proc new*( proc callImpl( client: RpcClient, call: string, args: JsonNode -): Future[JsonNode] {.async: (raises: [JsonRpcProviderError, CancelledError, JsonRpcError]).} = - try: +): Future[JsonNode] {. + async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]) +.} = + convertError: let response = await client.call(call, %args) without json =? JsonNode.fromJson(response.string), error: - raiseJsonRpcProviderError error, "Failed to parse response '" & response.string & "': " & - error.msg + raiseJsonRpcProviderError error, + "Failed to parse response '" & response.string & "': " & error.msg return json - except CancelledError as error: - raise error - except JsonRpcError as error: - raise error - except CatchableError as error: - raiseJsonRpcProviderError error proc send*( provider: JsonRpcProvider, call: string, arguments: seq[JsonNode] = @[] -): Future[JsonNode] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[JsonNode] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client return await client.callImpl(call, %arguments) proc listAccounts*( provider: JsonRpcProvider -): Future[seq[Address]] {.async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]).} = +): Future[seq[Address]] {. + async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client return await client.eth_accounts() @@ -141,7 +142,9 @@ method getBlock*( method call*( provider: JsonRpcProvider, tx: Transaction, blockTag = BlockTag.latest -): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[seq[byte]] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client return await client.eth_call(tx, blockTag) @@ -162,21 +165,27 @@ method getTransactionCount*( method getTransaction*( provider: JsonRpcProvider, txHash: TransactionHash -): Future[?PastTransaction] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[?PastTransaction] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client return await client.eth_getTransactionByHash(txHash) method getTransactionReceipt*( provider: JsonRpcProvider, txHash: TransactionHash -): Future[?TransactionReceipt] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[?TransactionReceipt] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client return await client.eth_getTransactionReceipt(txHash) method getLogs*( provider: JsonRpcProvider, filter: EventFilter -): Future[seq[Log]] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[seq[Log]] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client let logsJson = @@ -195,9 +204,7 @@ method getLogs*( return logs method estimateGas*( - provider: JsonRpcProvider, - transaction: Transaction, - blockTag = BlockTag.latest, + provider: JsonRpcProvider, transaction: Transaction, blockTag = BlockTag.latest ): Future[UInt256] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = try: convertError: @@ -225,7 +232,9 @@ method getChainId*( method sendTransaction*( provider: JsonRpcProvider, rawTransaction: seq[byte] -): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[TransactionResponse] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let client = await provider.client @@ -235,7 +244,9 @@ method sendTransaction*( method subscribe*( provider: JsonRpcProvider, filter: EventFilter, onLog: LogHandler -): Future[Subscription] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[Subscription] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let subscriptions = await provider.subscriptions let id = await subscriptions.subscribeLogs(filter, onLog) @@ -243,7 +254,9 @@ method subscribe*( method subscribe*( provider: JsonRpcProvider, onBlock: BlockHandler -): Future[Subscription] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} = +): Future[Subscription] {. + async: (raises: [ProviderError, CancelledError, RpcNetworkError]) +.} = convertError: let subscriptions = await provider.subscriptions let id = await subscriptions.subscribeBlocks(onBlock) @@ -277,21 +290,17 @@ method close*( # Signer template convertSignerError(body) = - try: + convertErrorsTo(JsonRpcSignerError): body - except CancelledError as error: - raise error - except CatchableError as error: - raise newException(JsonRpcSignerError, error.msg, error) - -method provider*(signer: JsonRpcSigner): Provider - {.gcsafe, raises: [SignerError].} = +method provider*(signer: JsonRpcSigner): Provider {.gcsafe, raises: [SignerError].} = signer.provider method getAddress*( signer: JsonRpcSigner -): Future[Address] {.async: (raises: [ProviderError, SignerError, CancelledError, RpcNetworkError]).} = +): Future[Address] {. + async: (raises: [ProviderError, SignerError, CancelledError, RpcNetworkError]) +.} = if address =? signer.address: return address diff --git a/ethers/providers/jsonrpc/errors.nim b/ethers/providers/jsonrpc/errors.nim index 3dfbb37..0ab7953 100644 --- a/ethers/providers/jsonrpc/errors.nim +++ b/ethers/providers/jsonrpc/errors.nim @@ -4,10 +4,11 @@ import ../../basics import ../../errors import ../../provider import ./conversions +import pkg/websock/websock export errors -{.push raises:[].} +{.push raises: [].} type JsonRpcProviderError* = object of ProviderError @@ -31,53 +32,13 @@ func new*(_: type JsonRpcProviderError, json: JsonNode): ref JsonRpcProviderErro error proc raiseJsonRpcProviderError*( - error: ref CatchableError, message = error.msg) {.raises: [JsonRpcProviderError].} = + error: ref CatchableError, message = error.msg +) {.raises: [JsonRpcProviderError].} = if json =? JsonNode.fromJson(error.msg): raise JsonRpcProviderError.new(json) else: raise newException(JsonRpcProviderError, message) -proc underlyingErrorOf(e: ref Exception, T: type CatchableError): (ref CatchableError) = - if e of (ref T): - return (ref T)(e) - elif not e.parent.isNil: - return e.parent.underlyingErrorOf T - else: - return nil - template convertError*(body) = - try: - try: - body - # Inspect SubscriptionErrors and re-raise underlying JsonRpcErrors so that - # exception inspection and resolution only needs to happen once. All - # CatchableErrors that occur in the Subscription module are converted to - # SubscriptionError, with the original error preserved as the exception's - # parent. - except SubscriptionError, SignerError: - let e = getCurrentException() - let parent = e.underlyingErrorOf(JsonRpcError) - if not parent.isNil: - raise parent - except CancelledError as error: - raise error - except RpcPostError as error: - raiseNetworkError(error) - except FailedHttpResponse as error: - raiseNetworkError(error) - except ErrorResponse as error: - if error.status == 429: - raise newException(HttpRequestLimitError, error.msg, error) - elif error.status == 408: - raise newException(HttpRequestTimeoutError, error.msg, error) - else: - raiseJsonRpcProviderError(error) - except JsonRpcError as error: - var message = error.msg - if jsn =? JsonNode.fromJson(message): - if "message" in jsn: - message = jsn{"message"}.getStr - raiseJsonRpcProviderError(error, message) - except CatchableError as error: - raiseJsonRpcProviderError(error) - + convertErrorsTo(JsonRpcProviderError): + body diff --git a/ethers/providers/jsonrpc/subscriptions.nim b/ethers/providers/jsonrpc/subscriptions.nim index 39a0ba9..e1669ff 100644 --- a/ethers/providers/jsonrpc/subscriptions.nim +++ b/ethers/providers/jsonrpc/subscriptions.nim @@ -25,63 +25,60 @@ type # 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:[].} + MethodHandler* = proc(j: JsonNode) {.gcsafe, raises: [].} + SubscriptionCallback = + proc(id: JsonNode, arguments: ?!JsonNode) {.gcsafe, raises: [].} + +{.push raises: [].} template convertErrorsToSubscriptionError(body) = - try: + convertErrorsTo(SubscriptionError): 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 + 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 + # true = continue processing message using json_rpc's default message handler + return ok true proc setMethodHandler( - subscriptions: JsonRpcSubscriptions, - `method`: string, - handler: MethodHandler + subscriptions: JsonRpcSubscriptions, `method`: string, handler: MethodHandler ) = subscriptions.methodHandlers[`method`] = handler -method subscribeBlocks*(subscriptions: JsonRpcSubscriptions, - onBlock: BlockHandler): - Future[JsonNode] - {.async: (raises: [SubscriptionError, CancelledError]), base,.} = +method subscribeBlocks*( + subscriptions: JsonRpcSubscriptions, onBlock: BlockHandler +): Future[JsonNode] {. + async: (raises: [SubscriptionError, CancelledError, RpcNetworkError]), base +.} = raiseAssert "not implemented" -method subscribeLogs*(subscriptions: JsonRpcSubscriptions, - filter: EventFilter, - onLog: LogHandler): - Future[JsonNode] - {.async: (raises: [SubscriptionError, CancelledError]), base.} = +method subscribeLogs*( + subscriptions: JsonRpcSubscriptions, filter: EventFilter, onLog: LogHandler +): Future[JsonNode] {. + async: (raises: [SubscriptionError, CancelledError, RpcNetworkError]), base +.} = raiseAssert "not implemented" -method unsubscribe*(subscriptions: JsonRpcSubscriptions, - id: JsonNode) - {.async: (raises: [CancelledError]), base.} = +method unsubscribe*( + subscriptions: JsonRpcSubscriptions, id: JsonNode +) {.async: (raises: [CancelledError]), base.} = raiseAssert "not implemented " method close*(subscriptions: JsonRpcSubscriptions) {.async: (raises: []), base.} = @@ -92,23 +89,24 @@ method close*(subscriptions: JsonRpcSubscriptions) {.async: (raises: []), base.} except CatchableError as e: error "JsonRpc unsubscription failed", error = e.msg, id = id -proc getCallback(subscriptions: JsonRpcSubscriptions, - id: JsonNode): ?SubscriptionCallback {. raises:[].} = +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 + 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 +type WebSocketSubscriptions = ref object of JsonRpcSubscriptions + logFiltersLock: AsyncLock + resubscribeFut: Future[void] + resubscribeInterval: int template withLock*(subscriptions: WebSocketSubscriptions, body: untyped) = if subscriptions.logFiltersLock.isNil: @@ -122,13 +120,14 @@ template withLock*(subscriptions: WebSocketSubscriptions, body: untyped) = # 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]).} = +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] @@ -144,31 +143,37 @@ proc resubscribeWebsocketEventsOnTimeout*(subscriptions: WebSocketSubscriptions) except CancelledError as e: raise e except CatchableError as e: - error "WS resubscription failed" , error = e.msg + error "WS resubscription failed", error = e.msg -proc new*(_: type JsonRpcSubscriptions, - client: RpcWebSocketClient, - resubscribeInterval = WsResubscribe): JsonRpcSubscriptions = - let subscriptions = WebSocketSubscriptions(client: client, resubscribeInterval: resubscribeInterval) +proc new*( + _: type JsonRpcSubscriptions, + client: RpcWebSocketClient, + resubscribeInterval = WsResubscribe, +): JsonRpcSubscriptions = + let subscriptions = + WebSocketSubscriptions(client: client, resubscribeInterval: resubscribeInterval) - proc subscriptionHandler(arguments: JsonNode) {.raises:[].} = + 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 + 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]).} = +method subscribeBlocks*( + subscriptions: WebSocketSubscriptions, onBlock: BlockHandler +): Future[JsonNode] {. + async: (raises: [SubscriptionError, CancelledError, RpcNetworkError]) +.} = proc callback(id: JsonNode, argumentsResult: ?!JsonNode) {.raises: [].} = without arguments =? argumentsResult, error: onBlock(failure(Block, error.toErr(SubscriptionError))) @@ -183,11 +188,11 @@ method subscribeBlocks(subscriptions: WebSocketSubscriptions, subscriptions.callbacks[id] = callback return id -method subscribeLogs(subscriptions: WebSocketSubscriptions, - filter: EventFilter, - onLog: LogHandler): - Future[JsonNode] - {.async: (raises: [SubscriptionError, CancelledError]).} = +method subscribeLogs*( + subscriptions: WebSocketSubscriptions, filter: EventFilter, onLog: LogHandler +): Future[JsonNode] {. + async: (raises: [SubscriptionError, CancelledError, RpcNetworkError]) +.} = proc callback(id: JsonNode, argumentsResult: ?!JsonNode) = without arguments =? argumentsResult, error: onLog(failure(Log, error.toErr(SubscriptionError))) @@ -203,9 +208,9 @@ method subscribeLogs(subscriptions: WebSocketSubscriptions, subscriptions.logFilters[id] = filter return id -method unsubscribe*(subscriptions: WebSocketSubscriptions, - id: JsonNode) - {.async: (raises: [CancelledError]).} = +method unsubscribe*( + subscriptions: WebSocketSubscriptions, id: JsonNode +) {.async: (raises: [CancelledError]).} = try: withLock(subscriptions): subscriptions.callbacks.del(id) @@ -219,22 +224,20 @@ method unsubscribe*(subscriptions: WebSocketSubscriptions, method close*(subscriptions: WebSocketSubscriptions) {.async: (raises: []).} = await procCall JsonRpcSubscriptions(subscriptions).close() if not subscriptions.resubscribeFut.isNil: - await subscriptions.resubscribeFut.cancelAndWait() + await subscriptions.resubscribeFut.cancelAndWait() # Polling -type - PollingSubscriptions* = ref object of JsonRpcSubscriptions - polling: Future[void] +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 = + # 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]).} = @@ -252,12 +255,21 @@ proc new*(_: type JsonRpcSubscriptions, 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 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]: + 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: @@ -273,13 +285,27 @@ proc new*(_: type JsonRpcSubscriptions, # 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)) + 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 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]).} = @@ -293,7 +319,7 @@ proc new*(_: type JsonRpcSubscriptions, for change in changes: callback(id, success(change)) - proc poll {.async: (raises: []).} = + proc poll() {.async: (raises: []).} = try: while true: for id in toSeq subscriptions.callbacks.keys: @@ -310,22 +336,26 @@ 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:[]).} = +method subscribeBlocks*( + subscriptions: PollingSubscriptions, onBlock: BlockHandler +): Future[JsonNode] {. + async: (raises: [SubscriptionError, CancelledError, RpcNetworkError]) +.} = + 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) + 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:[].} = + proc callback(id: JsonNode, changeResult: ?!JsonNode) {.raises: [].} = without change =? changeResult, e: onBlock(failure(Block, e.toErr(SubscriptionError))) return @@ -339,12 +369,11 @@ method subscribeBlocks(subscriptions: PollingSubscriptions, subscriptions.subscriptionMapping[id] = id return id -method subscribeLogs(subscriptions: PollingSubscriptions, - filter: EventFilter, - onLog: LogHandler): - Future[JsonNode] - {.async: (raises: [SubscriptionError, CancelledError]).} = - +method subscribeLogs*( + subscriptions: PollingSubscriptions, filter: EventFilter, onLog: LogHandler +): Future[JsonNode] {. + async: (raises: [SubscriptionError, CancelledError, RpcNetworkError]) +.} = proc callback(id: JsonNode, argumentsResult: ?!JsonNode) = without arguments =? argumentsResult, error: onLog(failure(Log, error.toErr(SubscriptionError))) @@ -360,13 +389,13 @@ method subscribeLogs(subscriptions: PollingSubscriptions, subscriptions.subscriptionMapping[id] = id return id -method unsubscribe*(subscriptions: PollingSubscriptions, - id: JsonNode) - {.async: (raises: [CancelledError]).} = +method unsubscribe*( + subscriptions: PollingSubscriptions, id: JsonNode +) {.async: (raises: [CancelledError]).} = try: subscriptions.logFilters.del(id) subscriptions.callbacks.del(id) - if sub =? subscriptions.subscriptionMapping.?[id]: + if sub =? subscriptions.subscriptionMapping .? [id]: subscriptions.subscriptionMapping.del(id) discard await subscriptions.client.eth_uninstallFilter(sub) except CancelledError as e: diff --git a/testmodule/providers/jsonrpc/mocks/mockHttpServer.nim b/testmodule/providers/jsonrpc/mocks/mockHttpServer.nim index 3ed92a6..6463c8f 100644 --- a/testmodule/providers/jsonrpc/mocks/mockHttpServer.nim +++ b/testmodule/providers/jsonrpc/mocks/mockHttpServer.nim @@ -10,7 +10,7 @@ export httpserver {.push raises: [].} type - RpcResponse* = proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CancelledError]), raises: [].} + RpcResponse* = proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} MockHttpServer* = object server: HttpServerRef @@ -32,12 +32,9 @@ proc init*(_: type MockHttpServer, address: TransportAddress): MockHttpServer = let request = r.get() try: let body = string.fromBytes(await request.getBody()) - echo "mockHttpServer.processRequest request: ", body without req =? RequestRx.fromJson(body), error: - echo "failed to deserialize, error: ", error.msg return await request.respond(Http400, "Invalid request, must be valid json rpc request") - echo "Received request with method: ", req.method if not server.rpcResponses.contains(req.method): return await request.respond(Http404, "Method not registered") @@ -48,10 +45,8 @@ proc init*(_: type MockHttpServer, address: TransportAddress): MockHttpServer = return await request.respond(Http500, "Method lookup error with key, error: " & e.msg) except HttpProtocolError as e: - echo "HttpProtocolError encountered, error: ", e.msg return defaultResponse(e) except HttpTransportError as e: - echo "HttpTransportError encountered, error: ", e.msg return defaultResponse(e) except HttpWriteError as exc: return defaultResponse(exc) diff --git a/testmodule/providers/jsonrpc/mocks/mockWebSocketServer.nim b/testmodule/providers/jsonrpc/mocks/mockWebSocketServer.nim new file mode 100644 index 0000000..f6a1e3c --- /dev/null +++ b/testmodule/providers/jsonrpc/mocks/mockWebSocketServer.nim @@ -0,0 +1,128 @@ +import std/tables +import std/sequtils +import pkg/chronos +import pkg/chronicles except toJson, `%`, `%*` +import pkg/websock/websock except toJson, `%`, `%*` +import pkg/json_rpc/clients/websocketclient except toJson, `%`, `%*` +import pkg/json_rpc/client except toJson, `%`, `%*` +import pkg/json_rpc/server except toJson, `%`, `%*` +import pkg/questionable +import pkg/websock/http/common +import pkg/serde +import pkg/stew/byteutils + +type + MockWebSocketServer* = ref object + httpServer: HttpServer + address*: TransportAddress + connections: seq[WSSession] + rpcResponses: Table[string, WebSocketResponse] + running: bool + + WebSocketResponse* = proc(ws: WSSession) {.async.} + + RequestRx {.deserialize.} = object + jsonrpc*: string + id*: int + `method`*: string + + ResponseKind* = enum + rkResult + rkError + + ResponseError* {.serialize.} = object + code*: int + message*: string + data*: ?string + + ResponseTx* {.serialize.} = object + jsonrpc*: string + id*: int + case kind* {.serialize(ignore = true).}: ResponseKind + of rkResult: + result*: JsonNode + of rkError: + error*: ResponseError + +proc init*(T: type MockWebSocketServer, address: TransportAddress): T = + T( + address: address, + connections: @[], + rpcResponses: initTable[string, WebSocketResponse](), + running: false, + ) + +proc registerRpcResponse*( + server: MockWebSocketServer, `method`: string, response: WebSocketResponse +) = + server.rpcResponses[`method`] = response + +proc handleWebSocketConnection(server: MockWebSocketServer, ws: WSSession) {.async.} = + server.connections.add(ws) + + try: + while ws.readyState == ReadyState.Open: + let data = await ws.recvMsg() + let message = string.fromBytes(data) + + without request =? RequestRx.fromJson(message), error: + await ws.close(StatusProtocolError, "Invalid JSON") + break + + if request.method notin server.rpcResponses: + let response = ResponseTx( + jsonrpc: "2.0", + id: request.id, + kind: rkError, + error: ResponseError(code: 404, message: "Method not registered"), + ) + await ws.send(response.toJson()) + break + + let rpcResponseProc = server.rpcResponses[request.method] + + await ws.rpcResponseProc() + except WSClosedError: + # Connection was closed + trace "WebSocket connection closed" + except CatchableError as exc: + trace "WebSocket connection error", error = exc.msg + +proc processRequest(server: MockWebSocketServer, request: HttpRequest) {.async.} = + let wsServer = WSServer.new(protos = ["proto"]) + # perform upgrade + let ws = await wsServer.handleRequest(request) + await server.handleWebSocketConnection(ws) + +proc start*(server: MockWebSocketServer) {.async.} = + if server.running: + return + + let handler = proc(request: HttpRequest): Future[void] {.async, raises: [].} = + await server.processRequest(request) + + server.httpServer = + HttpServer.create(address = server.address, handler = handler, flags = {ReuseAddr}) + server.httpServer.start() + server.running = true + +proc stop*(server: MockWebSocketServer) {.async.} = + if not server.running: + return + + server.running = false + + # Close all active connections + for conn in server.connections: + if conn.readyState == ReadyState.Open: + await conn.close(StatusGoingAway, "Server shutting down") + + server.connections.setLen(0) + + if not server.httpServer.isNil: + server.httpServer.stop() + +proc localAddress*(server: MockWebSocketServer): TransportAddress = + if server.httpServer.isNil: + return server.address + return server.httpServer.localAddress() diff --git a/testmodule/providers/jsonrpc/mocks/mockWebSocketServer2.nim b/testmodule/providers/jsonrpc/mocks/mockWebSocketServer2.nim deleted file mode 100644 index d7a0654..0000000 --- a/testmodule/providers/jsonrpc/mocks/mockWebSocketServer2.nim +++ /dev/null @@ -1,38 +0,0 @@ -import std/tables -import std/strutils -import std/uri - # pkg/chronos, -import pkg/chronicles - # pkg/chronos/apps/http/httpserver, -import pkg/websock/websock -import pkg/websock/tests/helpers -import pkg/httputils -import pkg/asynctest/chronos/unittest - # json_rpc/clients/websocketclient, - # json_rpc/[client, server], - # json_serialization - -import pkg/stew/byteutils -import pkg/ethers - -const address = initTAddress("127.0.0.1:8888") - -proc handle(request: HttpRequest) {.async.} = - check request.uri.path == WSPath - - let server = WSServer.new(protos = ["proto"]) - let ws = await server.handleRequest(request) - let servRes = await ws.recvMsg() - - check string.fromBytes(servRes) == testString - await ws.waitForClose() - - -proc run() {.async.} = - - let server = createServer( - address = address, - handler = handle, - flags = {ReuseAddr}) - - let provider = JsonRpcProvider.new("ws://" & $address) \ No newline at end of file diff --git a/testmodule/providers/jsonrpc/testErrors.nim b/testmodule/providers/jsonrpc/testErrors.nim index 0b6bd06..ae9ada3 100644 --- a/testmodule/providers/jsonrpc/testErrors.nim +++ b/testmodule/providers/jsonrpc/testErrors.nim @@ -3,16 +3,18 @@ import std/sequtils import std/typetraits import std/net -import stew/byteutils +import pkg/stew/byteutils import pkg/asynctest/chronos/unittest import pkg/chronos/apps/http/httpclient import pkg/serde import pkg/questionable -import pkg/ethers/providers/jsonrpc -import pkg/ethers/providers/jsonrpc/errors -import pkg/ethers/erc20 -import pkg/json_rpc/clients/httpclient +import pkg/ethers/providers/jsonrpc except toJson, `%`, `%*` +import pkg/ethers/providers/jsonrpc/errors except toJson, `%`, `%*` +import pkg/ethers/erc20 except toJson, `%`, `%*` +import pkg/json_rpc/clients/httpclient except toJson, `%`, `%*` +import pkg/json_rpc/clients/websocketclient except toJson, `%`, `%*` import pkg/websock/websock +import pkg/websock/http/common import ./mocks/mockHttpServer import ./mocks/mockWebSocketServer import ../../examples @@ -37,7 +39,14 @@ suite "JSON RPC errors": } check JsonRpcProviderError.new(error).data == some @[0xab'u8, 0xcd'u8] -type TestToken = ref object of Erc20Token +type + TestToken = ref object of Erc20Token + Before = proc(): Future[void] {.gcsafe, raises: [].} + # A proc that runs before each test + +proc runBefore(before: Before) {.async.} = + if before != nil: + await before() method mint( token: TestToken, holder: Address, amount: UInt256 @@ -47,6 +56,7 @@ suite "Network errors - HTTP": var provider: JsonRpcProvider var mockServer: MockHttpServer var token: TestToken + var blockingSocket: Socket setup: mockServer = MockHttpServer.init(initTAddress("127.0.0.1:0")) @@ -59,6 +69,9 @@ suite "Network errors - HTTP": teardown: await provider.close() await mockServer.stop() + if not blockingSocket.isNil: + blockingSocket.close() + blockingSocket = nil proc registerRpcMethods(response: RpcResponse) = mockServer.registerRpcResponse("eth_accounts", response) @@ -67,43 +80,38 @@ suite "Network errors - HTTP": mockServer.registerRpcResponse("eth_sendRawTransaction", response) mockServer.registerRpcResponse("eth_newBlockFilter", response) mockServer.registerRpcResponse("eth_newFilter", response) - # mockServer.registerRpcResponse("eth_subscribe", response) # TODO: handle - # eth_subscribe for websockets proc testCustomResponse( - errorName: string, - responseHttpCode: HttpCode, - responseText: string, + testNamePrefix: string, + response: RpcResponse, errorType: type CatchableError, + before: Before = nil, ) = - let response = proc( - request: HttpRequestRef - ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = - try: - return await request.respond(responseHttpCode, responseText) - except HttpWriteError as exc: - return defaultResponse(exc) + let prefix = testNamePrefix & " when " - let testNamePrefix = - errorName & " error response is converted to " & errorType.name & " for " - test testNamePrefix & "sending a manual RPC method request": + test prefix & "sending a manual RPC method request": registerRpcMethods(response) + await runBefore(before) expect errorType: discard await provider.send("eth_accounts") - test testNamePrefix & + test prefix & "calling a provider method that converts errors when calling a generated RPC request": registerRpcMethods(response) + await runBefore(before) expect errorType: discard await provider.listAccounts() - test testNamePrefix & "calling a view method of a contract": + test prefix & "calling a view method of a contract": registerRpcMethods(response) + await runBefore(before) expect errorType: + token = TestToken.new(token.address, provider.getSigner()) discard await token.balanceOf(Address.example) - test testNamePrefix & "calling a contract method that executes a transaction": + test prefix & "calling a contract method that executes a transaction": registerRpcMethods(response) + await runBefore(before) expect errorType: token = TestToken.new(token.address, provider.getSigner()) discard await token.mint( @@ -112,14 +120,16 @@ suite "Network errors - HTTP": TransactionOverrides(gasLimit: 100.u256.some, chainId: 1.u256.some), ) - test testNamePrefix & "sending a manual transaction": + test prefix & "sending a manual transaction": registerRpcMethods(response) + await runBefore(before) expect errorType: let tx = Transaction.example discard await provider.getSigner().sendTransaction(tx) - test testNamePrefix & "sending a raw transaction": + test prefix & "sending a raw transaction": registerRpcMethods(response) + await runBefore(before) expect errorType: const pk_with_funds = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" @@ -134,15 +144,17 @@ suite "Network errors - HTTP": let signedTx = await wallet.signTransaction(tx) discard await provider.sendTransaction(signedTx) - test testNamePrefix & "subscribing to blocks": + test prefix & "subscribing to blocks": registerRpcMethods(response) + await runBefore(before) expect errorType: let emptyHandler = proc(blckResult: ?!Block) = discard discard await provider.subscribe(emptyHandler) - test testNamePrefix & "subscribing to logs": + test prefix & "subscribing to logs": registerRpcMethods(response) + await runBefore(before) expect errorType: let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) @@ -150,277 +162,250 @@ suite "Network errors - HTTP": discard discard await provider.subscribe(filter, emptyHandler) - testCustomResponse("429", Http429, "Too many requests", HttpRequestLimitError) - testCustomResponse("408", Http408, "Request timed out", HttpRequestTimeoutError) - testCustomResponse("non-429", Http500, "Server error", JsonRpcProviderError) + proc testCustomHttpResponse( + errorName: string, + responseHttpCode: HttpCode, + responseText: string, + errorType: type CatchableError, + ) = + let response = proc( + request: HttpRequestRef + ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = + try: + return await request.respond(responseHttpCode, responseText) + except HttpWriteError as exc: + return defaultResponse(exc) - test "raises RpcNetworkError when reading response headers times out": - privateAccess(JsonRpcProvider) - privateAccess(RpcHttpClient) + let prefix = errorName & " error response is converted to " & errorType.name - let responseTimeout = proc( + testCustomResponse(prefix, response, errorType) + + testCustomHttpResponse("429", Http429, "Too many requests", HttpRequestLimitError) + testCustomHttpResponse("408", Http408, "Request timed out", HttpRequestTimeoutError) + testCustomHttpResponse("non-429", Http500, "Server error", JsonRpcProviderError) + testCustomResponse( + "raises RpcNetworkError after a timeout waiting for reading response headers", + response = proc( request: HttpRequestRef ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = try: await sleepAsync(5.minutes) return await request.respond(Http200, "OK") except HttpWriteError as exc: - return defaultResponse(exc) + return defaultResponse(exc), + RpcNetworkError, + before = proc(): Future[void] {.async.} = + privateAccess(JsonRpcProvider) + privateAccess(RpcHttpClient) + let rpcClient = await provider.client + let client: RpcHttpClient = (RpcHttpClient)(rpcClient) + client.httpSession = HttpSessionRef.new(headersTimeout = 1.millis), + ) - let rpcClient = await provider.client - let client: RpcHttpClient = (RpcHttpClient)(rpcClient) - client.httpSession = HttpSessionRef.new(headersTimeout = 1.millis) - mockServer.registerRpcResponse("eth_accounts", responseTimeout) + testCustomResponse( + "raises RpcNetworkError for a closed connection", + response = proc( + request: HttpRequestRef + ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = + # Simulate a closed connection + return HttpResponseRef.new(), + RpcNetworkError, + before = proc(): Future[void] {.async.} = + await mockServer.stop() + , + ) - expect RpcNetworkError: - discard await provider.send("eth_accounts") - - test "raises RpcNetworkError when connection is closed": - await mockServer.stop() - expect RpcNetworkError: - discard await provider.send("eth_accounts") - - test "raises RpcNetworkError when connection times out": - privateAccess(JsonRpcProvider) - privateAccess(RpcHttpClient) - let rpcClient = await provider.client - let client: RpcHttpClient = (RpcHttpClient)(rpcClient) - client.httpSession.connectTimeout = 10.millis - - let blockingSocket = newSocket() - blockingSocket.setSockOpt(OptReuseAddr, true) - blockingSocket.bindAddr(Port(9999)) - - await client.connect("http://localhost:9999") - - expect RpcNetworkError: + testCustomResponse( + "raises RpcNetworkError for a timed out connection", + response = proc( + request: HttpRequestRef + ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = + # Simulate a closed connection + return HttpResponseRef.new(), + RpcNetworkError, # msg: Failed to send POST Request with JSON-RPC: Connection timed out - discard await provider.send("eth_accounts") + before = proc(): Future[void] {.async.} = + privateAccess(JsonRpcProvider) + privateAccess(RpcHttpClient) + let rpcClient = await provider.client + let client: RpcHttpClient = (RpcHttpClient)(rpcClient) + client.httpSession.connectTimeout = 10.millis - # We don't need to recreate each and every possible exception condition, as - # they are all wrapped up in RpcPostError and converted to RpcNetworkError. - # The tests above cover this conversion. + blockingSocket = newSocket() + blockingSocket.setSockOpt(OptReuseAddr, true) + blockingSocket.bindAddr(Port(9999)) -# suite "Network errors - WebSocket": + await client.connect("http://localhost:9999") + , + ) -# var provider: JsonRpcProvider -# var mockWsServer: MockWebSocketServer -# var token: TestToken +suite "Network errors - WebSocket": + var provider: JsonRpcProvider + var token: TestToken + var mockWsServer: MockWebSocketServer -# setup: -# mockWsServer = MockWebSocketServer.init(initTAddress("127.0.0.1:0")) -# await mockWsServer.start() -# # Get the actual bound address -# let actualAddress = mockWsServer.localAddress() -# provider = JsonRpcProvider.new("ws://" & $actualAddress & "/ws") + setup: + mockWsServer = MockWebSocketServer.init(initTAddress("127.0.0.1:0")) + await mockWsServer.start() + # Get the actual bound address + provider = JsonRpcProvider.new("ws://" & $mockWsServer.localAddress) -# let deployment = readDeployment() -# token = TestToken.new(!deployment.address(TestToken), provider) + let deployment = readDeployment() + token = TestToken.new(!deployment.address(TestToken), provider) -# teardown: -# await provider.close() -# await mockWsServer.stop() + teardown: + await mockWsServer.stop() + try: + await provider.close() + except WebsocketConnectionError: + # WebsocketConnectionError is raised when the connection is already closed + discard + provider = nil -# proc registerRpcMethods(behavior: WebSocketBehavior) = -# mockWsServer.registerRpcBehavior("eth_accounts", behavior) -# mockWsServer.registerRpcBehavior("eth_call", behavior) -# mockWsServer.registerRpcBehavior("eth_sendTransaction", behavior) -# mockWsServer.registerRpcBehavior("eth_sendRawTransaction", behavior) -# mockWsServer.registerRpcBehavior("eth_newBlockFilter", behavior) -# mockWsServer.registerRpcBehavior("eth_newFilter", behavior) -# mockWsServer.registerRpcBehavior("eth_subscribe", behavior) + proc registerRpcMethods(response: WebSocketResponse) = + mockWsServer.registerRpcResponse("eth_accounts", response) + mockWsServer.registerRpcResponse("eth_call", response) + mockWsServer.registerRpcResponse("eth_sendTransaction", response) + mockWsServer.registerRpcResponse("eth_sendRawTransaction", response) + mockWsServer.registerRpcResponse("eth_subscribe", response) -# proc testCustomBehavior(errorName: string, behavior: WebSocketBehavior, errorType: type CatchableError) = -# let testNamePrefix = errorName & " behavior is converted to " & errorType.name & " for " + proc testCustomResponse( + name: string, + errorType: type CatchableError, + response: WebSocketResponse, + before: Before = nil, + ) = + test name & " when sending a manual RPC method request": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + discard await provider.send("eth_accounts") -# test testNamePrefix & "sending a manual RPC method request": -# registerRpcMethods(behavior) -# expect errorType: -# discard await provider.send("eth_accounts") + test name & + " when calling a provider method that converts errors when calling a generated RPC request": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + discard await provider.listAccounts() -# test testNamePrefix & "calling a provider method that converts errors": -# registerRpcMethods(behavior) -# expect errorType: -# discard await provider.listAccounts() + test name & " when calling a view method of a contract": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + token = TestToken.new(token.address, provider.getSigner()) + discard await token.balanceOf(Address.example) -# test testNamePrefix & "calling a view method of a contract": -# registerRpcMethods(behavior) -# expect errorType: -# discard await token.balanceOf(Address.example) + test name & " when calling a contract method that executes a transaction": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + token = TestToken.new(token.address, provider.getSigner()) + discard await token.mint( + Address.example, + 100.u256, + TransactionOverrides(gasLimit: 100.u256.some, chainId: 1.u256.some), + ) -# test testNamePrefix & "calling a contract method that executes a transaction": -# registerRpcMethods(behavior) -# expect errorType: -# token = TestToken.new(token.address, provider.getSigner()) -# discard await token.mint( -# Address.example, 100.u256, -# TransactionOverrides(gasLimit: 100.u256.some, chainId: 1.u256.some) -# ) + test name & " when sending a manual transaction": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + let tx = Transaction.example + discard await provider.getSigner().sendTransaction(tx) -# test testNamePrefix & "sending a manual transaction": -# registerRpcMethods(behavior) -# expect errorType: -# let tx = Transaction.example -# discard await provider.getSigner().sendTransaction(tx) + test name & " when sending a raw transaction": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + const pk_with_funds = + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + let wallet = !Wallet.new(pk_with_funds) + let tx = Transaction( + to: wallet.address, + nonce: some 0.u256, + chainId: some 31337.u256, + gasPrice: some 1_000_000_000.u256, + gasLimit: some 21_000.u256, + ) + let signedTx = await wallet.signTransaction(tx) + discard await provider.sendTransaction(signedTx) -# test testNamePrefix & "sending a raw transaction": -# registerRpcMethods(behavior) -# expect errorType: -# const pk_with_funds = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" -# let wallet = !Wallet.new(pk_with_funds) -# let tx = Transaction( -# to: wallet.address, -# nonce: some 0.u256, -# chainId: some 31337.u256, -# gasPrice: some 1_000_000_000.u256, -# gasLimit: some 21_000.u256, -# ) -# let signedTx = await wallet.signTransaction(tx) -# discard await provider.sendTransaction(signedTx) + test name & " when subscribing to blocks": + privateAccess(JsonRpcProvider) + registerRpcMethods(response) + await runBefore(before) + expect errorType: + let emptyHandler = proc(blckResult: ?!Block) = + discard + discard await provider.subscribe(emptyHandler) -# test testNamePrefix & "subscribing to blocks": -# registerRpcMethods(behavior) -# expect errorType: -# let emptyHandler = proc(blckResult: ?!Block) = discard -# discard await provider.subscribe(emptyHandler) + test name & " when subscribing to logs": + registerRpcMethods(response) + await runBefore(before) + expect errorType: + let filter = + EventFilter(address: Address.example, topics: @[array[32, byte].example]) + let emptyHandler = proc(log: ?!Log) = + discard + discard await provider.subscribe(filter, emptyHandler) -# test testNamePrefix & "subscribing to logs": -# registerRpcMethods(behavior) -# expect errorType: -# let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) -# let emptyHandler = proc(log: ?!Log) = discard -# discard await provider.subscribe(filter, emptyHandler) + test "should not raise error on normal connection and request": + mockWsServer.registerRpcResponse( + "eth_accounts", + proc(ws: WSSession) {.async.} = + let response = + ResponseTx(jsonrpc: "2.0", id: 1, result: % @["123"], kind: rkResult) + await ws.send(response.toJson) + , + ) -# # WebSocket close codes equivalent to HTTP status codes -# testCustomBehavior( -# "Policy violation close (rate limit)", -# createBehavior(CloseWithCode, StatusPolicyError, "Policy violation - rate limited"), -# WebSocketPolicyError -# ) + let accounts = await provider.send("eth_accounts") + check @["123"] == !seq[string].fromJson(accounts) -# testCustomBehavior( -# "Server error close", -# createBehavior(CloseWithCode, StatusUnexpectedError, "Internal server error"), -# JsonRpcProviderError -# ) - -# # testCustomBehavior( -# # "Service unavailable close", -# # createBehavior(CloseWithCode, StatusCodes.TryAgainLater, "Try again later"), -# # WebSocketServiceUnavailableError -# # ) - -# testCustomBehavior( -# "Abrupt disconnect", -# createBehavior(AbruptClose), -# RpcNetworkError -# ) - -# test "raises RpcNetworkError when WebSocket connection times out": -# registerRpcMethods(createBehavior(Timeout, delay = 5.minutes)) - -# # Set a short timeout on the WebSocket client -# privateAccess(JsonRpcProvider) -# privateAccess(RpcWebSocketClient) - -# let rpcClient = await provider.client -# let client = RpcWebSocketClient(rpcClient) -# # Note: Actual timeout setting depends on nim-websock implementation -# # This may need to be adjusted based on available APIs - -# expect RpcNetworkError: -# discard await provider.send("eth_accounts").wait(1.seconds) - -# test "raises RpcNetworkError when WebSocket connection is closed unexpectedly": -# # Start a request, then close the server -# let sendFuture = provider.send("eth_accounts") -# await sleepAsync(10.millis) -# await mockWsServer.stop() - -# expect RpcNetworkError: -# discard await sendFuture - -# test "raises RpcNetworkError when WebSocket connection fails to establish": -# # Stop the server first -# await mockWsServer.stop() - -# expect RpcNetworkError: -# let deadProvider = JsonRpcProvider.new("ws://127.0.0.1:9999/ws") -# discard await deadProvider.send("eth_accounts") - -# test "handles WebSocket protocol errors gracefully": -# registerRpcMethods(createBehavior(InvalidFrame)) - -# expect JsonRpcProviderError: # or whatever error nim-json-rpc maps protocol errors to -# discard await provider.send("eth_accounts") - -# test "handles oversized WebSocket messages": -# registerRpcMethods(createBehavior(MessageTooBig)) - -# expect RpcNetworkError: # Large message handling depends on client limits -# discard await provider.send("eth_accounts") - -# test "raises timeout error on slow WebSocket handshake": -# # Create a server that delays the WebSocket upgrade -# let slowServer = MockWebSocketServer.init(initTAddress("127.0.0.1:0")) -# # This would need custom implementation to delay handshake - -# expect WebSocketTimeoutError: -# let slowProvider = JsonRpcProvider.new("ws://127.0.0.1:9998/ws") -# discard await slowProvider.send("eth_accounts").wait(100.millis) - -# test "handles connection drops during message exchange": -# # Register normal behavior initially -# registerRpcMethods(createBehavior(Normal)) - -# # Start multiple requests -# let futures = @[ -# provider.send("eth_accounts"), -# provider.send("eth_call"), -# provider.send("eth_newBlockFilter") -# ] - -# # Close connections after a short delay -# await sleepAsync(5.millis) -# for conn in mockWsServer.connections: -# await conn.close(StatusCodes.AbnormalClosure, "Abnormal closure") - -# # All should fail with network error -# for future in futures: -# expect RpcNetworkError: -# discard await future - -# test "recovers from temporary WebSocket disconnections": -# # This test would verify client reconnection logic if implemented -# # Initial connection works -# registerRpcMethods(createBehavior(Normal)) -# let result1 = await provider.send("eth_accounts") -# check result1.isOk - -# # Simulate connection drop -# for conn in mockWsServer.connections: -# await conn.close(StatusCodes.GoingAway, "Going away") - -# # Depending on provider implementation, this might auto-reconnect -# # or need manual reconnection -# expect RpcNetworkError: -# discard await provider.send("eth_accounts") - -# test "handles WebSocket ping/pong timeouts": -# # This would test the ping/pong mechanism if the client supports it -# registerRpcMethods(createBehavior(Normal)) - -# # Mock a scenario where server doesn't respond to pings -# for conn in mockWsServer.connections: -# # Disable pong responses (if we had access to this) -# conn.onPing = nil - -# # This test would need to trigger ping timeout -# # The exact implementation depends on the websocket client capabilities - -# test "handles WebSocket close frame with invalid payload": -# # Test handling of malformed close frames -# registerRpcMethods(createBehavior(CloseWithCode, StatusCodes.ProtocolError, "")) - -# expect JsonRpcProviderError: -# discard await provider.send("eth_accounts") + testCustomResponse( + "should raise JsonRpcProviderError for a returned error response", + JsonRpcProviderError, + proc(ws: WSSession) {.async.} = + let response = ResponseTx( + jsonrpc: "2.0", + id: 1, + error: ResponseError(code: 1, message: "some error"), + kind: rkError, + ) + await ws.send(response.toJson) + , + ) + testCustomResponse( + "raises WebsocketConnectionError for closed connection", + WebsocketConnectionError, + proc(ws: WSSession) {.async.} = + # Simulate a closed connection + await ws.close(StatusGoingAway, "Going away") + , + ) + testCustomResponse( + "raises WebsocketConnectionError for failed connection", + WebsocketConnectionError, + response = proc(ws: WSSession) {.async.} = + return , + before = proc() {.async.} = + # Used to simulate an HttpError, which is also raised for "Timeout expired + # while receiving headers", however replicating that exact scenario would + # take 120s as the HttpHeadersTimeout is hardcoded to 120 seconds. + provider = JsonRpcProvider.new("ws://localhost:9999"), + ) + testCustomResponse( + "raises JsonRpcProviderError for exceptions in onProcessMessage callback", + JsonRpcProviderError, + response = proc(ws: WSSession) {.async.} = + let response = ResponseTx(jsonrpc: "2.0", id: 1, result: %"", kind: rkResult) + await ws.send(response.toJson) + , + before = proc() {.async.} = + privateAccess(JsonRpcProvider) + let rpcClient = await provider.client + rpcClient.onProcessMessage = proc( + client: RpcClient, line: string + ): Result[bool, string] {.gcsafe, raises: [].} = + return err "Some error", + )