Convert retryable RPC/HTTP errors to RpcNetworkError type in ethers

Converts specific errors to RpcNetworkError, which can be bubbled to applications at a higher level and retried on the network (eg with exponential backoff) until resolved or timed out.
This commit is contained in:
Eric 2025-05-27 18:03:19 +10:00
parent bbced46733
commit af35395ace
No known key found for this signature in database
17 changed files with 417 additions and 172 deletions

View File

@ -69,7 +69,7 @@ func addOverridesParameter*(procedure: var NimNode) =
func addAsyncPragma*(procedure: var NimNode) =
procedure.addPragma nnkExprColonExpr.newTree(
quote do: async,
quote do: (raises: [CancelledError, ProviderError, EthersError])
quote do: (raises: [CancelledError, ProviderError, EthersError, RpcNetworkError])
)
func addUsedPragma*(procedure: var NimNode) =

View File

@ -34,13 +34,15 @@ proc decodeResponse(T: type, bytes: seq[byte]): T {.raises: [ContractError].} =
proc call(
provider: Provider, transaction: Transaction, overrides: TransactionOverrides
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
if overrides of CallOverrides and blockTag =? CallOverrides(overrides).blockTag:
await provider.call(transaction, blockTag)
else:
await provider.call(transaction)
proc callTransaction*(call: ContractCall) {.async: (raises: [ProviderError, SignerError, CancelledError]).} =
proc callTransaction*(
call: ContractCall
) {.async: (raises: [ProviderError, SignerError, CancelledError, RpcNetworkError]).} =
var transaction = createTransaction(call)
if signer =? call.contract.signer and transaction.sender.isNone:
@ -48,7 +50,10 @@ proc callTransaction*(call: ContractCall) {.async: (raises: [ProviderError, Sign
discard await call.contract.provider.call(transaction, call.overrides)
proc callTransaction*(call: ContractCall, ReturnType: type): Future[ReturnType] {.async: (raises: [ProviderError, SignerError, ContractError, CancelledError]).} =
proc callTransaction*(
call: ContractCall,
ReturnType: type
): Future[ReturnType] {.async: (raises: [ProviderError, SignerError, ContractError, CancelledError, RpcNetworkError]).} =
var transaction = createTransaction(call)
if signer =? call.contract.signer and transaction.sender.isNone:
@ -57,7 +62,9 @@ proc callTransaction*(call: ContractCall, ReturnType: type): Future[ReturnType]
let response = await call.contract.provider.call(transaction, call.overrides)
return decodeResponse(ReturnType, response)
proc sendTransaction*(call: ContractCall): Future[?TransactionResponse] {.async: (raises: [SignerError, ProviderError, CancelledError]).} =
proc sendTransaction*(
call: ContractCall
): Future[?TransactionResponse] {.async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError]).} =
if signer =? call.contract.signer:
withLock(signer):
let transaction = createTransaction(call)

View File

@ -7,6 +7,10 @@ type
SubscriptionError* = object of EthersError
ProviderError* = object of EthersError
data*: ?seq[byte]
RpcNetworkError* = object of EthersError
RpcHttpErrorResponse* = object of RpcNetworkError
RequestLimitError* = object of RpcHttpErrorResponse
RequestTimeoutError* = object of RpcHttpErrorResponse
{.push raises:[].}
@ -16,3 +20,7 @@ proc toErr*[E1: ref CatchableError, E2: EthersError](
msg: string = e1.msg): ref E2 =
return newException(E2, msg, e1)
proc raiseNetworkError*(
error: ref CatchableError) {.raises: [RpcNetworkError].} =
raise newException(RpcNetworkError, error.msg, error)

View File

@ -103,84 +103,86 @@ func toTransaction*(past: PastTransaction): Transaction =
method getBlockNumber*(
provider: Provider
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method getBlock*(
provider: Provider, tag: BlockTag
): Future[?Block] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[?Block] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method call*(
provider: Provider, tx: Transaction, blockTag = BlockTag.latest
): Future[seq[byte]] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[seq[byte]] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method getGasPrice*(
provider: Provider
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method getTransactionCount*(
provider: Provider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method getTransaction*(
provider: Provider, txHash: TransactionHash
): Future[?PastTransaction] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[?PastTransaction] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method getTransactionReceipt*(
provider: Provider, txHash: TransactionHash
): Future[?TransactionReceipt] {.
base, async: (raises: [ProviderError, CancelledError])
base, async: (raises: [ProviderError, CancelledError, RpcNetworkError])
.} =
doAssert false, "not implemented"
method sendTransaction*(
provider: Provider, rawTransaction: seq[byte]
): Future[TransactionResponse] {.
base, async: (raises: [ProviderError, CancelledError])
base, async: (raises: [ProviderError, CancelledError, RpcNetworkError])
.} =
doAssert false, "not implemented"
method getLogs*(
provider: Provider, filter: EventFilter
): Future[seq[Log]] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[seq[Log]] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method estimateGas*(
provider: Provider, transaction: Transaction, blockTag = BlockTag.latest
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method getChainId*(
provider: Provider
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method subscribe*(
provider: Provider, filter: EventFilter, callback: LogHandler
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method subscribe*(
provider: Provider, callback: BlockHandler
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} =
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method unsubscribe*(
subscription: Subscription
) {.base, async: (raises: [ProviderError, CancelledError]).} =
) {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method isSyncing*(provider: Provider): Future[bool] {.base, async: (raises: [ProviderError, CancelledError]).} =
method isSyncing*(
provider: Provider
): Future[bool] {.base, async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
proc replay*(
provider: Provider, tx: Transaction, blockNumber: UInt256
) {.async: (raises: [ProviderError, CancelledError]).} =
) {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
# Replay transaction at block. Useful for fetching revert reasons, which will
# be present in the raised error message. The replayed block number should
# include the state of the chain in the block previous to the block in which
@ -193,7 +195,7 @@ proc replay*(
proc ensureSuccess(
provider: Provider, receipt: TransactionReceipt
) {.async: (raises: [ProviderError, CancelledError]).} =
) {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
## If the receipt.status is Failed, the tx is replayed to obtain a revert
## reason, after which a ProviderError with the revert reason is raised.
## If no revert reason was obtained
@ -241,7 +243,7 @@ proc confirm*(
if number > blockNumber:
blockNumber = number
blockEvent.fire()
except ProviderError, CancelledError:
except ProviderError, CancelledError, RpcNetworkError:
# there's nothing we can do here
discard

View File

@ -56,7 +56,7 @@ proc new*(
var client: RpcClient
var subscriptions: JsonRpcSubscriptions
proc initialize() {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
proc initialize() {.async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]).} =
convertError:
case parseUri(url).scheme
of "ws", "wss":
@ -65,7 +65,7 @@ proc new*(
client = websocket
subscriptions = JsonRpcSubscriptions.new(websocket)
else:
let http = newRpcHttpClient(getHeaders = jsonHeaders)
let http = newRpcHttpClient()
await http.connect(url)
client = http
subscriptions = JsonRpcSubscriptions.new(http,
@ -73,14 +73,14 @@ proc new*(
subscriptions.start()
proc awaitClient(): Future[RpcClient] {.
async: (raises: [JsonRpcProviderError, CancelledError])
async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError])
.} =
convertError:
await initialized
return client
proc awaitSubscriptions(): Future[JsonRpcSubscriptions] {.
async: (raises: [JsonRpcProviderError, CancelledError])
async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError])
.} =
convertError:
await initialized
@ -91,28 +91,30 @@ proc new*(
proc callImpl(
client: RpcClient, call: string, args: JsonNode
): Future[JsonNode] {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
): Future[JsonNode] {.async: (raises: [JsonRpcProviderError, CancelledError, JsonRpcError]).} =
try:
let response = await client.call(call, %args)
without json =? JsonNode.fromJson(response.string), error:
raiseJsonRpcProviderError "Failed to parse response '" & response.string & "': " &
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.msg
raiseJsonRpcProviderError error
proc send*(
provider: JsonRpcProvider, call: string, arguments: seq[JsonNode] = @[]
): Future[JsonNode] {.async: (raises: [ProviderError, CancelledError]).} =
): 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]).} =
): Future[seq[Address]] {.async: (raises: [JsonRpcProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
return await client.eth_accounts()
@ -125,56 +127,56 @@ proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
method getBlockNumber*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
return await client.eth_blockNumber()
method getBlock*(
provider: JsonRpcProvider, tag: BlockTag
): Future[?Block] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[?Block] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
return await client.eth_getBlockByNumber(tag, false)
method call*(
provider: JsonRpcProvider, tx: Transaction, blockTag = BlockTag.latest
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
return await client.eth_call(tx, blockTag)
method getGasPrice*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
return await client.eth_gasPrice()
method getTransactionCount*(
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag)
method getTransaction*(
provider: JsonRpcProvider, txHash: TransactionHash
): Future[?PastTransaction] {.async: (raises: [ProviderError, CancelledError]).} =
): 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]).} =
): 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]).} =
): Future[seq[Log]] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
let logsJson =
@ -196,7 +198,7 @@ method estimateGas*(
provider: JsonRpcProvider,
transaction: Transaction,
blockTag = BlockTag.latest,
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
try:
convertError:
let client = await provider.client
@ -211,7 +213,7 @@ method estimateGas*(
method getChainId*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
try:
@ -223,7 +225,7 @@ method getChainId*(
method sendTransaction*(
provider: JsonRpcProvider, rawTransaction: seq[byte]
): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let
client = await provider.client
@ -233,7 +235,7 @@ method sendTransaction*(
method subscribe*(
provider: JsonRpcProvider, filter: EventFilter, onLog: LogHandler
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeLogs(filter, onLog)
@ -241,7 +243,7 @@ method subscribe*(
method subscribe*(
provider: JsonRpcProvider, onBlock: BlockHandler
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeBlocks(onBlock)
@ -249,7 +251,7 @@ method subscribe*(
method unsubscribe*(
subscription: JsonRpcSubscription
) {.async: (raises: [ProviderError, CancelledError]).} =
) {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let subscriptions = subscription.subscriptions
let id = subscription.id
@ -257,7 +259,7 @@ method unsubscribe*(
method isSyncing*(
provider: JsonRpcProvider
): Future[bool] {.async: (raises: [ProviderError, CancelledError]).} =
): Future[bool] {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
let response = await provider.send("eth_syncing")
if response.kind == JsonNodeKind.JObject:
return true
@ -265,7 +267,7 @@ method isSyncing*(
method close*(
provider: JsonRpcProvider
) {.async: (raises: [ProviderError, CancelledError]).} =
) {.async: (raises: [ProviderError, CancelledError, RpcNetworkError]).} =
convertError:
let client = await provider.client
let subscriptions = await provider.subscriptions
@ -274,24 +276,13 @@ method close*(
# Signer
proc raiseJsonRpcSignerError(
message: string) {.raises: [JsonRpcSignerError].} =
var message = message
if json =? JsonNode.fromJson(message):
if "message" in json:
message = json{"message"}.getStr
raise newException(JsonRpcSignerError, message)
template convertSignerError(body) =
try:
body
except CancelledError as error:
raise error
except JsonRpcError as error:
raiseJsonRpcSignerError(error.msg)
except CatchableError as error:
raise newException(JsonRpcSignerError, error.msg)
raise newException(JsonRpcSignerError, error.msg, error)
method provider*(signer: JsonRpcSigner): Provider
{.gcsafe, raises: [SignerError].} =
@ -300,7 +291,7 @@ method provider*(signer: JsonRpcSigner): Provider
method getAddress*(
signer: JsonRpcSigner
): Future[Address] {.async: (raises: [ProviderError, SignerError, CancelledError]).} =
): Future[Address] {.async: (raises: [ProviderError, SignerError, CancelledError, RpcNetworkError]).} =
if address =? signer.address:
return address
@ -308,11 +299,11 @@ method getAddress*(
if accounts.len > 0:
return accounts[0]
raiseJsonRpcSignerError "no address found"
raise newException(SignerError, "no address found")
method signMessage*(
signer: JsonRpcSigner, message: seq[byte]
): Future[seq[byte]] {.async: (raises: [SignerError, CancelledError]).} =
): Future[seq[byte]] {.async: (raises: [SignerError, CancelledError, RpcNetworkError]).} =
convertSignerError:
let client = await signer.provider.client
let address = await signer.getAddress()
@ -321,7 +312,7 @@ method signMessage*(
method sendTransaction*(
signer: JsonRpcSigner, transaction: Transaction
): Future[TransactionResponse] {.
async: (raises: [SignerError, ProviderError, CancelledError])
async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError])
.} =
convertError:
let

View File

@ -31,19 +31,53 @@ func new*(_: type JsonRpcProviderError, json: JsonNode): ref JsonRpcProviderErro
error
proc raiseJsonRpcProviderError*(
message: string) {.raises: [JsonRpcProviderError].} =
if json =? JsonNode.fromJson(message):
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:
body
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(RequestLimitError, error.msg, error)
elif error.status == 408:
raise newException(RequestTimeoutError, error.msg, error)
else:
raiseJsonRpcProviderError(error)
except JsonRpcError as error:
raiseJsonRpcProviderError(error.msg)
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.msg)
raiseJsonRpcProviderError(error)

View File

@ -122,7 +122,7 @@ 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:

View File

@ -1,4 +1,5 @@
import pkg/questionable
import pkg/json_rpc/errors
import ./basics
import ./errors
import ./provider
@ -15,16 +16,6 @@ type
template raiseSignerError*(message: string, parent: ref CatchableError = nil) =
raise newException(SignerError, message, parent)
template convertError(body) =
try:
body
except CancelledError as error:
raise error
except ProviderError as error:
raise error # do not convert provider errors
except CatchableError as error:
raiseSignerError(error.msg)
method provider*(
signer: Signer): Provider {.base, gcsafe, raises: [SignerError].} =
doAssert false, "not implemented"
@ -32,42 +23,41 @@ method provider*(
method getAddress*(
signer: Signer
): Future[Address] {.
base, async: (raises: [ProviderError, SignerError, CancelledError])
base, async: (raises: [ProviderError, SignerError, CancelledError, RpcNetworkError])
.} =
doAssert false, "not implemented"
method signMessage*(
signer: Signer, message: seq[byte]
): Future[seq[byte]] {.base, async: (raises: [SignerError, CancelledError]).} =
): Future[seq[byte]] {.base, async: (raises: [SignerError, CancelledError, RpcNetworkError]).} =
doAssert false, "not implemented"
method sendTransaction*(
signer: Signer, transaction: Transaction
): Future[TransactionResponse] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
base, async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError])
.} =
doAssert false, "not implemented"
method getGasPrice*(
signer: Signer
): Future[UInt256] {.
base, async: (raises: [ProviderError, SignerError, CancelledError])
base, async: (raises: [ProviderError, SignerError, CancelledError, RpcNetworkError])
.} =
return await signer.provider.getGasPrice()
method getTransactionCount*(
signer: Signer, blockTag = BlockTag.latest
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
base, async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError])
.} =
convertError:
let address = await signer.getAddress()
return await signer.provider.getTransactionCount(address, blockTag)
let address = await signer.getAddress()
return await signer.provider.getTransactionCount(address, blockTag)
method estimateGas*(
signer: Signer, transaction: Transaction, blockTag = BlockTag.latest
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
base, async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError])
.} =
var transaction = transaction
transaction.sender = some(await signer.getAddress())
@ -76,14 +66,14 @@ method estimateGas*(
method getChainId*(
signer: Signer
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
base, async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError])
.} =
return await signer.provider.getChainId()
method getNonce(
signer: Signer
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
base, async: (raises: [SignerError, ProviderError, CancelledError, RpcNetworkError])
.} =
return await signer.getTransactionCount(BlockTag.pending)
@ -103,15 +93,14 @@ template withLock*(signer: Signer, body: untyped) =
method populateTransaction*(
signer: Signer,
transaction: Transaction): Future[Transaction]
{.base, async: (raises: [CancelledError, ProviderError, SignerError]).} =
{.base, async: (raises: [CancelledError, ProviderError, SignerError, RpcNetworkError]).} =
## Populates a transaction with sender, chainId, gasPrice, nonce, and gasLimit.
## NOTE: to avoid async concurrency issues, this routine should be called with
## a lock if it is followed by sendTransaction. For reference, see the `send`
## function in contract.nim.
var address: Address
convertError:
address = await signer.getAddress()
address = await signer.getAddress()
if sender =? transaction.sender and sender != address:
raiseSignerError("from address mismatch")
@ -154,7 +143,7 @@ method populateTransaction*(
method cancelTransaction*(
signer: Signer,
tx: Transaction
): Future[TransactionResponse] {.base, async: (raises: [SignerError, CancelledError, ProviderError]).} =
): Future[TransactionResponse] {.base, async: (raises: [SignerError, CancelledError, ProviderError, RpcNetworkError]).} =
# cancels a transaction by sending with a 0-valued transaction to ourselves
# with the failed tx's nonce
@ -164,7 +153,7 @@ method cancelTransaction*(
raiseSignerError "transaction must have nonce"
withLock(signer):
convertError:
var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce)
cancelTx = await signer.populateTransaction(cancelTx)
return await signer.sendTransaction(cancelTx)
# convertError:
var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce)
cancelTx = await signer.populateTransaction(cancelTx)
return await signer.sendTransaction(cancelTx)

View File

@ -69,7 +69,7 @@ method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} =
method getAddress*(
wallet: Wallet): Future[Address]
{.async: (raises:[ProviderError, SignerError, CancelledError]).} =
{.async: (raises:[ProviderError, SignerError, CancelledError, RpcNetworkError]).} =
return wallet.address
@ -83,7 +83,7 @@ proc signTransaction*(wallet: Wallet,
method sendTransaction*(
wallet: Wallet,
transaction: Transaction): Future[TransactionResponse]
{.async: (raises:[SignerError, ProviderError, CancelledError]).} =
{.async: (raises:[SignerError, ProviderError, CancelledError, RpcNetworkError]).} =
let signed = await signTransaction(wallet, transaction)
return await provider(wallet).sendTransaction(signed)

View File

@ -6,10 +6,3 @@ type
func raiseWalletError*(message: string) {.raises: [WalletError].}=
raise newException(WalletError, message)
template convertError*(body) =
try:
body
except CancelledError as error:
raise error
except CatchableError as error:
raiseWalletError(error.msg)

View File

@ -13,13 +13,13 @@ method provider*(signer: MockSigner): Provider =
method getAddress*(
signer: MockSigner): Future[Address]
{.async: (raises:[ProviderError, SignerError, CancelledError]).} =
{.async: (raises:[ProviderError, SignerError, CancelledError, RpcNetworkError]).} =
return signer.address
method sendTransaction*(
signer: MockSigner,
transaction: Transaction): Future[TransactionResponse]
{.async: (raises:[SignerError, ProviderError, CancelledError]).} =
{.async: (raises:[SignerError, ProviderError, CancelledError, RpcNetworkError]).} =
signer.transactions.add(transaction)

View File

@ -0,0 +1,80 @@
import std/tables
import pkg/serde
import pkg/chronos/apps/http/httpclient
import pkg/chronos/apps/http/httpserver
import pkg/stew/byteutils
import pkg/questionable
export httpserver
{.push raises: [].}
type
RpcResponse* = proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CancelledError]), raises: [].}
MockHttpServer* = object
server: HttpServerRef
rpcResponses: ref Table[string, RpcResponse]
RequestRx {.deserialize.} = object
jsonrpc*: string
id* : int
`method`* : string
proc init*(_: type MockHttpServer, address: TransportAddress): MockHttpServer =
var server: MockHttpServer
proc processRequest(r: RequestFence): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
if r.isErr:
return defaultResponse()
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")
try:
let rpcResponseProc = server.rpcResponses[req.method]
return await rpcResponseProc(request)
except KeyError as e:
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)
let
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
serverFlags = {HttpServerFlags.Http11Pipeline}
res = HttpServerRef.new(address, processRequest,
socketFlags = socketFlags,
serverFlags = serverFlags)
server = MockHttpServer(server: res.get(), rpcResponses: newTable[string, RpcResponse]())
return server
template registerRpcMethod*(server: MockHttpServer, `method`: string, rpcResponse: untyped): untyped =
server.rpcResponses[`method`] = rpcResponse
proc address*(server: MockHttpServer): TransportAddress =
server.server.instance.localAddress()
proc start*(server: MockHttpServer) =
server.server.start()
proc stop*(server: MockHttpServer) {.async: (raises: []).} =
await server.server.stop()
await server.server.closeWait()

View File

@ -0,0 +1,31 @@
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
{.push raises: [].}
type MockRpcHttpServer* = ref object of RootObj
srv: RpcHttpServer
proc new*(_: type MockRpcHttpServer): MockRpcHttpServer {.raises: [JsonRpcError].} =
let srv = newRpcHttpServer(["127.0.0.1:0"])
MockRpcHttpServer(srv: srv)
template registerRpcMethod*(server: MockRpcHttpServer, path: string, body: untyped): untyped =
server.srv.router.rpc(path, body)
method start*(server: MockRpcHttpServer) {.gcsafe, base.} =
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

@ -0,0 +1,53 @@
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
import ./mockRpcHttpServer
export mockRpcHttpServer
{.push raises: [].}
type MockRpcHttpServerSubscriptions* = ref object of MockRpcHttpServer
filters*: seq[string]
nextGetChangesReturnsError*: bool
proc new*(_: type MockRpcHttpServerSubscriptions): MockRpcHttpServerSubscriptions {.raises: [JsonRpcError].} =
let srv = newRpcHttpServer(["127.0.0.1:0"])
MockRpcHttpServerSubscriptions(filters: @[], srv: srv, nextGetChangesReturnsError: false)
proc invalidateFilter*(server: MockRpcHttpServerSubscriptions, jsonId: JsonNode) =
server.filters.keepItIf it != jsonId.getStr
method start*(server: MockRpcHttpServerSubscriptions) =
server.registerRpcMethod("eth_newFilter") do(filter: EventFilter) -> string:
let filterId = "0x" & (array[16, byte].example).toHex
server.filters.add filterId
return filterId
server.registerRpcMethod("eth_newBlockFilter") do() -> string:
let filterId = "0x" & (array[16, byte].example).toHex
server.filters.add filterId
return filterId
server.registerRpcMethod("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.registerRpcMethod("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
procCall MockRpcHttpServer(server).start()

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

@ -1,7 +1,17 @@
import std/unittest
import std/sequtils
import std/typetraits
import stew/byteutils
import pkg/asynctest/chronos/unittest
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 ./mocks/mockHttpServer
import ../../examples
import ../../hardhat
suite "JSON RPC errors":
@ -25,3 +35,106 @@ suite "JSON RPC errors":
}
}
check JsonRpcProviderError.new(error).data == some @[0xab'u8, 0xcd'u8]
type
TestToken = ref object of Erc20Token
method mint(token: TestToken, holder: Address, amount: UInt256): Confirmable {.base, contract.}
suite "Network errors":
var provider: JsonRpcProvider
var mockServer: MockHttpServer
var token: TestToken
setup:
mockServer = MockHttpServer.init(initTAddress("127.0.0.1:0"))
mockServer.start()
provider = JsonRpcProvider.new("http://" & $mockServer.address)
let deployment = readDeployment()
token = TestToken.new(!deployment.address(TestToken), provider)
teardown:
await provider.close()
await mockServer.stop()
proc registerRpcMethods(response: RpcResponse) =
mockServer.registerRpcMethod("eth_accounts", response)
mockServer.registerRpcMethod("eth_call", response)
mockServer.registerRpcMethod("eth_sendTransaction", response)
mockServer.registerRpcMethod("eth_sendRawTransaction", response)
mockServer.registerRpcMethod("eth_newBlockFilter", response)
mockServer.registerRpcMethod("eth_newFilter", response)
# mockServer.registerRpcMethod("eth_subscribe", response) # TODO: handle
# eth_subscribe for websockets
proc testCustomResponse(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)
let testNamePrefix = errorName & " error response is converted to " & errorType.name & " for "
test testNamePrefix & "sending a manual RPC method request":
registerRpcMethods(response)
expect errorType:
discard await provider.send("eth_accounts")
test testNamePrefix & "calling a provider method that converts errors when calling a generated RPC request":
registerRpcMethods(response)
expect errorType:
discard await provider.listAccounts()
test testNamePrefix & "calling a view method of a contract":
registerRpcMethods(response)
expect errorType:
discard await token.balanceOf(Address.example)
test testNamePrefix & "calling a contract method that executes a transaction":
registerRpcMethods(response)
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 & "sending a manual transaction":
registerRpcMethods(response)
expect errorType:
let tx = Transaction.example
discard await provider.getSigner().sendTransaction(tx)
test testNamePrefix & "sending a raw transaction":
registerRpcMethods(response)
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 & "subscribing to blocks":
registerRpcMethods(response)
expect errorType:
let emptyHandler = proc(blckResult: ?!Block) = discard
discard await provider.subscribe(emptyHandler)
test testNamePrefix & "subscribing to logs":
registerRpcMethods(response)
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)
testCustomResponse("429", Http429, "Too many requests", RequestLimitError)
testCustomResponse("408", Http408, "Request timed out", RequestTimeoutError)
testCustomResponse("non-429", Http500, "Server error", JsonRpcProviderError)

View File

@ -8,7 +8,7 @@ import ethers/provider
import ethers/providers/jsonrpc/subscriptions
import ../../examples
import ./rpc_mock
import ./mocks/mockRpcHttpServerSubscriptions
suite "JsonRpcSubscriptions":
@ -111,18 +111,18 @@ suite "HTTP polling subscriptions - mock tests":
var subscriptions: PollingSubscriptions
var client: RpcHttpClient
var mockServer: MockRpcHttpServer
var mockServer: MockRpcHttpServerSubscriptions
privateAccess(PollingSubscriptions)
privateAccess(JsonRpcSubscriptions)
proc startServer() {.async.} =
mockServer = MockRpcHttpServer.new()
mockServer = MockRpcHttpServerSubscriptions.new()
mockServer.start()
await client.connect("http://" & $mockServer.localAddress()[0])
await client.connect("http://" & $MockRpcHttpServer(mockServer).localAddress()[0])
proc stopServer() {.async.} =
await mockServer.stop()
await MockRpcHttpServer(mockServer).stop()
setup:
client = newRpcHttpClient()