Compare commits

..

No commits in common. "main" and "v0.10.1" have entirely different histories.

54 changed files with 743 additions and 1332 deletions

View File

@ -1,11 +1,6 @@
name: CI name: CI
on: on: [push, pull_request, workflow_dispatch]
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs: jobs:
test: test:
@ -13,12 +8,10 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
nim: [2.0.14, 2.2.2] nim: [1.6.20, stable]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Nim - name: Install Nim
uses: iffy/install-nim@v4 uses: iffy/install-nim@v4
@ -39,7 +32,7 @@ jobs:
run: npm start & run: npm start &
- name: Build - name: Build
run: nimble install -y --maximumtaggedversions=2 run: nimble install -y
- name: Test - name: Test
run: nimble test -y run: nimble test -y

1
.gitignore vendored
View File

@ -6,4 +6,3 @@ nimble.paths
.idea .idea
.nimble .nimble
.envrc .envrc
nimbledeps

View File

@ -14,13 +14,7 @@ Use the [Nimble][2] package manager to add `ethers` to an existing
project. Add the following to its .nimble file: project. Add the following to its .nimble file:
```nim ```nim
requires "ethers >= 2.0.0 & < 3.0.0" requires "ethers >= 0.10.0 & < 0.11.0"
```
To avoid conflicts with previous versions of `contractabi`, use the following command to install dependencies:
```bash
nimble install --maximumtaggedversions=2
``` ```
Usage Usage
@ -137,22 +131,14 @@ You can now subscribe to Transfer events by calling `subscribe` on the contract
instance. instance.
```nim ```nim
proc handleTransfer(transferResult: ?!Transfer) = proc handleTransfer(transfer: Transfer) =
if transferResult.isOk: echo "received transfer: ", transfer
echo "received transfer: ", transferResult.value
else:
echo "error during transfer: ", transferResult.error.msg
let subscription = await token.subscribe(Transfer, handleTransfer) let subscription = await token.subscribe(Transfer, handleTransfer)
``` ```
When a Transfer event is emitted, the `handleTransfer` proc that you just When a Transfer event is emitted, the `handleTransfer` proc that you just
defined will be called with a [Result](https://github.com/arnetheduck/nim-results) type defined will be called.
which contains the event value.
In case there is some underlying error in the event subscription, the handler will
be called as well, but the Result will contain error instead, so do proper error
management in your handlers.
When you're no longer interested in these events, you can unsubscribe: When you're no longer interested in these events, you can unsubscribe:
@ -204,13 +190,6 @@ This library ships with some optional modules that provides convenience utilitie
- `ethers/erc20` module provides you with ERC20 token implementation and its events - `ethers/erc20` module provides you with ERC20 token implementation and its events
Hardhat websockets workaround
---------
If you're working with Hardhat, you might encounter an issue where [websocket subscriptions stop working after 5 minutes](https://github.com/NomicFoundation/hardhat/issues/2053).
This library provides a workaround using the compile time `ws_resubscribe` symbol. When this symbol is defined and set to a value greater than 0, websocket subscriptions will automatically resubscribe after the amount of time (in seconds) specified. The recommended value is 240 seconds (4 minutes), eg `--define:ws_resubscribe=240`.
Contribution Contribution
------------ ------------

View File

@ -5,6 +5,3 @@
when fileExists("nimble.paths"): when fileExists("nimble.paths"):
include "nimble.paths" include "nimble.paths"
# end Nimble config # end Nimble config
when (NimMajor, NimMinor) >= (2, 0):
--mm:refc

View File

@ -1,11 +1,11 @@
import ./ethers/provider import ./ethers/provider
import ./ethers/signer import ./ethers/signer
import ./ethers/providers/jsonrpc import ./ethers/providers/jsonrpc
import ./ethers/contracts import ./ethers/contract
import ./ethers/wallet import ./ethers/wallet
export provider export provider
export signer export signer
export jsonrpc export jsonrpc
export contracts export contract
export wallet export wallet

View File

@ -1,18 +1,18 @@
version = "2.1.0" version = "0.10.1"
author = "Nim Ethers Authors" author = "Nim Ethers Authors"
description = "library for interacting with Ethereum" description = "library for interacting with Ethereum"
license = "MIT" license = "MIT"
requires "nim >= 2.0.14" requires "nim >= 1.6.0"
requires "chronicles >= 0.10.3 & < 0.13.0" requires "chronicles >= 0.10.3 & < 0.11.0"
requires "chronos >= 4.0.4 & < 4.1.0" requires "chronos >= 4.0.0 & < 4.1.0"
requires "contractabi >= 0.7.2 & < 0.8.0" requires "contractabi >= 0.6.0 & < 0.7.0"
requires "questionable >= 0.10.2 & < 0.11.0" requires "questionable >= 0.10.2 & < 0.11.0"
requires "json_rpc >= 0.5.0 & < 0.6.0" requires "https://github.com/codex-storage/nim-json-rpc >= 0.5.0 & < 0.6.0"
requires "serde >= 1.2.1 & < 1.3.0" requires "serde >= 1.2.1 & < 1.3.0"
requires "stint >= 0.8.1 & < 0.9.0" requires "https://github.com/codex-storage/nim-stint-versioned >= 1.0.0 & < 2.0.0"
requires "stew >= 0.2.0" requires "https://github.com/codex-storage/nim-stew-versioned >= 1.0.0 & < 2.0.0"
requires "eth >= 0.6.0 & < 0.10.0" requires "https://github.com/codex-storage/nim-eth-versioned >= 1.0.0 & < 2.0.0"
task test, "Run the test suite": task test, "Run the test suite":
# exec "nimble install -d -y" # exec "nimble install -d -y"

View File

@ -1,5 +1,4 @@
import pkg/stint import pkg/stint
import pkg/questionable
{.push raises:[].} {.push raises:[].}
@ -42,10 +41,3 @@ func `==`*(a, b: BlockTag): bool =
a.stringValue == b.stringValue a.stringValue == b.stringValue
of numberBlockTag: of numberBlockTag:
a.numberValue == b.numberValue a.numberValue == b.numberValue
func number*(blockTag: BlockTag): ?UInt256 =
case blockTag.kind
of stringBlockTag:
UInt256.none
of numberBlockTag:
blockTag.numberValue.some

376
ethers/contract.nim Normal file
View File

@ -0,0 +1,376 @@
import pkg/serde
import std/macros
import std/sequtils
import pkg/chronicles
import pkg/chronos
import pkg/contractabi
import ./basics
import ./provider
import ./signer
import ./events
import ./errors
import ./errors/conversion
import ./fields
export basics
export provider
export events
export errors.SolidityError
export errors.errors
logScope:
topics = "ethers contract"
type
Contract* = ref object of RootObj
provider: Provider
signer: ?Signer
address: Address
TransactionOverrides* = ref object of RootObj
nonce*: ?UInt256
chainId*: ?UInt256
gasPrice*: ?UInt256
maxFee*: ?UInt256
maxPriorityFee*: ?UInt256
gasLimit*: ?UInt256
CallOverrides* = ref object of TransactionOverrides
blockTag*: ?BlockTag
ContractError* = object of EthersError
Confirmable* = object
response*: ?TransactionResponse
convert*: ConvertCustomErrors
EventHandler*[E: Event] = proc(event: E) {.gcsafe, raises:[].}
func new*(ContractType: type Contract,
address: Address,
provider: Provider): ContractType =
ContractType(provider: provider, address: address)
func new*(ContractType: type Contract,
address: Address,
signer: Signer): ContractType =
ContractType(signer: some signer, provider: signer.provider, address: address)
func connect*[T: Contract](contract: T, provider: Provider | Signer): T =
T.new(contract.address, provider)
func provider*(contract: Contract): Provider =
contract.provider
func signer*(contract: Contract): ?Signer =
contract.signer
func address*(contract: Contract): Address =
contract.address
template raiseContractError(message: string) =
raise newException(ContractError, message)
proc createTransaction(contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()): Transaction =
let selector = selector(function, typeof parameters).toArray
let data = @selector & AbiEncoder.encode(parameters)
Transaction(
to: contract.address,
data: data,
nonce: overrides.nonce,
chainId: overrides.chainId,
gasPrice: overrides.gasPrice,
maxFee: overrides.maxFee,
maxPriorityFee: overrides.maxPriorityFee,
gasLimit: overrides.gasLimit,
)
proc decodeResponse(T: type, bytes: seq[byte]): T =
without decoded =? AbiDecoder.decode(bytes, T):
raiseContractError "unable to decode return value as " & $T
return decoded
proc call(provider: Provider,
transaction: Transaction,
overrides: TransactionOverrides): Future[seq[byte]] {.async: (raises: [ProviderError]).} =
if overrides of CallOverrides and
blockTag =? CallOverrides(overrides).blockTag:
await provider.call(transaction, blockTag)
else:
await provider.call(transaction)
proc call(contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()) {.async: (raises: [ProviderError, SignerError]).} =
var transaction = createTransaction(contract, function, parameters, overrides)
if signer =? contract.signer and transaction.sender.isNone:
transaction.sender = some(await signer.getAddress())
discard await contract.provider.call(transaction, overrides)
proc call(contract: Contract,
function: string,
parameters: tuple,
ReturnType: type,
overrides = TransactionOverrides()): Future[ReturnType] {.async: (raises: [ProviderError, SignerError, ContractError]).} =
var transaction = createTransaction(contract, function, parameters, overrides)
if signer =? contract.signer and transaction.sender.isNone:
transaction.sender = some(await signer.getAddress())
let response = await contract.provider.call(transaction, overrides)
return decodeResponse(ReturnType, response)
proc send(
contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()
): Future[?TransactionResponse] {.async: (raises: [AsyncLockError, CancelledError, CatchableError]).} =
if signer =? contract.signer:
withLock(signer):
let transaction = createTransaction(contract, function, parameters, overrides)
let populated = await signer.populateTransaction(transaction)
trace "sending contract transaction", function, params = $parameters
let txResp = await signer.sendTransaction(populated)
return txResp.some
else:
await call(contract, function, parameters, overrides)
return TransactionResponse.none
func getParameterTuple(procedure: NimNode): NimNode =
let parameters = procedure[3]
var tupl = newNimNode(nnkTupleConstr, parameters)
for parameter in parameters[2..^1]:
for name in parameter[0..^3]:
tupl.add name
return tupl
func getErrorTypes(procedure: NimNode): NimNode =
let pragmas = procedure[4]
var tupl = newNimNode(nnkTupleConstr)
for pragma in pragmas:
if pragma.kind == nnkExprColonExpr:
if pragma[0].eqIdent "errors":
pragma[1].expectKind(nnkBracket)
for error in pragma[1]:
tupl.add error
if tupl.len == 0:
quote do: tuple[]
else:
tupl
func isGetter(procedure: NimNode): bool =
let pragmas = procedure[4]
for pragma in pragmas:
if pragma.eqIdent "getter":
return true
false
func isConstant(procedure: NimNode): bool =
let pragmas = procedure[4]
for pragma in pragmas:
if pragma.eqIdent "view":
return true
elif pragma.eqIdent "pure":
return true
elif pragma.eqIdent "getter":
return true
false
func isMultipleReturn(returnType: NimNode): bool =
(returnType.kind == nnkPar and returnType.len > 1) or
(returnType.kind == nnkTupleConstr) or
(returnType.kind == nnkTupleTy)
func addOverrides(procedure: var NimNode) =
procedure[3].add(
newIdentDefs(
ident("overrides"),
newEmptyNode(),
quote do: TransactionOverrides()
)
)
func addContractCall(procedure: var NimNode) =
let contract = procedure[3][1][0]
let function = $basename(procedure[0])
let parameters = getParameterTuple(procedure)
let returnType = procedure[3][0]
let isGetter = procedure.isGetter
procedure.addOverrides()
let errors = getErrorTypes(procedure)
func call: NimNode =
if returnType.kind == nnkEmpty:
quote:
await call(`contract`, `function`, `parameters`, overrides)
elif returnType.isMultipleReturn or isGetter:
quote:
return await call(
`contract`, `function`, `parameters`, `returnType`, overrides
)
else:
quote:
# solidity functions return a tuple, so wrap return type in a tuple
let tupl = await call(
`contract`, `function`, `parameters`, (`returnType`,), overrides
)
return tupl[0]
func send: NimNode =
if returnType.kind == nnkEmpty:
quote:
discard await send(`contract`, `function`, `parameters`, overrides)
else:
quote:
when typeof(result) isnot Confirmable:
{.error:
"unexpected return type, " &
"missing {.view.}, {.pure.} or {.getter.} ?"
.}
let response = await send(`contract`, `function`, `parameters`, overrides)
let convert = customErrorConversion(`errors`)
Confirmable(response: response, convert: convert)
procedure[6] =
if procedure.isConstant:
call()
else:
send()
func addErrorHandling(procedure: var NimNode) =
let body = procedure[6]
let errors = getErrorTypes(procedure)
procedure[6] = quote do:
try:
`body`
except ProviderError as error:
if data =? error.data:
let convert = customErrorConversion(`errors`)
raise convert(error)
else:
raise error
func addFuture(procedure: var NimNode) =
let returntype = procedure[3][0]
if returntype.kind != nnkEmpty:
procedure[3][0] = quote: Future[`returntype`]
func addAsyncPragma(procedure: var NimNode) =
let pragmas = procedure[4]
if pragmas.kind == nnkEmpty:
procedure[4] = newNimNode(nnkPragma)
procedure[4].add ident("async")
macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped =
let parameters = procedure[3]
let body = procedure[6]
parameters.expectMinLen(2) # at least return type and contract instance
body.expectKind(nnkEmpty)
var contractcall = copyNimTree(procedure)
contractcall.addContractCall()
contractcall.addErrorHandling()
contractcall.addFuture()
contractcall.addAsyncPragma()
contractcall
template view* {.pragma.}
template pure* {.pragma.}
template getter* {.pragma.}
proc subscribe*[E: Event](contract: Contract,
_: type E,
handler: EventHandler[E]):
Future[Subscription] =
let topic = topic($E, E.fieldTypes).toArray
let filter = EventFilter(address: contract.address, topics: @[topic])
proc logHandler(log: Log) {.raises: [].} =
if event =? E.decode(log.data, log.topics):
handler(event)
contract.provider.subscribe(filter, logHandler)
proc confirm(tx: Confirmable, confirmations, timeout: int):
Future[TransactionReceipt] {.async.} =
without response =? tx.response:
raise newException(
EthersError,
"Transaction hash required. Possibly was a call instead of a send?"
)
try:
return await response.confirm(confirmations, timeout)
except ProviderError as error:
let convert = tx.convert
raise convert(error)
proc confirm*(tx: Future[Confirmable],
confirmations: int = EthersDefaultConfirmations,
timeout: int = EthersReceiptTimeoutBlks):
Future[TransactionReceipt] {.async.} =
## Convenience method that allows confirm to be chained to a contract
## transaction, eg:
## `await token.connect(signer0)
## .mint(accounts[1], 100.u256)
## .confirm(3)`
return await (await tx).confirm(confirmations, timeout)
proc queryFilter[E: Event](contract: Contract,
_: type E,
filter: EventFilter):
Future[seq[E]] {.async.} =
var logs = await contract.provider.getLogs(filter)
logs.keepItIf(not it.removed)
var events: seq[E] = @[]
for log in logs:
if event =? E.decode(log.data, log.topics):
events.add event
return events
proc queryFilter*[E: Event](contract: Contract,
_: type E):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = EventFilter(address: contract.address,
topics: @[topic])
contract.queryFilter(E, filter)
proc queryFilter*[E: Event](contract: Contract,
_: type E,
blockHash: BlockHash):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = FilterByBlockHash(address: contract.address,
topics: @[topic],
blockHash: blockHash)
contract.queryFilter(E, filter)
proc queryFilter*[E: Event](contract: Contract,
_: type E,
fromBlock: BlockTag,
toBlock: BlockTag):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = Filter(address: contract.address,
topics: @[topic],
fromBlock: fromBlock,
toBlock: toBlock)
contract.queryFilter(E, filter)

View File

@ -1,31 +0,0 @@
import std/macros
import ./contracts/contract
import ./contracts/overrides
import ./contracts/confirmation
import ./contracts/events
import ./contracts/filters
import ./contracts/syntax
import ./contracts/gas
import ./contracts/function
export contract
export overrides
export confirmation
export events
export filters
export syntax.view
export syntax.pure
export syntax.getter
export syntax.errors
export gas.estimateGas
{.push raises: [].}
macro contract*(procedure: untyped{nkProcDef | nkMethodDef}): untyped =
procedure.params.expectMinLen(2) # at least return type and contract instance
procedure.body.expectKind(nnkEmpty)
newStmtList(
createContractFunction(procedure),
createGasEstimationCall(procedure)
)

View File

@ -1,45 +0,0 @@
import ../provider
import ./errors/conversion
{.push raises: [].}
type Confirmable* = object
response*: ?TransactionResponse
convert*: ConvertCustomErrors
proc confirm(tx: Confirmable, confirmations, timeout: int):
Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} =
without response =? tx.response:
raise newException(
EthersError,
"Transaction hash required. Possibly was a call instead of a send?"
)
try:
return await response.confirm(confirmations, timeout)
except ProviderError as error:
let convert = tx.convert
raise convert(error)
proc confirm*(tx: Future[Confirmable],
confirmations: int = EthersDefaultConfirmations,
timeout: int = EthersReceiptTimeoutBlks):
Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} =
## Convenience method that allows confirm to be chained to a contract
## transaction, eg:
## `await token.connect(signer0)
## .mint(accounts[1], 100.u256)
## .confirm(3)`
try:
return await (await tx).confirm(confirmations, timeout)
except CancelledError as e:
raise e
except EthersError as e:
raise e
except CatchableError as e:
raise newException(
EthersError,
"Error when trying to confirm the contract transaction: " & e.msg
)

View File

@ -1,36 +0,0 @@
import ../basics
import ../provider
import ../signer
{.push raises:[].}
type Contract* = ref object of RootObj
provider: Provider
signer: ?Signer
address: Address
func new*(ContractType: type Contract,
address: Address,
provider: Provider): ContractType =
ContractType(provider: provider, address: address)
func new*(ContractType: type Contract,
address: Address,
signer: Signer): ContractType {.raises: [SignerError].} =
ContractType(signer: some signer, provider: signer.provider, address: address)
func connect*[C: Contract](contract: C, provider: Provider): C =
C.new(contract.address, provider)
func connect*[C: Contract](contract: C, signer: Signer): C {.raises: [SignerError].} =
C.new(contract.address, signer)
func provider*(contract: Contract): Provider =
contract.provider
func signer*(contract: Contract): ?Signer =
contract.signer
func address*(contract: Contract): Address =
contract.address

View File

@ -1,35 +0,0 @@
import ../basics
import ./contract
import ./overrides
type ContractCall*[Arguments: tuple] = object
contract: Contract
function: string
arguments: Arguments
overrides: TransactionOverrides
func init*[Arguments: tuple](
_: type ContractCall,
contract: Contract,
function: string,
arguments: Arguments,
overrides: TransactionOverrides
): ContractCall[arguments] =
ContractCall[Arguments](
contract: contract,
function: function,
arguments: arguments,
overrides: overrides
)
func contract*(call: ContractCall): Contract =
call.contract
func function*(call: ContractCall): string =
call.function
func arguments*(call: ContractCall): auto =
call.arguments
func overrides*(call: ContractCall): TransactionOverrides =
call.overrides

View File

@ -1,29 +0,0 @@
import std/macros
import ./errors/conversion
func getErrorTypes*(procedure: NimNode): NimNode =
let pragmas = procedure[4]
var tupl = newNimNode(nnkTupleConstr)
for pragma in pragmas:
if pragma.kind == nnkExprColonExpr:
if pragma[0].eqIdent "errors":
pragma[1].expectKind(nnkBracket)
for error in pragma[1]:
tupl.add error
if tupl.len == 0:
quote do: tuple[]
else:
tupl
func addErrorHandling*(procedure: var NimNode) =
let body = procedure[6]
let errors = getErrorTypes(procedure)
procedure.body = quote do:
try:
`body`
except ProviderError as error:
if data =? error.data:
let convert = customErrorConversion(`errors`)
raise convert(error)
else:
raise error

View File

@ -1,78 +0,0 @@
import std/sequtils
import pkg/contractabi
import ../basics
import ../provider
import ./contract
import ./events
import ./fields
type EventHandler*[E: Event] = proc(event: ?!E) {.gcsafe, raises:[].}
proc subscribe*[E: Event](contract: Contract,
_: type E,
handler: EventHandler[E]):
Future[Subscription] =
let topic = topic($E, E.fieldTypes).toArray
let filter = EventFilter(address: contract.address, topics: @[topic])
proc logHandler(logResult: ?!Log) {.raises: [].} =
without log =? logResult, error:
handler(failure(E, error))
return
if event =? E.decode(log.data, log.topics):
handler(success(event))
contract.provider.subscribe(filter, logHandler)
proc queryFilter[E: Event](contract: Contract,
_: type E,
filter: EventFilter):
Future[seq[E]] {.async.} =
var logs = await contract.provider.getLogs(filter)
logs.keepItIf(not it.removed)
var events: seq[E] = @[]
for log in logs:
if event =? E.decode(log.data, log.topics):
events.add event
return events
proc queryFilter*[E: Event](contract: Contract,
_: type E):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = EventFilter(address: contract.address,
topics: @[topic])
contract.queryFilter(E, filter)
proc queryFilter*[E: Event](contract: Contract,
_: type E,
blockHash: BlockHash):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = FilterByBlockHash(address: contract.address,
topics: @[topic],
blockHash: blockHash)
contract.queryFilter(E, filter)
proc queryFilter*[E: Event](contract: Contract,
_: type E,
fromBlock: BlockTag,
toBlock: BlockTag):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = Filter(address: contract.address,
topics: @[topic],
fromBlock: fromBlock,
toBlock: toBlock)
contract.queryFilter(E, filter)

View File

@ -1,60 +0,0 @@
import std/macros
import ./errors/conversion
import ./syntax
import ./transactions
import ./errors
func addContractCall(procedure: var NimNode) =
let contractCall = getContractCall(procedure)
let returnType = procedure.params[0]
let isGetter = procedure.isGetter
let errors = getErrorTypes(procedure)
func call: NimNode =
if returnType.kind == nnkEmpty:
quote:
await callTransaction(`contractCall`)
elif returnType.isMultipleReturn or isGetter:
quote:
return await callTransaction(`contractCall`, `returnType`)
else:
quote:
# solidity functions return a tuple, so wrap return type in a tuple
let tupl = await callTransaction(`contractCall`, (`returnType`,))
return tupl[0]
func send: NimNode =
if returnType.kind == nnkEmpty:
quote:
discard await sendTransaction(`contractCall`)
else:
quote:
when typeof(result) isnot Confirmable:
{.error:
"unexpected return type, " &
"missing {.view.}, {.pure.} or {.getter.} ?"
.}
let response = await sendTransaction(`contractCall`)
let convert = customErrorConversion(`errors`)
Confirmable(response: response, convert: convert)
procedure.body =
if procedure.isConstant:
call()
else:
send()
func addFuture(procedure: var NimNode) =
let returntype = procedure[3][0]
if returntype.kind != nnkEmpty:
procedure[3][0] = quote: Future[`returntype`]
func createContractFunction*(procedure: NimNode): NimNode =
result = copyNimTree(procedure)
result.addOverridesParameter()
result.addContractCall()
result.addErrorHandling()
result.addFuture()
result.addAsyncPragma()

View File

@ -1,51 +0,0 @@
import std/macros
import ../basics
import ../provider
import ../signer
import ./contract
import ./contractcall
import ./transactions
import ./overrides
import ./errors
import ./syntax
type ContractGasEstimations[C] = distinct C
func estimateGas*[C: Contract](contract: C): ContractGasEstimations[C] =
ContractGasEstimations[C](contract)
proc estimateGas(
call: ContractCall
): Future[UInt256] {.async: (raises: [CancelledError, ProviderError, EthersError]).} =
let transaction = createTransaction(call)
var blockTag = BlockTag.pending
if call.overrides of CallOverrides:
if tag =? CallOverrides(call.overrides).blockTag:
blockTag = tag
if signer =? call.contract.signer:
await signer.estimateGas(transaction, blockTag)
else:
await call.contract.provider.estimateGas(transaction, blockTag)
func wrapFirstParameter(procedure: var NimNode) =
let contractType = procedure.params[1][1]
let gasEstimationsType = quote do: ContractGasEstimations[`contractType`]
procedure.params[1][1] = gasEstimationsType
func setReturnType(procedure: var NimNode) =
procedure.params[0] = quote do: Future[UInt256]
func addEstimateCall(procedure: var NimNode) =
let contractCall = getContractCall(procedure)
procedure.body = quote do:
return await estimateGas(`contractCall`)
func createGasEstimationCall*(procedure: NimNode): NimNode =
result = copyNimTree(procedure)
result.wrapFirstParameter()
result.addOverridesParameter()
result.setReturnType()
result.addAsyncPragma()
result.addUsedPragma()
result.addEstimateCall()
result.addErrorHandling()

View File

@ -1,13 +0,0 @@
import ../basics
import ../blocktag
type
TransactionOverrides* = ref object of RootObj
nonce*: ?UInt256
chainId*: ?UInt256
gasPrice*: ?UInt256
maxFeePerGas*: ?UInt256
maxPriorityFeePerGas*: ?UInt256
gasLimit*: ?UInt256
CallOverrides* = ref object of TransactionOverrides
blockTag*: ?BlockTag

View File

@ -1,76 +0,0 @@
import std/macros
import ./contractcall
template view* {.pragma.}
template pure* {.pragma.}
template getter* {.pragma.}
template errors*(types) {.pragma.}
func isGetter*(procedure: NimNode): bool =
let pragmas = procedure[4]
for pragma in pragmas:
if pragma.eqIdent "getter":
return true
false
func isConstant*(procedure: NimNode): bool =
let pragmas = procedure[4]
for pragma in pragmas:
if pragma.eqIdent "view":
return true
elif pragma.eqIdent "pure":
return true
elif pragma.eqIdent "getter":
return true
false
func isMultipleReturn*(returnType: NimNode): bool =
(returnType.kind == nnkPar and returnType.len > 1) or
(returnType.kind == nnkTupleConstr) or
(returnType.kind == nnkTupleTy)
func getContract(procedure: NimNode): NimNode =
let firstArgument = procedure.params[1][0]
quote do:
Contract(`firstArgument`)
func getFunctionName(procedure: NimNode): string =
$basename(procedure[0])
func getArgumentTuple(procedure: NimNode): NimNode =
let parameters = procedure.params
var arguments = newNimNode(nnkTupleConstr, parameters)
for parameter in parameters[2..^2]:
for name in parameter[0..^3]:
arguments.add name
return arguments
func getOverrides(procedure: NimNode): NimNode =
procedure.params.last[^3]
func getContractCall*(procedure: NimNode): NimNode =
let contract = getContract(procedure)
let function = getFunctionName(procedure)
let arguments = getArgumentTuple(procedure)
let overrides = getOverrides(procedure)
quote do:
ContractCall.init(`contract`, `function`, `arguments`, `overrides`)
func addOverridesParameter*(procedure: var NimNode) =
let overrides = genSym(nskParam, "overrides")
procedure.params.add(
newIdentDefs(
overrides,
newEmptyNode(),
quote do: TransactionOverrides()
)
)
func addAsyncPragma*(procedure: var NimNode) =
procedure.addPragma nnkExprColonExpr.newTree(
quote do: async,
quote do: (raises: [CancelledError, ProviderError, EthersError])
)
func addUsedPragma*(procedure: var NimNode) =
procedure.addPragma(quote do: used)

View File

@ -1,71 +0,0 @@
import pkg/contractabi
import pkg/chronicles
import ../basics
import ../provider
import ../signer
import ../transaction
import ./contract
import ./contractcall
import ./overrides
{.push raises: [].}
logScope:
topics = "ethers contract"
proc createTransaction*(call: ContractCall): Transaction =
let selector = selector(call.function, typeof call.arguments).toArray
let data = @selector & AbiEncoder.encode(call.arguments)
Transaction(
to: call.contract.address,
data: data,
nonce: call.overrides.nonce,
chainId: call.overrides.chainId,
gasPrice: call.overrides.gasPrice,
maxFeePerGas: call.overrides.maxFeePerGas,
maxPriorityFeePerGas: call.overrides.maxPriorityFeePerGas,
gasLimit: call.overrides.gasLimit,
)
proc decodeResponse(T: type, bytes: seq[byte]): T {.raises: [ContractError].} =
without decoded =? AbiDecoder.decode(bytes, T):
raise newException(ContractError, "unable to decode return value as " & $T)
return decoded
proc call(
provider: Provider, transaction: Transaction, overrides: TransactionOverrides
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} =
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]).} =
var transaction = createTransaction(call)
if signer =? call.contract.signer and transaction.sender.isNone:
transaction.sender = some(await signer.getAddress())
discard await call.contract.provider.call(transaction, call.overrides)
proc callTransaction*(call: ContractCall, ReturnType: type): Future[ReturnType] {.async: (raises: [ProviderError, SignerError, ContractError, CancelledError]).} =
var transaction = createTransaction(call)
if signer =? call.contract.signer and transaction.sender.isNone:
transaction.sender = some(await signer.getAddress())
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]).} =
if signer =? call.contract.signer:
withLock(signer):
let transaction = createTransaction(call)
let populated = await signer.populateTransaction(transaction)
trace "sending contract transaction", function = call.function, params = $call.arguments
let txResp = await signer.sendTransaction(populated)
return txResp.some
else:
await callTransaction(call)
return TransactionResponse.none

View File

@ -1,18 +1,7 @@
import ./basics import ./basics
type type SolidityError* = object of EthersError
SolidityError* = object of EthersError
ContractError* = object of EthersError
SignerError* = object of EthersError
SubscriptionError* = object of EthersError
ProviderError* = object of EthersError
data*: ?seq[byte]
{.push raises:[].} {.push raises:[].}
proc toErr*[E1: ref CatchableError, E2: EthersError]( template errors*(types) {.pragma.}
e1: E1,
_: type E2,
msg: string = e1.msg): ref E2 =
return newException(E2, msg, e1)

View File

@ -1,5 +1,5 @@
import ../../basics import ../basics
import ../../provider import ../provider
import ./encoding import ./encoding
type ConvertCustomErrors* = type ConvertCustomErrors* =

View File

@ -1,7 +1,7 @@
import pkg/contractabi import pkg/contractabi
import pkg/contractabi/selector import pkg/contractabi/selector
import ../../basics import ../basics
import ../../errors import ../errors
func selector(E: type): FunctionSelector = func selector(E: type): FunctionSelector =
when compiles(E.arguments): when compiles(E.arguments):

View File

@ -1,7 +1,7 @@
import std/macros import std/macros
import pkg/contractabi import pkg/contractabi
import ../basics import ./basics
import ../provider import ./provider
type type
Event* = object of RootObj Event* = object of RootObj

View File

@ -1,6 +1,5 @@
import pkg/chronicles import pkg/chronicles
import pkg/serde import pkg/serde
import pkg/questionable
import ./basics import ./basics
import ./transaction import ./transaction
import ./blocktag import ./blocktag
@ -9,12 +8,13 @@ import ./errors
export basics export basics
export transaction export transaction
export blocktag export blocktag
export errors
{.push raises: [].} {.push raises: [].}
type type
Provider* = ref object of RootObj Provider* = ref object of RootObj
ProviderError* = object of EthersError
data*: ?seq[byte]
EstimateGasError* = object of ProviderError EstimateGasError* = object of ProviderError
transaction*: Transaction transaction*: Transaction
Subscription* = ref object of RootObj Subscription* = ref object of RootObj
@ -56,14 +56,13 @@ type
effectiveGasPrice*: ?UInt256 effectiveGasPrice*: ?UInt256
status*: TransactionStatus status*: TransactionStatus
transactionType* {.serialize("type"), deserialize("type").}: TransactionType transactionType* {.serialize("type"), deserialize("type").}: TransactionType
LogHandler* = proc(log: ?!Log) {.gcsafe, raises:[].} LogHandler* = proc(log: Log) {.gcsafe, raises:[].}
BlockHandler* = proc(blck: ?!Block) {.gcsafe, raises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].}
Topic* = array[32, byte] Topic* = array[32, byte]
Block* {.serialize.} = object Block* {.serialize.} = object
number*: ?UInt256 number*: ?UInt256
timestamp*: UInt256 timestamp*: UInt256
hash*: ?BlockHash hash*: ?BlockHash
baseFeePerGas* : ?UInt256
PastTransaction* {.serialize.} = object PastTransaction* {.serialize.} = object
blockHash*: BlockHash blockHash*: BlockHash
blockNumber*: UInt256 blockNumber*: UInt256
@ -103,90 +102,96 @@ func toTransaction*(past: PastTransaction): Transaction =
) )
method getBlockNumber*( method getBlockNumber*(
provider: Provider provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getBlock*( method getBlock*(
provider: Provider, tag: BlockTag provider: Provider,
): Future[?Block] {.base, async: (raises: [ProviderError, CancelledError]).} = tag: BlockTag): Future[?Block] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method call*( method call*(
provider: Provider, tx: Transaction, blockTag = BlockTag.latest provider: Provider,
): Future[seq[byte]] {.base, async: (raises: [ProviderError, CancelledError]).} = tx: Transaction,
blockTag = BlockTag.latest): Future[seq[byte]] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getGasPrice*( method getGasPrice*(
provider: Provider provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented"
method getMaxPriorityFeePerGas*(
provider: Provider
): Future[UInt256] {.base, async: (raises: [CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransactionCount*( method getTransactionCount*(
provider: Provider, address: Address, blockTag = BlockTag.latest provider: Provider,
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = address: Address,
blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransaction*( method getTransaction*(
provider: Provider, txHash: TransactionHash provider: Provider,
): Future[?PastTransaction] {.base, async: (raises: [ProviderError, CancelledError]).} = txHash: TransactionHash): Future[?PastTransaction] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransactionReceipt*( method getTransactionReceipt*(
provider: Provider, txHash: TransactionHash provider: Provider,
): Future[?TransactionReceipt] {. txHash: TransactionHash): Future[?TransactionReceipt] {.base, async: (raises:[ProviderError]).} =
base, async: (raises: [ProviderError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*( method sendTransaction*(
provider: Provider, rawTransaction: seq[byte] provider: Provider,
): Future[TransactionResponse] {. rawTransaction: seq[byte]): Future[TransactionResponse] {.base, async: (raises:[ProviderError]).} =
base, async: (raises: [ProviderError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getLogs*( method getLogs*(
provider: Provider, filter: EventFilter provider: Provider,
): Future[seq[Log]] {.base, async: (raises: [ProviderError, CancelledError]).} = filter: EventFilter): Future[seq[Log]] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method estimateGas*( method estimateGas*(
provider: Provider, transaction: Transaction, blockTag = BlockTag.latest provider: Provider,
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getChainId*( method getChainId*(
provider: Provider provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method subscribe*( method subscribe*(
provider: Provider, filter: EventFilter, callback: LogHandler provider: Provider,
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} = filter: EventFilter,
callback: LogHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method subscribe*( method subscribe*(
provider: Provider, callback: BlockHandler provider: Provider,
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} = callback: BlockHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method unsubscribe*( method unsubscribe*(
subscription: Subscription subscription: Subscription) {.base, async: (raises:[ProviderError]).} =
) {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method isSyncing*(provider: Provider): Future[bool] {.base, async: (raises: [ProviderError, CancelledError]).} = method isSyncing*(provider: Provider): Future[bool] {.base, async.} =
doAssert false, "not implemented" doAssert false, "not implemented"
proc replay*( proc replay*(
provider: Provider, tx: Transaction, blockNumber: UInt256 provider: Provider,
) {.async: (raises: [ProviderError, CancelledError]).} = tx: Transaction,
blockNumber: UInt256) {.async: (raises:[ProviderError]).} =
# Replay transaction at block. Useful for fetching revert reasons, which will # Replay transaction at block. Useful for fetching revert reasons, which will
# be present in the raised error message. The replayed block number should # be present in the raised error message. The replayed block number should
# include the state of the chain in the block previous to the block in which # include the state of the chain in the block previous to the block in which
@ -198,8 +203,8 @@ proc replay*(
discard await provider.call(tx, BlockTag.init(blockNumber)) discard await provider.call(tx, BlockTag.init(blockNumber))
proc ensureSuccess( proc ensureSuccess(
provider: Provider, receipt: TransactionReceipt provider: Provider,
) {.async: (raises: [ProviderError, CancelledError]).} = receipt: TransactionReceipt) {.async: (raises: [ProviderError]).} =
## If the receipt.status is Failed, the tx is replayed to obtain a revert ## If the receipt.status is Failed, the tx is replayed to obtain a revert
## reason, after which a ProviderError with the revert reason is raised. ## reason, after which a ProviderError with the revert reason is raised.
## If no revert reason was obtained ## If no revert reason was obtained
@ -222,7 +227,7 @@ proc confirm*(
tx: TransactionResponse, tx: TransactionResponse,
confirmations = EthersDefaultConfirmations, confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt] timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt]
{.async: (raises: [CancelledError, ProviderError, SubscriptionError, EthersError]).} = {.async: (raises: [CancelledError, ProviderError, EthersError]).} =
## Waits for a transaction to be mined and for the specified number of blocks ## Waits for a transaction to be mined and for the specified number of blocks
## to pass since it was mined (confirmations). The number of confirmations ## to pass since it was mined (confirmations). The number of confirmations
@ -233,12 +238,6 @@ proc confirm*(
assert confirmations > 0 assert confirmations > 0
var blockNumber: UInt256 var blockNumber: UInt256
## We need initialized succesfull Result, because the first iteration of the `while` loop
## bellow is triggered "manually" by calling `await updateBlockNumber` and not by block
## subscription. If left uninitialized then the Result is in error state and error is raised.
## This result is not used for block value, but for block subscription errors.
var blockSubscriptionResult: ?!Block = success(Block(number: UInt256.none, timestamp: 0.u256, hash: BlockHash.none))
let blockEvent = newAsyncEvent() let blockEvent = newAsyncEvent()
proc updateBlockNumber {.async: (raises: []).} = proc updateBlockNumber {.async: (raises: []).} =
@ -247,17 +246,11 @@ proc confirm*(
if number > blockNumber: if number > blockNumber:
blockNumber = number blockNumber = number
blockEvent.fire() blockEvent.fire()
except ProviderError, CancelledError: except ProviderError:
# there's nothing we can do here # there's nothing we can do here
discard discard
proc onBlock(blckResult: ?!Block) = proc onBlock(_: Block) =
blockSubscriptionResult = blckResult
if blckResult.isErr:
blockEvent.fire()
return
# ignore block parameter; hardhat may call this with pending blocks # ignore block parameter; hardhat may call this with pending blocks
asyncSpawn updateBlockNumber() asyncSpawn updateBlockNumber()
@ -271,16 +264,6 @@ proc confirm*(
await blockEvent.wait() await blockEvent.wait()
blockEvent.clear() blockEvent.clear()
if blockSubscriptionResult.isErr:
let error = blockSubscriptionResult.error()
if error of SubscriptionError:
raise (ref SubscriptionError)(error)
elif error of CancelledError:
raise (ref CancelledError)(error)
else:
raise error.toErr(ProviderError)
if blockNumber >= finish: if blockNumber >= finish:
await subscription.unsubscribe() await subscription.unsubscribe()
raise newException(EthersError, "tx not mined before timeout") raise newException(EthersError, "tx not mined before timeout")
@ -299,24 +282,13 @@ proc confirm*(
proc confirm*( proc confirm*(
tx: Future[TransactionResponse], tx: Future[TransactionResponse],
confirmations: int = EthersDefaultConfirmations, confirmations: int = EthersDefaultConfirmations,
timeout: int = EthersReceiptTimeoutBlks): Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = timeout: int = EthersReceiptTimeoutBlks): Future[TransactionReceipt] {.async.} =
## Convenience method that allows wait to be chained to a sendTransaction ## Convenience method that allows wait to be chained to a sendTransaction
## call, eg: ## call, eg:
## `await signer.sendTransaction(populated).confirm(3)` ## `await signer.sendTransaction(populated).confirm(3)`
try:
let txResp = await tx
return await txResp.confirm(confirmations, timeout)
except CancelledError as e:
raise e
except EthersError as e:
raise e
except CatchableError as e:
raise newException(
EthersError,
"Error when trying to confirm the provider transaction: " & e.msg
)
method close*( let txResp = await tx
provider: Provider return await txResp.confirm(confirmations, timeout)
) {.base, async: (raises: [ProviderError, CancelledError]).} =
method close*(provider: Provider) {.base, async: (raises:[ProviderError]).} =
discard discard

View File

@ -1,7 +1,7 @@
import std/tables import std/tables
import std/uri import std/uri
import pkg/chronicles import pkg/chronicles
import pkg/eth/common/eth_types except Block, Log, Address, Transaction import pkg/eth/common/eth_types_json_serialization
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/json_rpc/errors import pkg/json_rpc/errors
import pkg/serde import pkg/serde
@ -28,7 +28,6 @@ type
JsonRpcProvider* = ref object of Provider JsonRpcProvider* = ref object of Provider
client: Future[RpcClient] client: Future[RpcClient]
subscriptions: Future[JsonRpcSubscriptions] subscriptions: Future[JsonRpcSubscriptions]
maxPriorityFeePerGas: UInt256
JsonRpcSubscription* = ref object of Subscription JsonRpcSubscription* = ref object of Subscription
subscriptions: JsonRpcSubscriptions subscriptions: JsonRpcSubscriptions
@ -44,7 +43,6 @@ type
const defaultUrl = "http://localhost:8545" const defaultUrl = "http://localhost:8545"
const defaultPollingInterval = 4.seconds const defaultPollingInterval = 4.seconds
const defaultMaxPriorityFeePerGas = 1_000_000_000.u256
proc jsonHeaders: seq[(string, string)] = proc jsonHeaders: seq[(string, string)] =
@[("Content-Type", "application/json")] @[("Content-Type", "application/json")]
@ -52,14 +50,13 @@ proc jsonHeaders: seq[(string, string)] =
proc new*( proc new*(
_: type JsonRpcProvider, _: type JsonRpcProvider,
url=defaultUrl, url=defaultUrl,
pollingInterval=defaultPollingInterval, pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} =
maxPriorityFeePerGas=defaultMaxPriorityFeePerGas): JsonRpcProvider {.raises: [JsonRpcProviderError].} =
var initialized: Future[void] var initialized: Future[void]
var client: RpcClient var client: RpcClient
var subscriptions: JsonRpcSubscriptions var subscriptions: JsonRpcSubscriptions
proc initialize() {.async: (raises: [JsonRpcProviderError, CancelledError]).} = proc initialize {.async: (raises:[JsonRpcProviderError]).} =
convertError: convertError:
case parseUri(url).scheme case parseUri(url).scheme
of "ws", "wss": of "ws", "wss":
@ -75,47 +72,43 @@ proc new*(
pollingInterval = pollingInterval) pollingInterval = pollingInterval)
subscriptions.start() subscriptions.start()
proc awaitClient(): Future[RpcClient] {. proc awaitClient: Future[RpcClient] {.async:(raises:[JsonRpcProviderError]).} =
async: (raises: [JsonRpcProviderError, CancelledError])
.} =
convertError: convertError:
await initialized await initialized
return client return client
proc awaitSubscriptions(): Future[JsonRpcSubscriptions] {. proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async:(raises:[JsonRpcProviderError]).} =
async: (raises: [JsonRpcProviderError, CancelledError])
.} =
convertError: convertError:
await initialized await initialized
return subscriptions return subscriptions
initialized = initialize() initialized = initialize()
return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions(), maxPriorityFeePerGas: maxPriorityFeePerGas) return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions())
proc callImpl( proc callImpl(
client: RpcClient, call: string, args: JsonNode client: RpcClient,
): Future[JsonNode] {.async: (raises: [JsonRpcProviderError, CancelledError]).} = call: string,
try: args: JsonNode): Future[JsonNode] {.async: (raises: [JsonRpcProviderError]).} =
let response = await client.call(call, %args)
without json =? JsonNode.fromJson(response.string), error: without response =? (await client.call(call, %args)).catch, error:
raiseJsonRpcProviderError "Failed to parse response '" & response.string & "': " &
error.msg
return json
except CancelledError as error:
raise error
except CatchableError as error:
raiseJsonRpcProviderError error.msg raiseJsonRpcProviderError error.msg
without json =? JsonNode.fromJson(response.string), error:
raiseJsonRpcProviderError "Failed to parse response: " & error.msg
json
proc send*( proc send*(
provider: JsonRpcProvider, call: string, arguments: seq[JsonNode] = @[] provider: JsonRpcProvider,
): Future[JsonNode] {.async: (raises: [ProviderError, CancelledError]).} = call: string,
arguments: seq[JsonNode] = @[]): Future[JsonNode]
{.async: (raises: [JsonRpcProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.callImpl(call, %arguments) return await client.callImpl(call, %arguments)
proc listAccounts*( proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]]
provider: JsonRpcProvider {.async: (raises: [JsonRpcProviderError]).} =
): Future[seq[Address]] {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_accounts() return await client.eth_accounts()
@ -127,78 +120,73 @@ proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
JsonRpcSigner(provider: provider, address: some address) JsonRpcSigner(provider: provider, address: some address)
method getBlockNumber*( method getBlockNumber*(
provider: JsonRpcProvider provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_blockNumber() return await client.eth_blockNumber()
method getBlock*( method getBlock*(
provider: JsonRpcProvider, tag: BlockTag provider: JsonRpcProvider,
): Future[?Block] {.async: (raises: [ProviderError, CancelledError]).} = tag: BlockTag): Future[?Block] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getBlockByNumber(tag, false) return await client.eth_getBlockByNumber(tag, false)
method call*( method call*(
provider: JsonRpcProvider, tx: Transaction, blockTag = BlockTag.latest provider: JsonRpcProvider,
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} = tx: Transaction,
blockTag = BlockTag.latest): Future[seq[byte]] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_call(tx, blockTag) return await client.eth_call(tx, blockTag)
method getGasPrice*( method getGasPrice*(
provider: JsonRpcProvider provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_gasPrice() return await client.eth_gasPrice()
method getMaxPriorityFeePerGas*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [CancelledError]).} =
try:
convertError:
let client = await provider.client
return await client.eth_maxPriorityFeePerGas()
except JsonRpcProviderError:
# If the provider does not provide the implementation
# let's just remove the manual value
return provider.maxPriorityFeePerGas
method getTransactionCount*( method getTransactionCount*(
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest provider: JsonRpcProvider,
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} = address: Address,
blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag) return await client.eth_getTransactionCount(address, blockTag)
method getTransaction*( method getTransaction*(
provider: JsonRpcProvider, txHash: TransactionHash provider: JsonRpcProvider,
): Future[?PastTransaction] {.async: (raises: [ProviderError, CancelledError]).} = txHash: TransactionHash): Future[?PastTransaction] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionByHash(txHash) return await client.eth_getTransactionByHash(txHash)
method getTransactionReceipt*( method getTransactionReceipt*(
provider: JsonRpcProvider, txHash: TransactionHash provider: JsonRpcProvider,
): Future[?TransactionReceipt] {.async: (raises: [ProviderError, CancelledError]).} = txHash: TransactionHash): Future[?TransactionReceipt] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionReceipt(txHash) return await client.eth_getTransactionReceipt(txHash)
method getLogs*( method getLogs*(
provider: JsonRpcProvider, filter: EventFilter provider: JsonRpcProvider,
): Future[seq[Log]] {.async: (raises: [ProviderError, CancelledError]).} = filter: EventFilter): Future[seq[Log]] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
let logsJson = let logsJson = if filter of Filter:
if filter of Filter: await client.eth_getLogs(Filter(filter))
await client.eth_getLogs(Filter(filter)) elif filter of FilterByBlockHash:
elif filter of FilterByBlockHash: await client.eth_getLogs(FilterByBlockHash(filter))
await client.eth_getLogs(FilterByBlockHash(filter)) else:
else: await client.eth_getLogs(filter)
await client.eth_getLogs(filter)
var logs: seq[Log] = @[] var logs: seq[Log] = @[]
for logJson in logsJson.getElems: for logJson in logsJson.getElems:
@ -208,10 +196,10 @@ method getLogs*(
return logs return logs
method estimateGas*( method estimateGas*(
provider: JsonRpcProvider, provider: JsonRpcProvider,
transaction: Transaction, transaction: Transaction,
blockTag = BlockTag.latest, blockTag = BlockTag.latest): Future[UInt256] {.async: (raises:[ProviderError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
try: try:
convertError: convertError:
let client = await provider.client let client = await provider.client
@ -221,24 +209,24 @@ method estimateGas*(
msg: "Estimate gas failed: " & error.msg, msg: "Estimate gas failed: " & error.msg,
data: error.data, data: error.data,
transaction: transaction, transaction: transaction,
parent: error, parent: error
) )
method getChainId*( method getChainId*(
provider: JsonRpcProvider provider: JsonRpcProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
try: try:
return await client.eth_chainId() return await client.eth_chainId()
except CancelledError as error:
raise error
except CatchableError: except CatchableError:
return parse(await client.net_version(), UInt256) return parse(await client.net_version(), UInt256)
method sendTransaction*( method sendTransaction*(
provider: JsonRpcProvider, rawTransaction: seq[byte] provider: JsonRpcProvider,
): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError]).} = rawTransaction: seq[byte]): Future[TransactionResponse]
{.async: (raises:[ProviderError]).} =
convertError: convertError:
let let
client = await provider.client client = await provider.client
@ -247,40 +235,41 @@ method sendTransaction*(
return TransactionResponse(hash: hash, provider: provider) return TransactionResponse(hash: hash, provider: provider)
method subscribe*( method subscribe*(
provider: JsonRpcProvider, filter: EventFilter, onLog: LogHandler provider: JsonRpcProvider,
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} = filter: EventFilter,
onLog: LogHandler): Future[Subscription] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let subscriptions = await provider.subscriptions let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeLogs(filter, onLog) let id = await subscriptions.subscribeLogs(filter, onLog)
return JsonRpcSubscription(subscriptions: subscriptions, id: id) return JsonRpcSubscription(subscriptions: subscriptions, id: id)
method subscribe*( method subscribe*(
provider: JsonRpcProvider, onBlock: BlockHandler provider: JsonRpcProvider,
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} = onBlock: BlockHandler): Future[Subscription] {.async: (raises:[ProviderError]).} =
convertError: convertError:
let subscriptions = await provider.subscriptions let subscriptions = await provider.subscriptions
let id = await subscriptions.subscribeBlocks(onBlock) let id = await subscriptions.subscribeBlocks(onBlock)
return JsonRpcSubscription(subscriptions: subscriptions, id: id) return JsonRpcSubscription(subscriptions: subscriptions, id: id)
method unsubscribe*( method unsubscribe*(
subscription: JsonRpcSubscription subscription: JsonRpcSubscription) {.async: (raises:[ProviderError]).} =
) {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let subscriptions = subscription.subscriptions let subscriptions = subscription.subscriptions
let id = subscription.id let id = subscription.id
await subscriptions.unsubscribe(id) await subscriptions.unsubscribe(id)
method isSyncing*( method isSyncing*(provider: JsonRpcProvider): Future[bool] {.async.} =
provider: JsonRpcProvider
): Future[bool] {.async: (raises: [ProviderError, CancelledError]).} =
let response = await provider.send("eth_syncing") let response = await provider.send("eth_syncing")
if response.kind == JsonNodeKind.JObject: if response.kind == JsonNodeKind.JObject:
return true return true
return response.getBool() return response.getBool()
method close*( method close*(
provider: JsonRpcProvider provider: JsonRpcProvider) {.async: (raises:[ProviderError]).} =
) {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
let subscriptions = await provider.subscriptions let subscriptions = await provider.subscriptions
@ -301,8 +290,6 @@ proc raiseJsonRpcSignerError(
template convertSignerError(body) = template convertSignerError(body) =
try: try:
body body
except CancelledError as error:
raise error
except JsonRpcError as error: except JsonRpcError as error:
raiseJsonRpcSignerError(error.msg) raiseJsonRpcSignerError(error.msg)
except CatchableError as error: except CatchableError as error:
@ -314,8 +301,9 @@ method provider*(signer: JsonRpcSigner): Provider
signer.provider signer.provider
method getAddress*( method getAddress*(
signer: JsonRpcSigner signer: JsonRpcSigner): Future[Address]
): Future[Address] {.async: (raises: [ProviderError, SignerError, CancelledError]).} = {.async: (raises:[ProviderError, SignerError]).} =
if address =? signer.address: if address =? signer.address:
return address return address
@ -326,18 +314,19 @@ method getAddress*(
raiseJsonRpcSignerError "no address found" raiseJsonRpcSignerError "no address found"
method signMessage*( method signMessage*(
signer: JsonRpcSigner, message: seq[byte] signer: JsonRpcSigner,
): Future[seq[byte]] {.async: (raises: [SignerError, CancelledError]).} = message: seq[byte]): Future[seq[byte]] {.async: (raises:[SignerError]).} =
convertSignerError: convertSignerError:
let client = await signer.provider.client let client = await signer.provider.client
let address = await signer.getAddress() let address = await signer.getAddress()
return await client.personal_sign(message, address) return await client.personal_sign(message, address)
method sendTransaction*( method sendTransaction*(
signer: JsonRpcSigner, transaction: Transaction signer: JsonRpcSigner,
): Future[TransactionResponse] {. transaction: Transaction): Future[TransactionResponse]
async: (raises: [SignerError, ProviderError, CancelledError]) {.async: (raises:[SignerError, ProviderError]).} =
.} =
convertError: convertError:
let let
client = await signer.provider.client client = await signer.provider.client

View File

@ -1,12 +1,9 @@
import std/strutils import std/strutils
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../basics import ../../basics
import ../../errors
import ../../provider import ../../provider
import ./conversions import ./conversions
export errors
{.push raises:[].} {.push raises:[].}
type JsonRpcProviderError* = object of ProviderError type JsonRpcProviderError* = object of ProviderError
@ -40,8 +37,6 @@ proc raiseJsonRpcProviderError*(
template convertError*(body) = template convertError*(body) =
try: try:
body body
except CancelledError as error:
raise error
except JsonRpcError as error: except JsonRpcError as error:
raiseJsonRpcProviderError(error.msg) raiseJsonRpcProviderError(error.msg)
except CatchableError as error: except CatchableError as error:

View File

@ -21,4 +21,3 @@ proc eth_newBlockFilter(): JsonNode
proc eth_newFilter(filter: EventFilter): JsonNode proc eth_newFilter(filter: EventFilter): JsonNode
proc eth_getFilterChanges(id: JsonNode): JsonNode proc eth_getFilterChanges(id: JsonNode): JsonNode
proc eth_uninstallFilter(id: JsonNode): bool proc eth_uninstallFilter(id: JsonNode): bool
proc eth_maxPriorityFeePerGas(): UInt256

View File

@ -1,12 +1,9 @@
import std/tables import std/tables
import std/sequtils import std/sequtils
import std/strutils
import pkg/chronos import pkg/chronos
import pkg/questionable
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/serde import pkg/serde
import ../../basics import ../../basics
import ../../errors
import ../../provider import ../../provider
include ../../nimshims/hashes include ../../nimshims/hashes
import ./rpccalls import ./rpccalls
@ -19,25 +16,12 @@ type
client: RpcClient client: RpcClient
callbacks: Table[JsonNode, SubscriptionCallback] callbacks: Table[JsonNode, SubscriptionCallback]
methodHandlers: Table[string, MethodHandler] methodHandlers: Table[string, MethodHandler]
# Used by both PollingSubscriptions and WebsocketSubscriptions to store
# subscription filters so the subscriptions can be recreated. With
# PollingSubscriptions, the RPC node might prune/forget about them, and with
# WebsocketSubscriptions, when using hardhat, subscriptions are dropped after 5
# minutes.
logFilters: Table[JsonNode, EventFilter]
MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].} MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].}
SubscriptionCallback = proc(id: JsonNode, arguments: ?!JsonNode) {.gcsafe, raises:[].} SubscriptionCallback = proc(id, arguments: JsonNode) {.gcsafe, raises:[].}
SubscriptionError* = object of EthersError
{.push raises:[].} {.push raises:[].}
template convertErrorsToSubscriptionError(body) =
try:
body
except CancelledError as error:
raise error
except CatchableError as error:
raise error.toErr(SubscriptionError)
template `or`(a: JsonNode, b: typed): JsonNode = template `or`(a: JsonNode, b: typed): JsonNode =
if a.isNil: b else: a if a.isNil: b else: a
@ -59,6 +43,7 @@ func start*(subscriptions: JsonRpcSubscriptions) =
# true = continue processing message using json_rpc's default message handler # true = continue processing message using json_rpc's default message handler
return ok true return ok true
proc setMethodHandler( proc setMethodHandler(
subscriptions: JsonRpcSubscriptions, subscriptions: JsonRpcSubscriptions,
`method`: string, `method`: string,
@ -69,157 +54,80 @@ proc setMethodHandler(
method subscribeBlocks*(subscriptions: JsonRpcSubscriptions, method subscribeBlocks*(subscriptions: JsonRpcSubscriptions,
onBlock: BlockHandler): onBlock: BlockHandler):
Future[JsonNode] Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]), base,.} = {.async, base.} =
raiseAssert "not implemented" raiseAssert "not implemented"
method subscribeLogs*(subscriptions: JsonRpcSubscriptions, method subscribeLogs*(subscriptions: JsonRpcSubscriptions,
filter: EventFilter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[JsonNode] Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]), base.} = {.async, base.} =
raiseAssert "not implemented" raiseAssert "not implemented"
method unsubscribe*(subscriptions: JsonRpcSubscriptions, method unsubscribe*(subscriptions: JsonRpcSubscriptions,
id: JsonNode) id: JsonNode)
{.async: (raises: [CancelledError]), base.} = {.async, base.} =
raiseAssert "not implemented " raiseAssert "not implemented"
method close*(subscriptions: JsonRpcSubscriptions) {.async: (raises: []), base.} = method close*(subscriptions: JsonRpcSubscriptions) {.async, base.} =
let ids = toSeq subscriptions.callbacks.keys let ids = toSeq subscriptions.callbacks.keys
for id in ids: for id in ids:
try: await subscriptions.unsubscribe(id)
await subscriptions.unsubscribe(id)
except CatchableError as e:
error "JsonRpc unsubscription failed", error = e.msg, id = id
proc getCallback(subscriptions: JsonRpcSubscriptions, proc getCallback(subscriptions: JsonRpcSubscriptions,
id: JsonNode): ?SubscriptionCallback {. raises:[].} = id: JsonNode): ?SubscriptionCallback =
try: try:
if not id.isNil and id in subscriptions.callbacks: if not id.isNil and id in subscriptions.callbacks:
return subscriptions.callbacks[id].some subscriptions.callbacks[id].some
except: discard else:
SubscriptionCallback.none
except KeyError:
SubscriptionCallback.none
# Web sockets # Web sockets
# Default re-subscription period is seconds
const WsResubscribe {.intdefine.}: int = 0
type type
WebSocketSubscriptions = ref object of JsonRpcSubscriptions WebSocketSubscriptions = ref object of JsonRpcSubscriptions
logFiltersLock: AsyncLock
resubscribeFut: Future[void]
resubscribeInterval: int
template withLock*(subscriptions: WebSocketSubscriptions, body: untyped) =
if subscriptions.logFiltersLock.isNil:
subscriptions.logFiltersLock = newAsyncLock()
await subscriptions.logFiltersLock.acquire()
try:
body
finally:
subscriptions.logFiltersLock.release()
# This is a workaround to manage the 5 minutes limit due to hardhat.
# See https://github.com/NomicFoundation/hardhat/issues/2053#issuecomment-1061374064
proc resubscribeWebsocketEventsOnTimeout*(subscriptions: WebsocketSubscriptions) {.async: (raises: [CancelledError]).} =
while true:
await sleepAsync(subscriptions.resubscribeInterval.seconds)
try:
withLock(subscriptions):
for id, callback in subscriptions.callbacks:
var newId: JsonNode
if id in subscriptions.logFilters:
let filter = subscriptions.logFilters[id]
newId = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.logFilters[newId] = filter
subscriptions.logFilters.del(id)
else:
newId = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[newId] = callback
subscriptions.callbacks.del(id)
discard await subscriptions.client.eth_unsubscribe(id)
except CancelledError as e:
raise e
except CatchableError as e:
error "WS resubscription failed" , error = e.msg
proc new*(_: type JsonRpcSubscriptions, proc new*(_: type JsonRpcSubscriptions,
client: RpcWebSocketClient, client: RpcWebSocketClient): JsonRpcSubscriptions =
resubscribeInterval = WsResubscribe): JsonRpcSubscriptions =
let subscriptions = WebSocketSubscriptions(client: client, resubscribeInterval: resubscribeInterval)
let subscriptions = WebSocketSubscriptions(client: client)
proc subscriptionHandler(arguments: JsonNode) {.raises:[].} = proc subscriptionHandler(arguments: JsonNode) {.raises:[].} =
let id = arguments{"subscription"} or newJString("") let id = arguments{"subscription"} or newJString("")
if callback =? subscriptions.getCallback(id): if callback =? subscriptions.getCallback(id):
callback(id, success(arguments)) callback(id, arguments)
subscriptions.setMethodHandler("eth_subscription", subscriptionHandler) subscriptions.setMethodHandler("eth_subscription", subscriptionHandler)
if resubscribeInterval > 0:
if resubscribeInterval >= 300:
warn "Resubscription interval greater than 300 seconds is useless for hardhat workaround", resubscribeInterval = resubscribeInterval
subscriptions.resubscribeFut = resubscribeWebsocketEventsOnTimeout(subscriptions)
subscriptions subscriptions
method subscribeBlocks(subscriptions: WebSocketSubscriptions, method subscribeBlocks(subscriptions: WebSocketSubscriptions,
onBlock: BlockHandler): onBlock: BlockHandler):
Future[JsonNode] Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} = {.async.} =
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) {.raises: [].} = proc callback(id, arguments: JsonNode) {.raises: [].} =
without arguments =? argumentsResult, error: if blck =? Block.fromJson(arguments{"result"}):
onBlock(failure(Block, error.toErr(SubscriptionError))) onBlock(blck)
return let id = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[id] = callback
let res = Block.fromJson(arguments{"result"}).mapFailure(SubscriptionError) return id
onBlock(res)
convertErrorsToSubscriptionError:
withLock(subscriptions):
let id = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[id] = callback
return id
method subscribeLogs(subscriptions: WebSocketSubscriptions, method subscribeLogs(subscriptions: WebSocketSubscriptions,
filter: EventFilter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[JsonNode] Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} = {.async.} =
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) = proc callback(id, arguments: JsonNode) =
without arguments =? argumentsResult, error: if log =? Log.fromJson(arguments{"result"}):
onLog(failure(Log, error.toErr(SubscriptionError))) onLog(log)
return let id = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.callbacks[id] = callback
let res = Log.fromJson(arguments{"result"}).mapFailure(SubscriptionError) return id
onLog(res)
convertErrorsToSubscriptionError:
withLock(subscriptions):
let id = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
return id
method unsubscribe*(subscriptions: WebSocketSubscriptions, method unsubscribe*(subscriptions: WebSocketSubscriptions,
id: JsonNode) id: JsonNode)
{.async: (raises: [CancelledError]).} = {.async.} =
try: subscriptions.callbacks.del(id)
withLock(subscriptions): discard await subscriptions.client.eth_unsubscribe(id)
subscriptions.callbacks.del(id)
discard await subscriptions.client.eth_unsubscribe(id)
except CancelledError as e:
raise e
except CatchableError:
# Ignore if uninstallation of the subscribiton fails.
discard
method close*(subscriptions: WebSocketSubscriptions) {.async: (raises: []).} =
await procCall JsonRpcSubscriptions(subscriptions).close()
if not subscriptions.resubscribeFut.isNil:
await subscriptions.resubscribeFut.cancelAndWait()
# Polling # Polling
@ -227,6 +135,11 @@ type
PollingSubscriptions* = ref object of JsonRpcSubscriptions PollingSubscriptions* = ref object of JsonRpcSubscriptions
polling: Future[void] polling: Future[void]
# We need to keep around the filters that are used to create log filters on the RPC node
# as there might be a time when they need to be recreated as RPC node might prune/forget
# about them
logFilters: Table[JsonNode, EventFilter]
# Used when filters are recreated to translate from the id that user # Used when filters are recreated to translate from the id that user
# originally got returned to new filter id # originally got returned to new filter id
subscriptionMapping: Table[JsonNode, JsonNode] subscriptionMapping: Table[JsonNode, JsonNode]
@ -237,7 +150,7 @@ proc new*(_: type JsonRpcSubscriptions,
let subscriptions = PollingSubscriptions(client: client) let subscriptions = PollingSubscriptions(client: client)
proc resubscribe(id: JsonNode): Future[?!void] {.async: (raises: [CancelledError]).} = proc resubscribe(id: JsonNode) {.async: (raises: [CancelledError]).} =
try: try:
var newId: JsonNode var newId: JsonNode
# Log filters are stored in logFilters, block filters are not persisted # Log filters are stored in logFilters, block filters are not persisted
@ -249,49 +162,36 @@ proc new*(_: type JsonRpcSubscriptions,
else: else:
newId = await subscriptions.client.eth_newBlockFilter() newId = await subscriptions.client.eth_newBlockFilter()
subscriptions.subscriptionMapping[id] = newId subscriptions.subscriptionMapping[id] = newId
except CancelledError as e: except CancelledError as error:
raise e raise error
except CatchableError as e: except CatchableError:
return failure(void, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg)) # there's nothing further we can do here
discard
return success() proc getChanges(id: JsonNode): Future[JsonNode] {.async: (raises: [CancelledError]).} =
proc getChanges(id: JsonNode): Future[?!JsonNode] {.async: (raises: [CancelledError]).} =
if mappedId =? subscriptions.subscriptionMapping.?[id]: if mappedId =? subscriptions.subscriptionMapping.?[id]:
try: try:
let changes = await subscriptions.client.eth_getFilterChanges(mappedId) let changes = await subscriptions.client.eth_getFilterChanges(mappedId)
if changes.kind == JArray: if changes.kind == JArray:
return success(changes) return changes
except JsonRpcError as e: except JsonRpcError:
if error =? (await resubscribe(id)).errorOption: await resubscribe(id)
return failure(JsonNode, error)
# TODO: we could still miss some events between losing the subscription # TODO: we could still miss some events between losing the subscription
# and resubscribing. We should probably adopt a strategy like ethers.js, # and resubscribing. We should probably adopt a strategy like ethers.js,
# whereby we keep track of the latest block number that we've seen # whereby we keep track of the latest block number that we've seen
# filter changes for: # filter changes for:
# https://github.com/ethers-io/ethers.js/blob/f97b92bbb1bde22fcc44100af78d7f31602863ab/packages/providers/src.ts/base-provider.ts#L977 # https://github.com/ethers-io/ethers.js/blob/f97b92bbb1bde22fcc44100af78d7f31602863ab/packages/providers/src.ts/base-provider.ts#L977
except CancelledError as error:
if not ("filter not found" in e.msg): raise error
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg)) except CatchableError:
except CancelledError as e: # there's nothing we can do here
raise e discard
except SubscriptionError as e: return newJArray()
return failure(JsonNode, e)
except CatchableError as e:
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
return success(newJArray())
proc poll(id: JsonNode) {.async: (raises: [CancelledError]).} = proc poll(id: JsonNode) {.async: (raises: [CancelledError]).} =
without callback =? subscriptions.getCallback(id): for change in await getChanges(id):
return if callback =? subscriptions.getCallback(id):
callback(id, change)
without changes =? (await getChanges(id)), error:
callback(id, failure(JsonNode, error))
return
for change in changes:
callback(id, success(change))
proc poll {.async: (raises: []).} = proc poll {.async: (raises: []).} =
try: try:
@ -306,69 +206,56 @@ proc new*(_: type JsonRpcSubscriptions,
asyncSpawn subscriptions.polling asyncSpawn subscriptions.polling
subscriptions subscriptions
method close*(subscriptions: PollingSubscriptions) {.async: (raises: []).} = method close*(subscriptions: PollingSubscriptions) {.async.} =
await subscriptions.polling.cancelAndWait() await subscriptions.polling.cancelAndWait()
await procCall JsonRpcSubscriptions(subscriptions).close() await procCall JsonRpcSubscriptions(subscriptions).close()
method subscribeBlocks(subscriptions: PollingSubscriptions, method subscribeBlocks(subscriptions: PollingSubscriptions,
onBlock: BlockHandler): onBlock: BlockHandler):
Future[JsonNode] Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} = {.async.} =
proc getBlock(hash: BlockHash) {.async: (raises:[]).} = proc getBlock(hash: BlockHash) {.async.} =
try: try:
if blck =? (await subscriptions.client.eth_getBlockByHash(hash, false)): if blck =? (await subscriptions.client.eth_getBlockByHash(hash, false)):
onBlock(success(blck)) onBlock(blck)
except CancelledError: except CatchableError:
discard discard
except CatchableError as e:
let error = e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription's block: " & e.msg)
onBlock(failure(Block, error))
proc callback(id: JsonNode, changeResult: ?!JsonNode) {.raises:[].} =
without change =? changeResult, e:
onBlock(failure(Block, e.toErr(SubscriptionError)))
return
proc callback(id, change: JsonNode) =
if hash =? BlockHash.fromJson(change): if hash =? BlockHash.fromJson(change):
asyncSpawn getBlock(hash) asyncSpawn getBlock(hash)
convertErrorsToSubscriptionError: let id = await subscriptions.client.eth_newBlockFilter()
let id = await subscriptions.client.eth_newBlockFilter() subscriptions.callbacks[id] = callback
subscriptions.callbacks[id] = callback subscriptions.subscriptionMapping[id] = id
subscriptions.subscriptionMapping[id] = id return id
return id
method subscribeLogs(subscriptions: PollingSubscriptions, method subscribeLogs(subscriptions: PollingSubscriptions,
filter: EventFilter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[JsonNode] Future[JsonNode]
{.async: (raises: [SubscriptionError, CancelledError]).} = {.async.} =
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) = proc callback(id, change: JsonNode) =
without arguments =? argumentsResult, error: if log =? Log.fromJson(change):
onLog(failure(Log, error.toErr(SubscriptionError))) onLog(log)
return
let res = Log.fromJson(arguments).mapFailure(SubscriptionError) let id = await subscriptions.client.eth_newFilter(filter)
onLog(res) subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
convertErrorsToSubscriptionError: subscriptions.subscriptionMapping[id] = id
let id = await subscriptions.client.eth_newFilter(filter) return id
subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
subscriptions.subscriptionMapping[id] = id
return id
method unsubscribe*(subscriptions: PollingSubscriptions, method unsubscribe*(subscriptions: PollingSubscriptions,
id: JsonNode) id: JsonNode)
{.async: (raises: [CancelledError]).} = {.async.} =
subscriptions.logFilters.del(id)
subscriptions.callbacks.del(id)
let sub = subscriptions.subscriptionMapping[id]
subscriptions.subscriptionMapping.del(id)
try: try:
subscriptions.logFilters.del(id) discard await subscriptions.client.eth_uninstallFilter(sub)
subscriptions.callbacks.del(id)
if sub =? subscriptions.subscriptionMapping.?[id]:
subscriptions.subscriptionMapping.del(id)
discard await subscriptions.client.eth_uninstallFilter(sub)
except CancelledError as e: except CancelledError as e:
raise e raise e
except CatchableError: except CatchableError:

View File

@ -1,26 +1,22 @@
import pkg/questionable import pkg/questionable
import pkg/chronicles
import ./basics import ./basics
import ./errors
import ./provider import ./provider
export basics export basics
export errors
{.push raises: [].} {.push raises: [].}
type type
Signer* = ref object of RootObj Signer* = ref object of RootObj
populateLock: AsyncLock populateLock: AsyncLock
SignerError* = object of EthersError
template raiseSignerError*(message: string, parent: ref CatchableError = nil) = template raiseSignerError(message: string, parent: ref ProviderError = nil) =
raise newException(SignerError, message, parent) raise newException(SignerError, message, parent)
template convertError(body) = template convertError(body) =
try: try:
body body
except CancelledError as error:
raise error
except ProviderError as error: except ProviderError as error:
raise error # do not convert provider errors raise error # do not convert provider errors
except CatchableError as error: except CatchableError as error:
@ -31,66 +27,59 @@ method provider*(
doAssert false, "not implemented" doAssert false, "not implemented"
method getAddress*( method getAddress*(
signer: Signer signer: Signer): Future[Address]
): Future[Address] {. {.base, async: (raises:[ProviderError, SignerError]).} =
base, async: (raises: [ProviderError, SignerError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method signMessage*( method signMessage*(
signer: Signer, message: seq[byte] signer: Signer,
): Future[seq[byte]] {.base, async: (raises: [SignerError, CancelledError]).} = message: seq[byte]): Future[seq[byte]]
{.base, async: (raises: [SignerError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*( method sendTransaction*(
signer: Signer, transaction: Transaction signer: Signer,
): Future[TransactionResponse] {. transaction: Transaction): Future[TransactionResponse]
base, async: (raises: [SignerError, ProviderError, CancelledError]) {.base, async: (raises:[SignerError, ProviderError]).} =
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getGasPrice*( method getGasPrice*(
signer: Signer signer: Signer): Future[UInt256]
): Future[UInt256] {. {.base, async: (raises: [ProviderError, SignerError]).} =
base, async: (raises: [ProviderError, SignerError, CancelledError])
.} =
return await signer.provider.getGasPrice() return await signer.provider.getGasPrice()
method getMaxPriorityFeePerGas*(
signer: Signer
): Future[UInt256] {.async: (raises: [SignerError, CancelledError]).} =
return await signer.provider.getMaxPriorityFeePerGas()
method getTransactionCount*( method getTransactionCount*(
signer: Signer, blockTag = BlockTag.latest signer: Signer,
): Future[UInt256] {. blockTag = BlockTag.latest): Future[UInt256]
base, async: (raises: [SignerError, ProviderError, CancelledError]) {.base, async: (raises:[SignerError, ProviderError]).} =
.} =
convertError: convertError:
let address = await signer.getAddress() let address = await signer.getAddress()
return await signer.provider.getTransactionCount(address, blockTag) return await signer.provider.getTransactionCount(address, blockTag)
method estimateGas*( method estimateGas*(
signer: Signer, transaction: Transaction, blockTag = BlockTag.latest signer: Signer,
): Future[UInt256] {. transaction: Transaction,
base, async: (raises: [SignerError, ProviderError, CancelledError]) blockTag = BlockTag.latest): Future[UInt256]
.} = {.base, async: (raises:[SignerError, ProviderError]).} =
var transaction = transaction var transaction = transaction
transaction.sender = some(await signer.getAddress()) transaction.sender = some(await signer.getAddress())
return await signer.provider.estimateGas(transaction, blockTag) return await signer.provider.estimateGas(transaction, blockTag)
method getChainId*( method getChainId*(
signer: Signer signer: Signer): Future[UInt256]
): Future[UInt256] {. {.base, async: (raises: [ProviderError, SignerError]).} =
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
return await signer.provider.getChainId() return await signer.provider.getChainId()
method getNonce( method getNonce(
signer: Signer signer: Signer): Future[UInt256] {.base, async: (raises: [SignerError, ProviderError]).} =
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
return await signer.getTransactionCount(BlockTag.pending) return await signer.getTransactionCount(BlockTag.pending)
template withLock*(signer: Signer, body: untyped) = template withLock*(signer: Signer, body: untyped) =
@ -101,10 +90,7 @@ template withLock*(signer: Signer, body: untyped) =
try: try:
body body
finally: finally:
try: signer.populateLock.release()
signer.populateLock.release()
except AsyncLockError as e:
raiseSignerError e.msg, e
method populateTransaction*( method populateTransaction*(
signer: Signer, signer: Signer,
@ -130,26 +116,8 @@ method populateTransaction*(
populated.sender = some(address) populated.sender = some(address)
if transaction.chainId.isNone: if transaction.chainId.isNone:
populated.chainId = some(await signer.getChainId()) populated.chainId = some(await signer.getChainId())
if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone):
let blk = await signer.provider.getBlock(BlockTag.latest) populated.gasPrice = some(await signer.getGasPrice())
if baseFeePerGas =? blk.?baseFeePerGas:
let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas |? (await signer.provider.getMaxPriorityFeePerGas())
populated.maxPriorityFeePerGas = some(maxPriorityFeePerGas)
# Multiply by 2 because during times of congestion, baseFeePerGas can increase by 12.5% per block.
# https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273
let maxFeePerGas = transaction.maxFeePerGas |? (baseFeePerGas * 2 + maxPriorityFeePerGas)
populated.maxFeePerGas = some(maxFeePerGas)
populated.gasPrice = none(UInt256)
trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = maxFeePerGas
else:
populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice()))
populated.maxFeePerGas = none(UInt256)
populated.maxPriorityFeePerGas = none(UInt256)
trace "EIP-1559 is not supported", gasPrice = populated.gasPrice
if transaction.nonce.isNone and transaction.gasLimit.isNone: if transaction.nonce.isNone and transaction.gasLimit.isNone:
# when both nonce and gasLimit are not populated, we must ensure getNonce is # when both nonce and gasLimit are not populated, we must ensure getNonce is
@ -178,7 +146,7 @@ method populateTransaction*(
method cancelTransaction*( method cancelTransaction*(
signer: Signer, signer: Signer,
tx: Transaction tx: Transaction
): Future[TransactionResponse] {.base, async: (raises: [SignerError, CancelledError, ProviderError]).} = ): Future[TransactionResponse] {.base, async: (raises: [SignerError, CancelledError, AsyncLockError, ProviderError]).} =
# cancels a transaction by sending with a 0-valued transaction to ourselves # cancels a transaction by sending with a 0-valued transaction to ourselves
# with the failed tx's nonce # with the failed tx's nonce

View File

@ -27,7 +27,7 @@ type Wallet* = ref object of Signer
proc new*(_: type Wallet, privateKey: PrivateKey): Wallet = proc new*(_: type Wallet, privateKey: PrivateKey): Wallet =
let publicKey = privateKey.toPublicKey() let publicKey = privateKey.toPublicKey()
let address = Address(publicKey.toCanonicalAddress()) let address = Address.init(publicKey.toCanonicalAddress())
Wallet(privateKey: privateKey, publicKey: publicKey, address: address) Wallet(privateKey: privateKey, publicKey: publicKey, address: address)
proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet = proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet =
@ -53,13 +53,13 @@ proc createRandom*(_: type Wallet): Wallet =
result = Wallet() result = Wallet()
result.privateKey = PrivateKey.random(getRng()[]) result.privateKey = PrivateKey.random(getRng()[])
result.publicKey = result.privateKey.toPublicKey() result.publicKey = result.privateKey.toPublicKey()
result.address = Address(result.publicKey.toCanonicalAddress()) result.address = Address.init(result.publicKey.toCanonicalAddress())
proc createRandom*(_: type Wallet, provider: Provider): Wallet = proc createRandom*(_: type Wallet, provider: Provider): Wallet =
result = Wallet() result = Wallet()
result.privateKey = PrivateKey.random(getRng()[]) result.privateKey = PrivateKey.random(getRng()[])
result.publicKey = result.privateKey.toPublicKey() result.publicKey = result.privateKey.toPublicKey()
result.address = Address(result.publicKey.toCanonicalAddress()) result.address = Address.init(result.publicKey.toCanonicalAddress())
result.provider = some provider result.provider = some provider
method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} = method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} =
@ -69,7 +69,7 @@ method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} =
method getAddress*( method getAddress*(
wallet: Wallet): Future[Address] wallet: Wallet): Future[Address]
{.async: (raises:[ProviderError, SignerError, CancelledError]).} = {.async: (raises:[ProviderError, SignerError]).} =
return wallet.address return wallet.address
@ -83,7 +83,7 @@ proc signTransaction*(wallet: Wallet,
method sendTransaction*( method sendTransaction*(
wallet: Wallet, wallet: Wallet,
transaction: Transaction): Future[TransactionResponse] transaction: Transaction): Future[TransactionResponse]
{.async: (raises:[SignerError, ProviderError, CancelledError]).} = {.async: (raises:[SignerError, ProviderError]).} =
let signed = await signTransaction(wallet, transaction) let signed = await signTransaction(wallet, transaction)
return await provider(wallet).sendTransaction(signed) return await provider(wallet).sendTransaction(signed)

View File

@ -9,7 +9,5 @@ func raiseWalletError*(message: string) {.raises: [WalletError].}=
template convertError*(body) = template convertError*(body) =
try: try:
body body
except CancelledError as error:
raise error
except CatchableError as error: except CatchableError as error:
raiseWalletError(error.msg) raiseWalletError(error.msg)

View File

@ -1,13 +1,11 @@
import pkg/eth/keys import pkg/eth/keys
import pkg/eth/rlp import pkg/eth/rlp
import pkg/eth/common/transaction as eth import pkg/eth/common/transaction as eth
import pkg/eth/common/transaction_utils
import pkg/eth/common/eth_hash import pkg/eth/common/eth_hash
import ../../basics import ../../basics
import ../../transaction as ethers import ../../transaction as ethers
import ../../provider import ../../provider
import ./error import ./error
from pkg/eth/common/eth_types import EthAddress
type type
Transaction = ethers.Transaction Transaction = ethers.Transaction
@ -26,18 +24,17 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction =
raiseWalletError "missing gas limit" raiseWalletError "missing gas limit"
signable.nonce = nonce.truncate(uint64) signable.nonce = nonce.truncate(uint64)
signable.chainId = chainId signable.chainId = ChainId(chainId.truncate(uint64))
signable.gasLimit = GasInt(gasLimit.truncate(uint64)) signable.gasLimit = GasInt(gasLimit.truncate(uint64))
signable.to = some EthAddress(transaction.to)
signable.to = Opt.some(EthAddress(transaction.to))
signable.value = transaction.value signable.value = transaction.value
signable.payload = transaction.data signable.payload = transaction.data
if maxFeePerGas =? transaction.maxFeePerGas and if maxFee =? transaction.maxFee and
maxPriorityFeePerGas =? transaction.maxPriorityFeePerGas: maxPriorityFee =? transaction.maxPriorityFee:
signable.txType = TxEip1559 signable.txType = TxEip1559
signable.maxFeePerGas = GasInt(maxFeePerGas.truncate(uint64)) signable.maxFee = GasInt(maxFee.truncate(uint64))
signable.maxPriorityFeePerGas = GasInt(maxPriorityFeePerGas.truncate(uint64)) signable.maxPriorityFee = GasInt(maxPriorityFee.truncate(uint64))
elif gasPrice =? transaction.gasPrice: elif gasPrice =? transaction.gasPrice:
signable.txType = TxLegacy signable.txType = TxLegacy
signable.gasPrice = GasInt(gasPrice.truncate(uint64)) signable.gasPrice = GasInt(gasPrice.truncate(uint64))
@ -48,7 +45,21 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction =
func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] = func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] =
var transaction = transaction var transaction = transaction
transaction.signature = transaction.sign(key, true)
# Temporary V value, used to signal to the hashing function
# that we'd like to use an EIP-155 signature
transaction.V = int64(uint64(transaction.chainId)) * 2 + 35
let hash = transaction.txHashNoSignature().data
let signature = key.sign(SkMessage(hash)).toRaw()
transaction.R = UInt256.fromBytesBE(signature[0..<32])
transaction.S = UInt256.fromBytesBE(signature[32..<64])
transaction.V = int64(signature[64])
if transaction.txType == TxLegacy:
transaction.V += int64(uint64(transaction.chainId)) * 2 + 35
rlp.encode(transaction) rlp.encode(transaction)
func sign*(key: PrivateKey, transaction: Transaction): seq[byte] = func sign*(key: PrivateKey, transaction: Transaction): seq[byte] =

View File

@ -15,8 +15,8 @@ type
nonce*: ?UInt256 nonce*: ?UInt256
chainId*: ?UInt256 chainId*: ?UInt256
gasPrice*: ?UInt256 gasPrice*: ?UInt256
maxPriorityFeePerGas*: ?UInt256 maxFee*: ?UInt256
maxFeePerGas*: ?UInt256 maxPriorityFee*: ?UInt256
gasLimit*: ?UInt256 gasLimit*: ?UInt256
transactionType* {.serialize("type").}: ?TransactionType transactionType* {.serialize("type").}: ?TransactionType

View File

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

View File

@ -9,12 +9,11 @@ import pkg/json_rpc/errors
type MockRpcHttpServer* = ref object type MockRpcHttpServer* = ref object
filters*: seq[string] filters*: seq[string]
nextGetChangesReturnsError*: bool
srv: RpcHttpServer srv: RpcHttpServer
proc new*(_: type MockRpcHttpServer): MockRpcHttpServer = proc new*(_: type MockRpcHttpServer): MockRpcHttpServer =
let srv = newRpcHttpServer(["127.0.0.1:0"]) let srv = newRpcHttpServer(["127.0.0.1:0"])
MockRpcHttpServer(filters: @[], srv: srv, nextGetChangesReturnsError: false) MockRpcHttpServer(filters: @[], srv: srv)
proc invalidateFilter*(server: MockRpcHttpServer, jsonId: JsonNode) = proc invalidateFilter*(server: MockRpcHttpServer, jsonId: JsonNode) =
server.filters.keepItIf it != jsonId.getStr server.filters.keepItIf it != jsonId.getStr
@ -31,9 +30,6 @@ proc start*(server: MockRpcHttpServer) =
return filterId return filterId
server.srv.router.rpc("eth_getFilterChanges") do(id: string) -> seq[string]: 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: if id notin server.filters:
raise (ref ApplicationError)(code: -32000, msg: "filter not found") raise (ref ApplicationError)(code: -32000, msg: "filter not found")

View File

@ -1,5 +1,5 @@
import std/os import std/os
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/chronos import pkg/chronos
import pkg/ethers import pkg/ethers
import pkg/ethers/providers/jsonrpc/conversions import pkg/ethers/providers/jsonrpc/conversions
@ -49,7 +49,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
let oldBlock = !await provider.getBlock(BlockTag.latest) let oldBlock = !await provider.getBlock(BlockTag.latest)
discard await provider.send("evm_mine") discard await provider.send("evm_mine")
var newBlock: Block var newBlock: Block
let blockHandler = proc(blck: ?!Block) {.raises:[].}= newBlock = blck.value let blockHandler = proc(blck: Block) = newBlock = blck
let subscription = await provider.subscribe(blockHandler) let subscription = await provider.subscribe(blockHandler)
discard await provider.send("evm_mine") discard await provider.send("evm_mine")
check eventually newBlock.number.isSome check eventually newBlock.number.isSome
@ -98,7 +98,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
expect JsonRpcProviderError: expect JsonRpcProviderError:
discard await provider.getBlock(BlockTag.latest) discard await provider.getBlock(BlockTag.latest)
expect JsonRpcProviderError: expect JsonRpcProviderError:
discard await provider.subscribe(proc(_: ?!Block) = discard) discard await provider.subscribe(proc(_: Block) = discard)
expect JsonRpcProviderError: expect JsonRpcProviderError:
discard await provider.getSigner().sendTransaction(Transaction.example) discard await provider.getSigner().sendTransaction(Transaction.example)

View File

@ -1,5 +1,5 @@
import std/os import std/os
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../examples import ../../examples
@ -55,27 +55,20 @@ suite "JsonRpcSigner":
let transaction = Transaction.example let transaction = Transaction.example
let populated = await signer.populateTransaction(transaction) let populated = await signer.populateTransaction(transaction)
check !populated.sender == await signer.getAddress() check !populated.sender == await signer.getAddress()
check !populated.gasPrice == await signer.getGasPrice()
check !populated.nonce == await signer.getTransactionCount(BlockTag.pending) check !populated.nonce == await signer.getTransactionCount(BlockTag.pending)
check !populated.gasLimit == await signer.estimateGas(transaction) check !populated.gasLimit == await signer.estimateGas(transaction)
check !populated.chainId == await signer.getChainId() check !populated.chainId == await signer.getChainId()
let blk = !(await signer.provider.getBlock(BlockTag.latest))
check !populated.maxPriorityFeePerGas == await signer.getMaxPriorityFeePerGas()
check !populated.maxFeePerGas == !blk.baseFeePerGas * 2.u256 + !populated.maxPriorityFeePerGas
test "populate does not overwrite existing fields": test "populate does not overwrite existing fields":
let signer = provider.getSigner() let signer = provider.getSigner()
var transaction = Transaction.example var transaction = Transaction.example
transaction.sender = some await signer.getAddress() transaction.sender = some await signer.getAddress()
transaction.nonce = some UInt256.example transaction.nonce = some UInt256.example
transaction.chainId = some await signer.getChainId() transaction.chainId = some await signer.getChainId()
transaction.maxPriorityFeePerGas = some UInt256.example transaction.gasPrice = some UInt256.example
transaction.gasLimit = some UInt256.example transaction.gasLimit = some UInt256.example
let populated = await signer.populateTransaction(transaction) let populated = await signer.populateTransaction(transaction)
let blk = !(await signer.provider.getBlock(BlockTag.latest))
transaction.maxFeePerGas = some(!blk.baseFeePerGas * 2.u256 + !populated.maxPriorityFeePerGas)
check populated == transaction check populated == transaction
test "populate fails when sender does not match signer address": test "populate fails when sender does not match signer address":

View File

@ -1,6 +1,7 @@
import std/os import std/os
import std/sequtils
import std/importutils import std/importutils
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/serde import pkg/serde
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/json_rpc/rpcserver import pkg/json_rpc/rpcserver
@ -26,8 +27,8 @@ template subscriptionTests(subscriptions, client) =
test "subscribes to new blocks": test "subscribes to new blocks":
var latestBlock: Block var latestBlock: Block
proc callback(blck: ?!Block) = proc callback(blck: Block) =
latestBlock = blck.value latestBlock = blck
let subscription = await subscriptions.subscribeBlocks(callback) let subscription = await subscriptions.subscribeBlocks(callback)
discard await client.call("evm_mine", newJArray()) discard await client.call("evm_mine", newJArray())
check eventually latestBlock.number.isSome check eventually latestBlock.number.isSome
@ -37,9 +38,8 @@ template subscriptionTests(subscriptions, client) =
test "stops listening to new blocks when unsubscribed": test "stops listening to new blocks when unsubscribed":
var count = 0 var count = 0
proc callback(blck: ?!Block) = proc callback(blck: Block) =
if blck.isOk: inc count
inc count
let subscription = await subscriptions.subscribeBlocks(callback) let subscription = await subscriptions.subscribeBlocks(callback)
discard await client.call("evm_mine", newJArray()) discard await client.call("evm_mine", newJArray())
check eventually count > 0 check eventually count > 0
@ -49,20 +49,10 @@ template subscriptionTests(subscriptions, client) =
await sleepAsync(100.millis) await sleepAsync(100.millis)
check count == 0 check count == 0
test "unsubscribing from a non-existent subscription does not do any harm":
await subscriptions.unsubscribe(newJInt(0))
test "duplicate unsubscribe is harmless":
proc callback(blck: ?!Block) = discard
let subscription = await subscriptions.subscribeBlocks(callback)
await subscriptions.unsubscribe(subscription)
await subscriptions.unsubscribe(subscription)
test "stops listening to new blocks when provider is closed": test "stops listening to new blocks when provider is closed":
var count = 0 var count = 0
proc callback(blck: ?!Block) = proc callback(blck: Block) =
if blck.isOk: inc count
inc count
discard await subscriptions.subscribeBlocks(callback) discard await subscriptions.subscribeBlocks(callback)
discard await client.call("evm_mine", newJArray()) discard await client.call("evm_mine", newJArray())
check eventually count > 0 check eventually count > 0
@ -107,14 +97,13 @@ suite "HTTP polling subscriptions":
subscriptionTests(subscriptions, client) subscriptionTests(subscriptions, client)
suite "HTTP polling subscriptions - mock tests": suite "HTTP polling subscriptions - filter not found":
var subscriptions: PollingSubscriptions var subscriptions: PollingSubscriptions
var client: RpcHttpClient var client: RpcHttpClient
var mockServer: MockRpcHttpServer var mockServer: MockRpcHttpServer
privateAccess(PollingSubscriptions) privateAccess(PollingSubscriptions)
privateAccess(JsonRpcSubscriptions)
proc startServer() {.async.} = proc startServer() {.async.} =
mockServer = MockRpcHttpServer.new() mockServer = MockRpcHttpServer.new()
@ -141,7 +130,7 @@ suite "HTTP polling subscriptions - mock tests":
test "filter not found error recreates log filter": test "filter not found error recreates log filter":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard let emptyHandler = proc(log: Log) = discard
check subscriptions.logFilters.len == 0 check subscriptions.logFilters.len == 0
check subscriptions.subscriptionMapping.len == 0 check subscriptions.subscriptionMapping.len == 0
@ -159,7 +148,7 @@ suite "HTTP polling subscriptions - mock tests":
test "recreated log filter can be still unsubscribed using the original id": test "recreated log filter can be still unsubscribed using the original id":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard let emptyHandler = proc(log: Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler) let id = await subscriptions.subscribeLogs(filter, emptyHandler)
mockServer.invalidateFilter(id) mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id check eventually subscriptions.subscriptionMapping[id] != id
@ -170,7 +159,7 @@ suite "HTTP polling subscriptions - mock tests":
check not subscriptions.subscriptionMapping.hasKey id check not subscriptions.subscriptionMapping.hasKey id
test "filter not found error recreates block filter": test "filter not found error recreates block filter":
let emptyHandler = proc(blck: ?!Block) = discard let emptyHandler = proc(blck: Block) = discard
check subscriptions.subscriptionMapping.len == 0 check subscriptions.subscriptionMapping.len == 0
let id = await subscriptions.subscribeBlocks(emptyHandler) let id = await subscriptions.subscribeBlocks(emptyHandler)
@ -181,7 +170,7 @@ suite "HTTP polling subscriptions - mock tests":
check eventually subscriptions.subscriptionMapping[id] != id check eventually subscriptions.subscriptionMapping[id] != id
test "recreated block filter can be still unsubscribed using the original id": test "recreated block filter can be still unsubscribed using the original id":
let emptyHandler = proc(blck: ?!Block) = discard let emptyHandler = proc(blck: Block) = discard
let id = await subscriptions.subscribeBlocks(emptyHandler) let id = await subscriptions.subscribeBlocks(emptyHandler)
mockServer.invalidateFilter(id) mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id check eventually subscriptions.subscriptionMapping[id] != id
@ -192,7 +181,7 @@ suite "HTTP polling subscriptions - mock tests":
test "polling continues with new filter after temporary error": test "polling continues with new filter after temporary error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard let emptyHandler = proc(log: Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler) let id = await subscriptions.subscribeLogs(filter, emptyHandler)
@ -202,17 +191,3 @@ suite "HTTP polling subscriptions - mock tests":
await startServer() await startServer()
check eventually subscriptions.subscriptionMapping[id] != id check eventually subscriptions.subscriptionMapping[id] != id
test "calls callback with failed result on error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
var failedResultReceived = false
proc handler(log: ?!Log) =
if log.isErr:
failedResultReceived = true
let id = await subscriptions.subscribeLogs(filter, handler)
await sleepAsync(50.milliseconds)
mockServer.nextGetChangesReturnsError = true
check eventually failedResultReceived

View File

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

View File

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

View File

@ -9,6 +9,5 @@ import ./testErc20
import ./testGasEstimation import ./testGasEstimation
import ./testErrorDecoding import ./testErrorDecoding
import ./testCustomErrors import ./testCustomErrors
import ./testBlockTag
{.warning[UnusedImport]:off.} {.warning[UnusedImport]:off.}

View File

@ -3,7 +3,7 @@ author = "Nim Ethers Authors"
description = "Tests for Nim Ethers library" description = "Tests for Nim Ethers library"
license = "MIT" license = "MIT"
requires "asynctest >= 0.5.4 & < 0.6.0" requires "asynctest >= 0.4.0 & < 0.5.0"
task test, "Run the test suite": task test, "Run the test suite":
exec "nimble install -d -y" exec "nimble install -d -y"

View File

@ -1,56 +0,0 @@
import std/unittest
import std/strformat
import pkg/stint
import pkg/questionable
import ethers/blocktag
type
PredefinedTags = enum earliest, latest, pending
suite "BlockTag":
for predefinedTag in PredefinedTags:
test fmt"can be created with predefined special type: {predefinedTag}":
var blockTag: BlockTag
case predefinedTag:
of earliest: blockTag = BlockTag.earliest
of latest: blockTag = BlockTag.latest
of pending: blockTag = BlockTag.pending
check $blockTag == $predefinedTag
test "can be created with a number":
let blockTag = BlockTag.init(42.u256)
check blockTag.number == 42.u256.some
test "can be converted to string in hex format for BlockTags with number":
let blockTag = BlockTag.init(42.u256)
check $blockTag == "0x2a"
test "can be compared for equality when BlockTag with number":
let blockTag1 = BlockTag.init(42.u256)
let blockTag2 = BlockTag.init(42.u256)
let blockTag3 = BlockTag.init(43.u256)
check blockTag1 == blockTag2
check blockTag1 != blockTag3
for predefinedTag in [BlockTag.earliest, BlockTag.latest, BlockTag.pending]:
test fmt"can be compared for equality when predefined tag: {predefinedTag}":
case $predefinedTag:
of "earliest":
check predefinedTag == BlockTag.earliest
check predefinedTag != BlockTag.latest
check predefinedTag != BlockTag.pending
of "latest":
check predefinedTag != BlockTag.earliest
check predefinedTag == BlockTag.latest
check predefinedTag != BlockTag.pending
of "pending":
check predefinedTag != BlockTag.earliest
check predefinedTag != BlockTag.latest
check predefinedTag == BlockTag.pending
for predefinedTag in [BlockTag.earliest, BlockTag.latest, BlockTag.pending]:
test fmt"number accessor returns None for BlockTags with string: {predefinedTag}":
check predefinedTag.number == UInt256.none

View File

@ -1,7 +1,7 @@
import pkg/serde import pkg/serde
import std/os import std/os
import std/options import std/options
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/questionable import pkg/questionable
import pkg/stint import pkg/stint
import pkg/ethers import pkg/ethers
@ -64,7 +64,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
test "can call constant functions without a return type": test "can call constant functions without a return type":
token = TestToken.new(token.address, provider.getSigner()) token = TestToken.new(token.address, provider.getSigner())
proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract, view.} proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract, view.}
await token.mint(accounts[1], 100.u256) await mint(token, accounts[1], 100.u256)
check (await balanceOf(token, accounts[1])) == 0.u256 check (await balanceOf(token, accounts[1])) == 0.u256
test "can call non-constant functions without a return type": test "can call non-constant functions without a return type":
@ -74,6 +74,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
check (await balanceOf(token, accounts[1])) == 100.u256 check (await balanceOf(token, accounts[1])) == 100.u256
test "can call non-constant functions with a Confirmable return type": test "can call non-constant functions with a Confirmable return type":
token = TestToken.new(token.address, provider.getSigner()) token = TestToken.new(token.address, provider.getSigner())
proc mint(token: TestToken, proc mint(token: TestToken,
holder: Address, holder: Address,
@ -107,17 +108,17 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256 check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256
check (await token.connect(provider).balanceOf(accounts[2])) == 25.u256 check (await token.connect(provider).balanceOf(accounts[2])) == 25.u256
test "takes custom values for nonce, gasprice and maxPriorityFeePerGas": test "takes custom values for nonce, gasprice and gaslimit":
let overrides = TransactionOverrides( let overrides = TransactionOverrides(
nonce: some 100.u256, nonce: some 100.u256,
maxPriorityFeePerGas: some 200.u256, gasPrice: some 200.u256,
gasLimit: some 300.u256 gasLimit: some 300.u256
) )
let signer = MockSigner.new(provider) let signer = MockSigner.new(provider)
discard await token.connect(signer).mint(accounts[0], 42.u256, overrides) discard await token.connect(signer).mint(accounts[0], 42.u256, overrides)
check signer.transactions.len == 1 check signer.transactions.len == 1
check signer.transactions[0].nonce == overrides.nonce check signer.transactions[0].nonce == overrides.nonce
check signer.transactions[0].maxPriorityFeePerGas == overrides.maxPriorityFeePerGas check signer.transactions[0].gasPrice == overrides.gasPrice
check signer.transactions[0].gasLimit == overrides.gasLimit check signer.transactions[0].gasLimit == overrides.gasLimit
test "can call functions for different block heights": test "can call functions for different block heights":
@ -145,22 +146,10 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
discard await token.transfer(accounts[1], 50.u256, beforeMint) discard await token.transfer(accounts[1], 50.u256, beforeMint)
discard await token.transfer(accounts[1], 50.u256, afterMint) discard await token.transfer(accounts[1], 50.u256, afterMint)
test "can estimate gas of a function call":
proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract.}
let estimate = await token.estimateGas.mint(accounts[1], 100.u256)
let correctGas = TransactionOverrides(gasLimit: some estimate)
await token.mint(accounts[1], 100.u256, correctGas)
let invalidGas = TransactionOverrides(gasLimit: some (estimate - 1))
expect ProviderError:
await token.mint(accounts[1], 100.u256, invalidGas)
test "receives events when subscribed": test "receives events when subscribed":
var transfers: seq[Transfer] var transfers: seq[Transfer]
proc handleTransfer(transferRes: ?!Transfer) = proc handleTransfer(transfer: Transfer) =
without transfer =? transferRes, error:
echo error.msg
transfers.add(transfer) transfers.add(transfer)
let signer0 = provider.getSigner(accounts[0]) let signer0 = provider.getSigner(accounts[0])
@ -182,9 +171,8 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
test "stops receiving events when unsubscribed": test "stops receiving events when unsubscribed":
var transfers: seq[Transfer] var transfers: seq[Transfer]
proc handleTransfer(transferRes: ?!Transfer) = proc handleTransfer(transfer: Transfer) =
if transfer =? transferRes: transfers.add(transfer)
transfers.add(transfer)
let signer0 = provider.getSigner(accounts[0]) let signer0 = provider.getSigner(accounts[0])

View File

@ -1,6 +1,6 @@
import std/os import std/os
import pkg/serde import pkg/serde
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/ethers import pkg/ethers
import ./hardhat import ./hardhat
@ -97,7 +97,7 @@ suite "Contract custom errors":
expect ErrorWithArguments: expect ErrorWithArguments:
await contract.revertsMultipleErrors(simple = false) await contract.revertsMultipleErrors(simple = false)
test "handles gas estimation errors when calling a contract function": test "handles gas estimation errors":
proc revertsTransaction(contract: TestCustomErrors) proc revertsTransaction(contract: TestCustomErrors)
{.contract, errors:[ErrorWithArguments].} {.contract, errors:[ErrorWithArguments].}
@ -109,18 +109,6 @@ suite "Contract custom errors":
check error.arguments.one == 1.u256 check error.arguments.one == 1.u256
check error.arguments.two == true check error.arguments.two == true
test "handles errors when only doing gas estimation":
proc revertsTransaction(contract: TestCustomErrors)
{.contract, errors:[ErrorWithArguments], used.}
let contract = contract.connect(provider.getSigner())
try:
discard await contract.estimateGas.revertsTransaction()
fail()
except ErrorWithArguments as error:
check error.arguments.one == 1.u256
check error.arguments.two == true
test "handles transaction submission errors": test "handles transaction submission errors":
proc revertsTransaction(contract: TestCustomErrors) proc revertsTransaction(contract: TestCustomErrors)
{.contract, errors:[ErrorWithArguments].} {.contract, errors:[ErrorWithArguments].}

View File

@ -1,5 +1,5 @@
import std/os import std/os
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/serde import pkg/serde
import ./hardhat import ./hardhat

View File

@ -1,6 +1,6 @@
import std/os import std/os
import pkg/serde import pkg/serde
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/questionable import pkg/questionable
import pkg/stint import pkg/stint
import pkg/ethers import pkg/ethers

View File

@ -3,7 +3,7 @@ import std/strutils
import pkg/questionable/results import pkg/questionable/results
import pkg/contractabi import pkg/contractabi
import pkg/ethers/errors import pkg/ethers/errors
import pkg/ethers/contracts/errors/encoding import pkg/ethers/errors/encoding
suite "Decoding of custom errors": suite "Decoding of custom errors":

View File

@ -1,4 +1,4 @@
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/contractabi import pkg/contractabi
import ./examples import ./examples

View File

@ -1,5 +1,5 @@
import std/os import std/os
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/serde import pkg/serde
import ./hardhat import ./hardhat
@ -28,7 +28,7 @@ suite "gas estimation":
discard await provider.send("evm_revert", @[snapshot]) discard await provider.send("evm_revert", @[snapshot])
await provider.close() await provider.close()
test "contract function calls use pending block for gas estimations": test "uses pending block for gas estimations":
let latest = CallOverrides(blockTag: some BlockTag.latest) let latest = CallOverrides(blockTag: some BlockTag.latest)
let pending = CallOverrides(blockTag: some BlockTag.pending) let pending = CallOverrides(blockTag: some BlockTag.pending)
@ -38,40 +38,6 @@ suite "gas estimation":
# ensure that time of latest block and pending block differ # ensure that time of latest block and pending block differ
check (await contract.getTime(overrides=latest)) != time check (await contract.getTime(overrides=latest)) != time
# only succeeds when gas estimation is done using the pending block, # fails with "Transaction ran out of gas" when gas estimation
# otherwise it will fail with "Transaction ran out of gas" # is not done using the pending block
await contract.checkTimeEquals(time) await contract.checkTimeEquals(time)
test "contract gas estimation uses pending block":
let latest = CallOverrides(blockTag: some BlockTag.latest)
let pending = CallOverrides(blockTag: some BlockTag.pending)
# retrieve time of pending block
let time = await contract.getTime(overrides=pending)
# ensure that time of latest block and pending block differ
check (await contract.getTime(overrides=latest)) != time
# estimate gas
let gas = await contract.estimateGas.checkTimeEquals(time)
let overrides = TransactionOverrides(gasLimit: some gas)
# only succeeds when gas estimation is done using the pending block,
# otherwise it will fail with "Transaction ran out of gas"
await contract.checkTimeEquals(time, overrides)
test "contract gas estimation honors a block tag override":
let latest = CallOverrides(blockTag: some BlockTag.latest)
let pending = CallOverrides(blockTag: some BlockTag.pending)
# retrieve time of pending block
let time = await contract.getTime(overrides=pending)
# ensure that time of latest block and pending block differ
check (await contract.getTime(overrides=latest)) != time
# estimate gas
let gasLatest = await contract.estimateGas.checkTimeEquals(time, latest)
let gasPending = await contract.estimateGas.checkTimeEquals(time, pending)
check gasLatest != gasPending

View File

@ -1,5 +1,5 @@
import std/os import std/os
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/ethers import pkg/ethers
import pkg/serde import pkg/serde
import ./hardhat import ./hardhat

View File

@ -1,6 +1,6 @@
import std/os import std/os
import std/strformat import std/strformat
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/chronos import pkg/chronos
import pkg/ethers import pkg/ethers
import pkg/ethers/testing import pkg/ethers/testing

View File

@ -1,5 +1,5 @@
import std/os import std/os
import pkg/asynctest/chronos/unittest import pkg/asynctest
import pkg/serde import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ../ethers import ../ethers
@ -80,8 +80,8 @@ suite "Wallet":
to: wallet.address, to: wallet.address,
nonce: some 0.u256, nonce: some 0.u256,
chainId: some 31337.u256, chainId: some 31337.u256,
maxFeePerGas: some 2_000_000_000.u256, maxFee: some 2_000_000_000.u256,
maxPriorityFeePerGas: some 1_000_000_000.u256, maxPriorityFee: some 1_000_000_000.u256,
gasLimit: some 21_000.u256 gasLimit: some 21_000.u256
) )
let signedTx = await wallet.signTransaction(tx) let signedTx = await wallet.signTransaction(tx)
@ -115,8 +115,8 @@ suite "Wallet":
let wallet = !Wallet.new(pk_with_funds, provider) let wallet = !Wallet.new(pk_with_funds, provider)
let overrides = TransactionOverrides( let overrides = TransactionOverrides(
nonce: some 0.u256, nonce: some 0.u256,
maxFeePerGas: some 1_000_000_000.u256, maxFee: some 1_000_000_000.u256,
maxPriorityFeePerGas: some 1_000_000_000.u256, maxPriorityFee: some 1_000_000_000.u256,
gasLimit: some 22_000.u256) gasLimit: some 22_000.u256)
let testToken = Erc20.new(wallet.address, wallet) let testToken = Erc20.new(wallet.address, wallet)
await testToken.transfer(wallet.address, 24.u256, overrides) await testToken.transfer(wallet.address, 24.u256, overrides)