Compare commits

..

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

78 changed files with 2601 additions and 8747 deletions

View File

@ -1,53 +1,31 @@
name: CI name: CI
on: on: [push, pull_request]
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
strategy: strategy:
matrix: matrix:
nim: [2.0.14, 2.2.2] nim: [1.6.14, stable]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Nim - name: Install Nim
uses: iffy/install-nim@v4 uses: iffy/install-nim@v3
with: with:
version: ${{ matrix.nim }} version: ${{ matrix.nim }}
- name: Install NodeJS - name: Install NodeJS
uses: actions/setup-node@v4 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: '14'
- name: Install test node - name: Install test node
working-directory: testnode working-directory: testnode
run: npm install run: npm install
- name: Run test node - name: Run test node
working-directory: testnode working-directory: testnode
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
status:
if: always()
needs: [test]
runs-on: ubuntu-latest
steps:
- if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}
run: exit 1

3
.gitignore vendored
View File

@ -4,6 +4,3 @@
nimble.develop nimble.develop
nimble.paths nimble.paths
.idea .idea
.nimble
.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.7.0 & < 0.8.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:
@ -160,43 +146,6 @@ When you're no longer interested in these events, you can unsubscribe:
await subscription.unsubscribe() await subscription.unsubscribe()
``` ```
Custom errors
-------------
Solidity's [custom errors][4] are supported. To use them, you declare their type
and indicate in which contract functions they can occur. For instance, this is
how you would define the "InsufficientBalance" error to match the definition in
[this Solidity example][5]:
```nim
type
InsufficientBalance = object of SolidityError
arguments: tuple[available: UInt256, required: UInt256]
```
Notice that `InsufficientBalance` inherits from `SoldityError`, and that it has
an `arguments` tuple whose fields match the definition in Solidity.
You can use the `{.errors.}` pragma to declare that this error may occur in a
contract function:
```nim
proc transfer*(token: Erc20Token, recipient: Address, amount: UInt256)
{.contract, errors:[InsufficientBalance].}
```
This allows you to write error handling code for the `transfer` function like
this:
```nim
try:
await token.transfer(recipient, 100.u256)
except InsufficientBalance as error:
echo "insufficient balance"
echo "available balance: ", error.arguments.available
echo "required balance: ", error.arguments.required
```
Utilities Utilities
--------- ---------
@ -204,13 +153,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
------------ ------------
@ -223,9 +165,6 @@ $ npm ci
$ npm start $ npm start
``` ```
If you need to use different port for the RPC node, then you can start with `npm start -- --port 1111` and
then run the tests with `ETHERS_TEST_PROVIDER=1111 nimble test`.
Thanks Thanks
------ ------
@ -236,5 +175,3 @@ affiliation) and [nim-web3][1] developers.
[1]: https://github.com/status-im/nim-web3 [1]: https://github.com/status-im/nim-web3
[2]: https://github.com/nim-lang/nimble [2]: https://github.com/nim-lang/nimble
[3]: https://docs.soliditylang.org/en/v0.8.11/contracts.html#state-mutability [3]: https://docs.soliditylang.org/en/v0.8.11/contracts.html#state-mutability
[4]: https://docs.soliditylang.org/en/v0.8.25/contracts.html#errors-and-the-revert-statement
[5]: https://soliditylang.org/blog/2021/04/21/custom-errors/

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,20 +1,19 @@
version = "2.1.0" version = "0.7.0"
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 "chronos >= 3.0.0 & < 4.0.0"
requires "chronos >= 4.0.4 & < 4.1.0" requires "contractabi >= 0.6.0 & < 0.7.0"
requires "contractabi >= 0.7.2 & < 0.8.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 "upraises >= 0.1.0 & < 0.2.0"
requires "serde >= 1.2.1 & < 1.3.0" requires "json_rpc"
requires "stint >= 0.8.1 & < 0.9.0" requires "stint"
requires "stew >= 0.2.0" requires "stew"
requires "eth >= 0.6.0 & < 0.10.0" requires "eth"
task test, "Run the test suite": task test, "Run the test suite":
# exec "nimble install -d -y" exec "nimble install -d -y"
withDir "testmodule": withDir "testmodule":
exec "nimble test" exec "nimble test"

View File

@ -2,12 +2,14 @@ import pkg/chronos
import pkg/questionable import pkg/questionable
import pkg/questionable/results import pkg/questionable/results
import pkg/stint import pkg/stint
import pkg/upraises
import pkg/contractabi/address import pkg/contractabi/address
export chronos export chronos
export questionable export questionable
export results export results
export stint export stint
export upraises
export address export address
type type

View File

@ -1,7 +1,7 @@
import pkg/stint import pkg/stint
import pkg/questionable import pkg/upraises
{.push raises:[].} push: {.upraises: [].}
type type
BlockTagKind = enum BlockTagKind = enum
@ -35,17 +35,3 @@ func `$`*(blockTag: BlockTag): string =
blockTag.stringValue blockTag.stringValue
of numberBlockTag: of numberBlockTag:
"0x" & blockTag.numberValue.toHex "0x" & blockTag.numberValue.toHex
func `==`*(a, b: BlockTag): bool =
case a.kind
of stringBlockTag:
a.stringValue == b.stringValue
of numberBlockTag:
a.numberValue == b.numberValue
func number*(blockTag: BlockTag): ?UInt256 =
case blockTag.kind
of stringBlockTag:
UInt256.none
of numberBlockTag:
blockTag.numberValue.some

307
ethers/contract.nim Normal file
View File

@ -0,0 +1,307 @@
import std/json
import std/macros
import std/sequtils
import pkg/chronos
import pkg/contractabi
import ./basics
import ./provider
import ./signer
import ./events
import ./fields
export basics
export provider
export events
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* = ?TransactionResponse
EventHandler*[E: Event] = proc(event: E) {.gcsafe, upraises:[].}
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, multiple: static bool, bytes: seq[byte]): T =
when multiple:
without decoded =? AbiDecoder.decode(bytes, T):
raiseContractError "unable to decode return value as " & $T
return decoded
else:
return decodeResponse((T,), true, bytes)[0]
proc call(provider: Provider,
transaction: Transaction,
overrides: TransactionOverrides): Future[seq[byte]] =
if overrides of CallOverrides and
blockTag =? CallOverrides(overrides).blockTag:
provider.call(transaction, blockTag)
else:
provider.call(transaction)
proc call(contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()) {.async.} =
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,
returnMultiple: static bool,
overrides = TransactionOverrides()): Future[ReturnType] {.async.} =
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, returnMultiple, response)
proc send(contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()):
Future[?TransactionResponse] {.async.} =
if signer =? contract.signer:
let transaction = createTransaction(contract, function, parameters, overrides)
let populated = await signer.populateTransaction(transaction)
let txResp = await signer.sendTransaction(populated)
return txResp.some
else:
await call(contract, function, parameters)
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 isConstant(procedure: NimNode): bool =
let pragmas = procedure[4]
for pragma in pragmas:
if pragma.eqIdent "view":
return true
elif pragma.eqIdent "pure":
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 returnMultiple = returnType.isMultipleReturn.newLit
procedure.addOverrides()
func call: NimNode =
if returnType.kind == nnkEmpty:
quote:
await call(`contract`, `function`, `parameters`, overrides)
else:
quote:
return await call(
`contract`, `function`, `parameters`, `returnType`, `returnMultiple`, overrides)
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.} or {.pure.} ?".}
return await send(`contract`, `function`, `parameters`, overrides)
procedure[6] =
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 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.addFuture()
contractcall.addAsyncPragma()
contractcall
template view* {.pragma.}
template pure* {.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) {.upraises: [].} =
if event =? E.decode(log.data, log.topics):
handler(event)
contract.provider.subscribe(filter, logHandler)
proc confirm*(tx: Future[?TransactionResponse],
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)`
without response =? (await tx):
raise newException(
EthersError,
"Transaction hash required. Possibly was a call instead of a send?"
)
return await response.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,15 +0,0 @@
import ../../basics
import ../../provider
import ./encoding
type ConvertCustomErrors* =
proc(error: ref ProviderError): ref EthersError {.gcsafe, raises:[].}
func customErrorConversion*(ErrorTypes: type tuple): ConvertCustomErrors =
func convert(error: ref ProviderError): ref EthersError =
if data =? error.data:
for e in ErrorTypes.default.fields:
if error =? typeof(e).decode(data):
return error
return error
convert

View File

@ -1,37 +0,0 @@
import pkg/contractabi
import pkg/contractabi/selector
import ../../basics
import ../../errors
func selector(E: type): FunctionSelector =
when compiles(E.arguments):
selector($E, typeof(E.arguments))
else:
selector($E, tuple[])
func matchesSelector(E: type, data: seq[byte]): bool =
const selector = E.selector.toArray
data.len >= 4 and selector[0..<4] == data[0..<4]
func decodeArguments(E: type, data: seq[byte]): auto =
AbiDecoder.decode(data[4..^1], E.arguments)
func decode*[E: SolidityError](_: type E, data: seq[byte]): ?!(ref E) =
if not E.matchesSelector(data):
return failure "unable to decode " & $E & ": selector doesn't match"
when compiles(E.arguments):
without arguments =? E.decodeArguments(data), error:
return failure "unable to decode arguments of " & $E & ": " & error.msg
let message = "EVM reverted: " & $E & $arguments
success (ref E)(msg: message, arguments: arguments)
else:
if data.len > 4:
return failure "unable to decode: " & $E & ".arguments is not defined"
let message = "EVM reverted: " & $E & "()"
success (ref E)(msg: message)
func encode*[E: SolidityError](_: type AbiEncoder, error: ref E): seq[byte] =
result = @(E.selector.toArray)
when compiles(error.arguments):
result &= AbiEncoder.encode(error.arguments)

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

@ -46,35 +46,17 @@ method allowance*(token: Erc20Token,
method transfer*(token: Erc20Token, method transfer*(token: Erc20Token,
recipient: Address, recipient: Address,
amount: UInt256): Confirmable {.base, contract.} amount: UInt256): ?TransactionResponse {.base, contract.}
## Moves `amount` tokens from the caller's account to `recipient`. ## Moves `amount` tokens from the caller's account to `recipient`.
method approve*(token: Erc20Token, method approve*(token: Erc20Token,
spender: Address, spender: Address,
amount: UInt256): Confirmable {.base, contract.} amount: UInt256): ?TransactionResponse {.base, contract.}
## Sets `amount` as the allowance of `spender` over the caller's tokens. ## Sets `amount` as the allowance of `spender` over the caller's tokens.
method increaseAllowance*(token: Erc20Token,
spender: Address,
addedValue: UInt256): Confirmable {.base, contract.}
## Atomically increases the allowance granted to spender by the caller.
## This is an alternative to approve that can be used as a mitigation for problems described in IERC20.approve.
## Emits an Approval event indicating the updated allowance.
##
## WARNING: THIS IS NON-STANDARD ERC-20 FUNCTION, DOUBLE CHECK THAT YOUR TOKEN HAS IT!
method decreaseAllowance*(token: Erc20Token,
spender: Address,
addedValue: UInt256): Confirmable {.base, contract.}
## Atomically decreases the allowance granted to spender by the caller.
## This is an alternative to approve that can be used as a mitigation for problems described in IERC20.approve.
## Emits an Approval event indicating the updated allowance.
##
## WARNING: THIS IS NON-STANDARD ERC-20 FUNCTION, DOUBLE CHECK THAT YOUR TOKEN HAS IT!
method transferFrom*(token: Erc20Token, method transferFrom*(token: Erc20Token,
spender: Address, spender: Address,
recipient: Address, recipient: Address,
amount: UInt256): Confirmable {.base, contract.} amount: UInt256): ?TransactionResponse {.base, contract.}
## Moves `amount` tokens from `spender` to `recipient` using the allowance ## Moves `amount` tokens from `from` to `to` using the allowance
## mechanism. `amount` is then deducted from the caller's allowance. ## mechanism. `amount` is then deducted from the caller's allowance.

View File

@ -1,18 +0,0 @@
import ./basics
type
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:[].}
proc toErr*[E1: ref CatchableError, E2: EthersError](
e1: E1,
_: type E2,
msg: string = e1.msg): ref E2 =
return newException(E2, msg, e1)

View File

@ -1,12 +1,12 @@
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
{.push raises:[].} push: {.upraises: [].}
template indexed* {.pragma.} template indexed* {.pragma.}

View File

@ -1,49 +0,0 @@
## Fixes an underlying Exception caused by missing forward declarations for
## `std/json.JsonNode.hash`, eg when using `JsonNode` as a `Table` key. Adds
## {.raises: [].} for proper exception tracking. Copied from the std/json module
import pkg/serde
import std/hashes
{.push raises:[].}
when (NimMajor) >= 2:
proc hash*[A](x: openArray[A]): Hash =
## Efficient hashing of arrays and sequences.
## There must be a `hash` proc defined for the element type `A`.
when A is byte:
result = murmurHash(x)
elif A is char:
when nimvm:
result = hashVmImplChar(x, 0, x.high)
else:
result = murmurHash(toOpenArrayByte(x, 0, x.high))
else:
for a in x:
result = result !& hash(a)
result = !$result
func hash*(n: OrderedTable[string, JsonNode]): Hash
func hash*(n: JsonNode): Hash =
## Compute the hash for a JSON node
case n.kind
of JArray:
result = hash(n.elems)
of JObject:
result = hash(n.fields)
of JInt:
result = hash(n.num)
of JFloat:
result = hash(n.fnum)
of JBool:
result = hash(n.bval.int)
of JString:
result = hash(n.str)
of JNull:
result = Hash(0)
func hash*(n: OrderedTable[string, JsonNode]): Hash =
for key, val in n:
result = result xor (hash(key) !& hash(val))
result = !$result

View File

@ -1,32 +1,26 @@
import pkg/chronicles
import pkg/serde
import pkg/questionable
import ./basics import ./basics
import ./transaction import ./transaction
import ./blocktag import ./blocktag
import ./errors
export basics export basics
export transaction export transaction
export blocktag export blocktag
export errors
{.push raises: [].} push: {.upraises: [].}
type type
Provider* = ref object of RootObj Provider* = ref object of RootObj
EstimateGasError* = object of ProviderError ProviderError* = object of EthersError
transaction*: Transaction
Subscription* = ref object of RootObj Subscription* = ref object of RootObj
EventFilter* {.serialize.} = ref object of RootObj EventFilter* = ref object of RootObj
address*: Address address*: Address
topics*: seq[Topic] topics*: seq[Topic]
Filter* {.serialize.} = ref object of EventFilter Filter* = ref object of EventFilter
fromBlock*: BlockTag fromBlock*: BlockTag
toBlock*: BlockTag toBlock*: BlockTag
FilterByBlockHash* {.serialize.} = ref object of EventFilter FilterByBlockHash* = ref object of EventFilter
blockHash*: BlockHash blockHash*: BlockHash
Log* {.serialize.} = object Log* = object
blockNumber*: UInt256 blockNumber*: UInt256
data*: seq[byte] data*: seq[byte]
logIndex*: UInt256 logIndex*: UInt256
@ -40,9 +34,9 @@ type
Invalid = 2 Invalid = 2
TransactionResponse* = object TransactionResponse* = object
provider*: Provider provider*: Provider
hash* {.serialize.}: TransactionHash hash*: TransactionHash
TransactionReceipt* {.serialize.} = object TransactionReceipt* = object
sender* {.serialize("from"), deserialize("from").}: ?Address sender*: ?Address
to*: ?Address to*: ?Address
contractAddress*: ?Address contractAddress*: ?Address
transactionIndex*: UInt256 transactionIndex*: UInt256
@ -53,215 +47,95 @@ type
logs*: seq[Log] logs*: seq[Log]
blockNumber*: ?UInt256 blockNumber*: ?UInt256
cumulativeGasUsed*: UInt256 cumulativeGasUsed*: UInt256
effectiveGasPrice*: ?UInt256
status*: TransactionStatus status*: TransactionStatus
transactionType* {.serialize("type"), deserialize("type").}: TransactionType LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
LogHandler* = proc(log: ?!Log) {.gcsafe, raises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
BlockHandler* = proc(blck: ?!Block) {.gcsafe, raises:[].}
Topic* = array[32, byte] Topic* = array[32, byte]
Block* {.serialize.} = object Block* = object
number*: ?UInt256 number*: ?UInt256
timestamp*: UInt256 timestamp*: UInt256
hash*: ?BlockHash hash*: ?BlockHash
baseFeePerGas* : ?UInt256
PastTransaction* {.serialize.} = object
blockHash*: BlockHash
blockNumber*: UInt256
sender* {.serialize("from"), deserialize("from").}: Address
gas*: UInt256
gasPrice*: UInt256
hash*: TransactionHash
input*: seq[byte]
nonce*: UInt256
to*: Address
transactionIndex*: UInt256
transactionType* {.serialize("type"), deserialize("type").}: ?TransactionType
chainId*: ?UInt256
value*: UInt256
v*, r*, s*: UInt256
const EthersDefaultConfirmations* {.intdefine.} = 12 const EthersDefaultConfirmations* {.intdefine.} = 12
const EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks const EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks
logScope: method getBlockNumber*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
topics = "ethers provider"
template raiseProviderError(msg: string) =
raise newException(ProviderError, msg)
func toTransaction*(past: PastTransaction): Transaction =
Transaction(
sender: some past.sender,
to: past.to,
data: past.input,
value: past.value,
nonce: some past.nonce,
chainId: past.chainId,
gasPrice: some past.gasPrice,
gasLimit: some past.gas,
transactionType: past.transactionType
)
method getBlockNumber*(
provider: Provider
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getBlock*( method getBlock*(provider: Provider, tag: BlockTag): Future[?Block] {.base, gcsafe.} =
provider: Provider, tag: BlockTag
): Future[?Block] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method call*( method call*(provider: Provider,
provider: Provider, tx: Transaction, blockTag = BlockTag.latest tx: Transaction,
): Future[seq[byte]] {.base, async: (raises: [ProviderError, CancelledError]).} = blockTag = BlockTag.latest): Future[seq[byte]] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getGasPrice*( method getGasPrice*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
provider: Provider
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getMaxPriorityFeePerGas*( method getTransactionCount*(provider: Provider,
provider: Provider address: Address,
): Future[UInt256] {.base, async: (raises: [CancelledError]).} = blockTag = BlockTag.latest):
Future[UInt256] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransactionCount*( method getTransactionReceipt*(provider: Provider,
provider: Provider, address: Address, blockTag = BlockTag.latest txHash: TransactionHash):
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = Future[?TransactionReceipt] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransaction*( method sendTransaction*(provider: Provider,
provider: Provider, txHash: TransactionHash rawTransaction: seq[byte]):
): Future[?PastTransaction] {.base, async: (raises: [ProviderError, CancelledError]).} = Future[TransactionResponse] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getTransactionReceipt*( method getLogs*(provider: Provider,
provider: Provider, txHash: TransactionHash filter: EventFilter): Future[seq[Log]] {.base, gcsafe.} =
): Future[?TransactionReceipt] {.
base, async: (raises: [ProviderError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*( method estimateGas*(provider: Provider,
provider: Provider, rawTransaction: seq[byte] transaction: Transaction): Future[UInt256] {.base, gcsafe.} =
): Future[TransactionResponse] {.
base, async: (raises: [ProviderError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getLogs*( method getChainId*(provider: Provider): Future[UInt256] {.base, gcsafe.} =
provider: Provider, filter: EventFilter
): Future[seq[Log]] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method estimateGas*( method subscribe*(provider: Provider,
provider: Provider, transaction: Transaction, blockTag = BlockTag.latest filter: EventFilter,
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = callback: LogHandler):
Future[Subscription] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getChainId*( method subscribe*(provider: Provider,
provider: Provider callback: BlockHandler):
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = Future[Subscription] {.base, gcsafe.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method subscribe*( method unsubscribe*(subscription: Subscription) {.base, async.} =
provider: Provider, filter: EventFilter, callback: LogHandler
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method subscribe*( proc confirm*(tx: TransactionResponse,
provider: Provider, callback: BlockHandler
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented"
method unsubscribe*(
subscription: Subscription
) {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented"
method isSyncing*(provider: Provider): Future[bool] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented"
proc replay*(
provider: Provider, tx: Transaction, blockNumber: UInt256
) {.async: (raises: [ProviderError, CancelledError]).} =
# Replay transaction at block. Useful for fetching revert reasons, which will
# be present in the raised error message. The replayed block number should
# include the state of the chain in the block previous to the block in which
# the transaction was mined. This means that transactions that were mined in
# the same block BEFORE this transaction will not have their state transitions
# included in the replay.
# More information: https://snakecharmers.ethereum.org/web3py-revert-reason-parsing/
trace "replaying transaction", gasLimit = tx.gasLimit, tx = $tx
discard await provider.call(tx, BlockTag.init(blockNumber))
proc ensureSuccess(
provider: Provider, receipt: TransactionReceipt
) {.async: (raises: [ProviderError, CancelledError]).} =
## If the receipt.status is Failed, the tx is replayed to obtain a revert
## reason, after which a ProviderError with the revert reason is raised.
## If no revert reason was obtained
# TODO: handle TransactionStatus.Invalid?
if receipt.status != TransactionStatus.Failure:
return
without blockNumber =? receipt.blockNumber and
pastTx =? await provider.getTransaction(receipt.transactionHash):
raiseProviderError("Transaction reverted with unknown reason")
try:
await provider.replay(pastTx.toTransaction, blockNumber)
raiseProviderError("Transaction reverted with unknown reason")
except ProviderError as error:
raise error
proc confirm*(
tx: TransactionResponse,
confirmations = EthersDefaultConfirmations, confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt] timeout = EthersReceiptTimeoutBlks):
{.async: (raises: [CancelledError, ProviderError, SubscriptionError, EthersError]).} = Future[TransactionReceipt]
{.async, upraises: [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).
## includes the block in which the transaction was mined.
## A timeout, in blocks, can be specified that will raise an error if too many ## A timeout, in blocks, can be specified that will raise an error if too many
## blocks have passed without the tx having been mined. ## blocks have passed without the tx having been mined.
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 onBlockNumber(number: UInt256) =
try:
let number = await tx.provider.getBlockNumber()
if number > blockNumber:
blockNumber = number blockNumber = number
blockEvent.fire() blockEvent.fire()
except ProviderError, CancelledError:
# there's nothing we can do here
discard
proc onBlock(blckResult: ?!Block) = proc onBlock(blck: Block) =
blockSubscriptionResult = blckResult if number =? blck.number:
onBlockNumber(number)
if blckResult.isErr: onBlockNumber(await tx.provider.getBlockNumber())
blockEvent.fire()
return
# ignore block parameter; hardhat may call this with pending blocks
asyncSpawn updateBlockNumber()
await updateBlockNumber()
let subscription = await tx.provider.subscribe(onBlock) let subscription = await tx.provider.subscribe(onBlock)
let finish = blockNumber + timeout.u256 let finish = blockNumber + timeout.u256
@ -271,16 +145,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")
@ -293,30 +157,18 @@ proc confirm*(
if txBlockNumber + confirmations.u256 <= blockNumber + 1: if txBlockNumber + confirmations.u256 <= blockNumber + 1:
await subscription.unsubscribe() await subscription.unsubscribe()
await tx.provider.ensureSuccess(receipt)
return receipt return receipt
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 let txResp = await tx
return await txResp.confirm(confirmations, timeout) 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*( method close*(provider: Provider) {.async, base.} =
provider: Provider
) {.base, async: (raises: [ProviderError, CancelledError]).} =
discard discard

View File

@ -1,66 +1,67 @@
import std/json
import std/tables import std/tables
import std/uri import std/uri
import pkg/chronicles
import pkg/eth/common/eth_types except Block, Log, Address, Transaction
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/json_rpc/errors import pkg/json_rpc/errors
import pkg/serde
import ../basics import ../basics
import ../provider import ../provider
import ../signer import ../signer
import ./jsonrpc/rpccalls import ./jsonrpc/rpccalls
import ./jsonrpc/conversions import ./jsonrpc/conversions
import ./jsonrpc/subscriptions import ./jsonrpc/subscriptions
import ./jsonrpc/errors
export json
export basics export basics
export provider export provider
export chronicles
export errors.JsonRpcProviderError
export subscriptions
{.push raises: [].} push: {.upraises: [].}
logScope:
topics = "ethers jsonrpc"
type 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 JsonRpcSigner* = ref object of Signer
provider: JsonRpcProvider
address: ?Address
JsonRpcProviderError* = object of ProviderError
JsonRpcSubscription* = ref object of Subscription JsonRpcSubscription* = ref object of Subscription
subscriptions: JsonRpcSubscriptions subscriptions: JsonRpcSubscriptions
id: JsonNode id: JsonNode
# Signer proc raiseProviderError(message: string) {.upraises: [JsonRpcProviderError].} =
JsonRpcSigner* = ref object of Signer var message = message
provider: JsonRpcProvider try:
address: ?Address message = parseJson(message){"message"}.getStr
JsonRpcSignerError* = object of SignerError except Exception:
discard
raise newException(JsonRpcProviderError, message)
template convertError(body) =
try:
body
except JsonRpcError as error:
raiseProviderError(error.msg)
# Catch all ValueErrors for now, at least until JsonRpcError is actually
# raised. PR created: https://github.com/status-im/nim-json-rpc/pull/151
except ValueError as error:
raiseProviderError(error.msg)
# Provider # Provider
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")]
proc new*( proc new*(_: type JsonRpcProvider,
_: type JsonRpcProvider,
url=defaultUrl, url=defaultUrl,
pollingInterval=defaultPollingInterval, pollingInterval=defaultPollingInterval): JsonRpcProvider =
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.} =
convertError:
case parseUri(url).scheme case parseUri(url).scheme
of "ws", "wss": of "ws", "wss":
let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders) let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders)
@ -73,49 +74,28 @@ proc new*(
client = http client = http
subscriptions = JsonRpcSubscriptions.new(http, subscriptions = JsonRpcSubscriptions.new(http,
pollingInterval = pollingInterval) pollingInterval = pollingInterval)
subscriptions.start()
proc awaitClient(): Future[RpcClient] {. proc awaitClient: Future[RpcClient] {.async.} =
async: (raises: [JsonRpcProviderError, CancelledError])
.} =
convertError: convertError:
await initialized await initialized
return client return client
proc awaitSubscriptions(): Future[JsonRpcSubscriptions] {. proc awaitSubscriptions: Future[JsonRpcSubscriptions] {.async.} =
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) JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions())
proc callImpl( proc send*(provider: JsonRpcProvider,
client: RpcClient, call: string, args: JsonNode call: string,
): Future[JsonNode] {.async: (raises: [JsonRpcProviderError, CancelledError]).} = arguments: seq[JsonNode] = @[]): Future[JsonNode] {.async.} =
try:
let response = await client.call(call, %args)
without json =? JsonNode.fromJson(response.string), 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
proc send*(
provider: JsonRpcProvider, call: string, arguments: seq[JsonNode] = @[]
): Future[JsonNode] {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.callImpl(call, %arguments) return await client.call(call, %arguments)
proc listAccounts*( proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] {.async.} =
provider: JsonRpcProvider
): 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()
@ -126,74 +106,50 @@ proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner =
proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner = 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): Future[UInt256] {.async.} =
provider: JsonRpcProvider
): 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,
provider: JsonRpcProvider, tag: BlockTag tag: BlockTag): Future[?Block] {.async.} =
): Future[?Block] {.async: (raises: [ProviderError, CancelledError]).} =
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,
provider: JsonRpcProvider, tx: Transaction, blockTag = BlockTag.latest tx: Transaction,
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} = blockTag = BlockTag.latest): Future[seq[byte]] {.async.} =
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): Future[UInt256] {.async.} =
provider: JsonRpcProvider
): 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*( method getTransactionCount*(provider: JsonRpcProvider,
provider: JsonRpcProvider address: Address,
): Future[UInt256] {.async: (raises: [CancelledError]).} = blockTag = BlockTag.latest):
try: Future[UInt256] {.async.} =
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*(
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
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 getTransactionReceipt*(provider: JsonRpcProvider,
provider: JsonRpcProvider, txHash: TransactionHash txHash: TransactionHash):
): Future[?PastTransaction] {.async: (raises: [ProviderError, CancelledError]).} = Future[?TransactionReceipt] {.async.} =
convertError:
let client = await provider.client
return await client.eth_getTransactionByHash(txHash)
method getTransactionReceipt*(
provider: JsonRpcProvider, txHash: TransactionHash
): Future[?TransactionReceipt] {.async: (raises: [ProviderError, CancelledError]).} =
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,
provider: JsonRpcProvider, filter: EventFilter filter: EventFilter):
): Future[seq[Log]] {.async: (raises: [ProviderError, CancelledError]).} = Future[seq[Log]] {.async.} =
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))
@ -202,43 +158,26 @@ method getLogs*(
var logs: seq[Log] = @[] var logs: seq[Log] = @[]
for logJson in logsJson.getElems: for logJson in logsJson.getElems:
if log =? Log.fromJson(logJson): if log =? Log.fromJson(logJson).catch:
logs.add log logs.add log
return logs return logs
method estimateGas*( method estimateGas*(provider: JsonRpcProvider,
provider: JsonRpcProvider, transaction: Transaction): Future[UInt256] {.async.} =
transaction: Transaction,
blockTag = BlockTag.latest,
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
try:
convertError: convertError:
let client = await provider.client let client = await provider.client
return await client.eth_estimateGas(transaction, blockTag) return await client.eth_estimateGas(transaction)
except ProviderError as error:
raise (ref EstimateGasError)(
msg: "Estimate gas failed: " & error.msg,
data: error.data,
transaction: transaction,
parent: error,
)
method getChainId*( method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
provider: JsonRpcProvider
): 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]): Future[TransactionResponse] {.async.} =
provider: JsonRpcProvider, rawTransaction: seq[byte]
): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError]).} =
convertError: convertError:
let let
client = await provider.client client = await provider.client
@ -246,41 +185,30 @@ method sendTransaction*(
return TransactionResponse(hash: hash, provider: provider) return TransactionResponse(hash: hash, provider: provider)
method subscribe*( method subscribe*(provider: JsonRpcProvider,
provider: JsonRpcProvider, filter: EventFilter, onLog: LogHandler filter: EventFilter,
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} = onLog: LogHandler):
Future[Subscription] {.async.} =
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,
provider: JsonRpcProvider, onBlock: BlockHandler onBlock: BlockHandler):
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} = Future[Subscription] {.async.} =
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) {.async.} =
subscription: JsonRpcSubscription
) {.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 close*(provider: JsonRpcProvider) {.async.} =
provider: JsonRpcProvider
): Future[bool] {.async: (raises: [ProviderError, CancelledError]).} =
let response = await provider.send("eth_syncing")
if response.kind == JsonNodeKind.JObject:
return true
return response.getBool()
method close*(
provider: JsonRpcProvider
) {.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
@ -289,33 +217,10 @@ method close*(
# Signer # Signer
proc raiseJsonRpcSignerError( method provider*(signer: JsonRpcSigner): Provider =
message: string) {.raises: [JsonRpcSignerError].} =
var message = message
if json =? JsonNode.fromJson(message):
if "message" in json:
message = json{"message"}.getStr
raise newException(JsonRpcSignerError, message)
template convertSignerError(body) =
try:
body
except CancelledError as error:
raise error
except JsonRpcError as error:
raiseJsonRpcSignerError(error.msg)
except CatchableError as error:
raise newException(JsonRpcSignerError, error.msg)
method provider*(signer: JsonRpcSigner): Provider
{.gcsafe, raises: [SignerError].} =
signer.provider signer.provider
method getAddress*( method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
signer: JsonRpcSigner
): Future[Address] {.async: (raises: [ProviderError, SignerError, CancelledError]).} =
if address =? signer.address: if address =? signer.address:
return address return address
@ -323,21 +228,17 @@ method getAddress*(
if accounts.len > 0: if accounts.len > 0:
return accounts[0] return accounts[0]
raiseJsonRpcSignerError "no address found" raiseProviderError "no address found"
method signMessage*( method signMessage*(signer: JsonRpcSigner,
signer: JsonRpcSigner, message: seq[byte] message: seq[byte]): Future[seq[byte]] {.async.} =
): Future[seq[byte]] {.async: (raises: [SignerError, CancelledError]).} = convertError:
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.eth_sign(address, message)
method sendTransaction*( method sendTransaction*(signer: JsonRpcSigner,
signer: JsonRpcSigner, transaction: Transaction transaction: Transaction): Future[TransactionResponse] {.async.} =
): Future[TransactionResponse] {.
async: (raises: [SignerError, ProviderError, CancelledError])
.} =
convertError: convertError:
let let
client = await signer.provider.client client = await signer.provider.client

View File

@ -1,62 +1,56 @@
import std/strformat import std/json
import std/strutils import std/strutils
import pkg/chronicles except fromJson, `%`, `%*`, toJson import pkg/json_rpc/jsonmarshal
import pkg/json_rpc/jsonmarshal except toJson
import pkg/questionable/results
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../basics import ../../basics
import ../../transaction import ../../transaction
import ../../blocktag import ../../blocktag
import ../../provider import ../../provider
export jsonmarshal except toJson export jsonmarshal
export serde
export chronicles except fromJson, `%`, `%*`, toJson
{.push raises: [].} func fromJson*(T: type, json: JsonNode, name = ""): T =
fromJson(json, name, result)
proc getOrRaise*[T, E](self: ?!T, exc: typedesc[E]): T {.raises: [E].} = # byte sequence
let val = self.valueOr:
raise newException(E, self.error.msg)
val
template mapFailure*[T, V, E]( func `%`*(bytes: seq[byte]): JsonNode =
exp: Result[T, V], %("0x" & bytes.toHex)
exc: typedesc[E],
): Result[T, ref CatchableError] =
## Convert `Result[T, E]` to `Result[E, ref CatchableError]`
##
exp.mapErr(proc (e: V): ref CatchableError = (ref exc)(msg: e.msg)) func fromJson*(json: JsonNode, name: string, result: var seq[byte]) =
result = hexToSeqByte(json.getStr())
# byte arrays
func `%`*[N](bytes: array[N, byte]): JsonNode =
%("0x" & bytes.toHex)
func fromJson*[N](json: JsonNode, name: string, result: var array[N, byte]) =
hexToByteArray(json.getStr(), result)
# Address # Address
func `%`*(address: Address): JsonNode = func `%`*(address: Address): JsonNode =
%($address) %($address)
func fromJson(_: type Address, json: JsonNode): ?!Address = func fromJson*(json: JsonNode, name: string, result: var Address) =
expectJsonKind(Address, JString, json) if address =? Address.init(json.getStr()):
without address =? Address.init(json.getStr), error: result = address
return failure newException(SerializationError, else:
"Failed to convert '" & $json & "' to Address: " & error.msg) raise newException(ValueError, "\"" & name & "\"is not an Address")
success address
# UInt256 # UInt256
func `%`*(integer: UInt256): JsonNode = func `%`*(integer: UInt256): JsonNode =
%("0x" & toHex(integer)) %("0x" & toHex(integer))
func fromJson*(json: JsonNode, name: string, result: var UInt256) =
result = UInt256.fromHex(json.getStr())
# Transaction # Transaction
# TODO: add option that ignores none Option[T]
# TODO: add name option (gasLimit => gas, sender => from)
func `%`*(transaction: Transaction): JsonNode = func `%`*(transaction: Transaction): JsonNode =
result = %*{ result = %{ "to": %transaction.to, "data": %transaction.data }
"to": transaction.to,
"data": %transaction.data,
"value": %transaction.value
}
if sender =? transaction.sender: if sender =? transaction.sender:
result["from"] = %sender result["from"] = %sender
if nonce =? transaction.nonce: if nonce =? transaction.nonce:
@ -70,53 +64,23 @@ func `%`*(transaction: Transaction): JsonNode =
# BlockTag # BlockTag
func `%`*(tag: BlockTag): JsonNode = func `%`*(blockTag: BlockTag): JsonNode =
% $tag %($blockTag)
func fromJson*(_: type BlockTag, json: JsonNode): ?!BlockTag = # Log
expectJsonKind(BlockTag, JString, json)
let jsonVal = json.getStr
if jsonVal.len >= 2 and jsonVal[0..1].toLowerAscii == "0x":
without blkNum =? UInt256.fromHex(jsonVal).catch, error:
return BlockTag.failure error.msg
return success BlockTag.init(blkNum)
case jsonVal: func fromJson*(json: JsonNode, name: string, result: var Log) =
of "earliest": return success BlockTag.earliest var data: seq[byte]
of "latest": return success BlockTag.latest var topics: seq[Topic]
of "pending": return success BlockTag.pending fromJson(json["data"], "data", data)
else: return failure newException(SerializationError, fromJson(json["topics"], "topics", topics)
"Failed to convert '" & $json & result = Log(data: data, topics: topics)
"' to BlockTag: must be one of 'earliest', 'latest', 'pending'")
# TransactionStatus | TransactionType # TransactionStatus
type TransactionEnums = TransactionStatus | TransactionType
func `%`*(e: TransactionEnums): JsonNode = func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) =
% ("0x" & e.int8.toHex(1)) let val = fromHex[int](json.getStr)
result = TransactionStatus(val)
proc fromJson*( func `%`*(status: TransactionStatus): JsonNode =
T: type TransactionEnums, %(status.int.toHex)
json: JsonNode
): ?!T =
expectJsonKind(string, JString, json)
let integer = ? fromHex[int](json.str).catch.mapFailure(SerializationError)
success T(integer)
## Generic conversions to use nim-json instead of nim-json-serialization for
## json rpc serialization purposes
## writeValue => `%`
## readValue => fromJson
proc writeValue*[T: not JsonNode](
writer: var JsonWriter[JrpcConv],
value: T) {.raises:[IOError].} =
writer.writeValue(%value)
proc readValue*[T: not JsonNode](
r: var JsonReader[JrpcConv],
result: var T) {.raises: [SerializationError, IOError].} =
var json = r.readValue(JsonNode)
result = T.fromJson(json).getOrRaise(SerializationError)

View File

@ -1,49 +0,0 @@
import std/strutils
import pkg/stew/byteutils
import ../../basics
import ../../errors
import ../../provider
import ./conversions
export errors
{.push raises:[].}
type JsonRpcProviderError* = object of ProviderError
func extractErrorData(json: JsonNode): ?seq[byte] =
if json.kind == JObject:
if "message" in json and "data" in json:
let message = json{"message"}.getStr()
let hex = json{"data"}.getStr()
if "reverted" in message and hex.startsWith("0x"):
if data =? hexToSeqByte(hex).catch:
return some data
for key in json.keys:
if data =? extractErrorData(json{key}):
return some data
func new*(_: type JsonRpcProviderError, json: JsonNode): ref JsonRpcProviderError =
let error = (ref JsonRpcProviderError)()
if "message" in json:
error.msg = json{"message"}.getStr
error.data = extractErrorData(json)
error
proc raiseJsonRpcProviderError*(
message: string) {.raises: [JsonRpcProviderError].} =
if json =? JsonNode.fromJson(message):
raise JsonRpcProviderError.new(json)
else:
raise newException(JsonRpcProviderError, message)
template convertError*(body) =
try:
body
except CancelledError as error:
raise error
except JsonRpcError as error:
raiseJsonRpcProviderError(error.msg)
except CatchableError as error:
raiseJsonRpcProviderError(error.msg)

View File

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

View File

@ -1,19 +1,18 @@
proc net_version(): string proc net_version(): string
proc personal_sign(message: seq[byte], account: Address): seq[byte]
proc eth_accounts: seq[Address] proc eth_accounts: seq[Address]
proc eth_blockNumber: UInt256 proc eth_blockNumber: UInt256
proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte] proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte]
proc eth_gasPrice(): UInt256 proc eth_gasPrice(): UInt256
proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode
proc eth_getTransactionByHash(hash: TransactionHash): ?PastTransaction
proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256 proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256
proc eth_estimateGas(transaction: Transaction, blockTag: BlockTag): UInt256 proc eth_estimateGas(transaction: Transaction): UInt256
proc eth_chainId(): UInt256 proc eth_chainId(): UInt256
proc eth_sendTransaction(transaction: Transaction): TransactionHash proc eth_sendTransaction(transaction: Transaction): TransactionHash
proc eth_sendRawTransaction(data: seq[byte]): TransactionHash proc eth_sendRawTransaction(data: seq[byte]): TransactionHash
proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt
proc eth_sign(account: Address, message: seq[byte]): seq[byte]
proc eth_subscribe(name: string, filter: EventFilter): JsonNode proc eth_subscribe(name: string, filter: EventFilter): JsonNode
proc eth_subscribe(name: string): JsonNode proc eth_subscribe(name: string): JsonNode
proc eth_unsubscribe(id: JsonNode): bool proc eth_unsubscribe(id: JsonNode): bool
@ -21,4 +20,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,184 +1,74 @@
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 ../../basics import ../../basics
import ../../errors
import ../../provider import ../../provider
include ../../nimshims/hashes
import ./rpccalls import ./rpccalls
import ./conversions import ./conversions
import ./looping
export serde
type type
JsonRpcSubscriptions* = ref object of RootObj JsonRpcSubscriptions* = ref object of RootObj
client: RpcClient client: RpcClient
callbacks: Table[JsonNode, SubscriptionCallback] callbacks: Table[JsonNode, SubscriptionCallback]
methodHandlers: Table[string, MethodHandler] SubscriptionCallback = proc(id, arguments: JsonNode) {.gcsafe, upraises:[].}
# Used by both PollingSubscriptions and WebsocketSubscriptions to store
# subscription filters so the subscriptions can be recreated. With
# PollingSubscriptions, the RPC node might prune/forget about them, and with
# WebsocketSubscriptions, when using hardhat, subscriptions are dropped after 5
# minutes.
logFilters: Table[JsonNode, EventFilter]
MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].}
SubscriptionCallback = proc(id: JsonNode, arguments: ?!JsonNode) {.gcsafe, raises:[].}
{.push raises:[].}
template convertErrorsToSubscriptionError(body) =
try:
body
except CancelledError as error:
raise error
except CatchableError as error:
raise error.toErr(SubscriptionError)
template `or`(a: JsonNode, b: typed): JsonNode =
if a.isNil: b else: a
func start*(subscriptions: JsonRpcSubscriptions) =
subscriptions.client.onProcessMessage =
proc(client: RpcClient,
line: string): Result[bool, string] {.gcsafe, raises: [].} =
if json =? JsonNode.fromJson(line):
if "method" in json:
let methodName = json{"method"}.getStr()
if methodName in subscriptions.methodHandlers:
let handler = subscriptions.methodHandlers.getOrDefault(methodName)
if not handler.isNil:
handler(json{"params"} or newJArray())
# false = do not continue processing message using json_rpc's
# default processing handler
return ok false
# true = continue processing message using json_rpc's default message handler
return ok true
proc setMethodHandler(
subscriptions: JsonRpcSubscriptions,
`method`: string,
handler: MethodHandler
) =
subscriptions.methodHandlers[`method`] = handler
method subscribeBlocks*(subscriptions: JsonRpcSubscriptions, 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 subscriptions.callbacks.hasKey(id):
return subscriptions.callbacks[id].some subscriptions.callbacks[id].some
except: discard else:
SubscriptionCallback.none
except Exception:
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)
let subscriptions = WebSocketSubscriptions(client: client, resubscribeInterval: resubscribeInterval) proc subscriptionHandler(arguments: JsonNode) {.upraises:[].} =
if id =? arguments["subscription"].catch and
proc subscriptionHandler(arguments: JsonNode) {.raises:[].} = callback =? subscriptions.getCallback(id):
let id = arguments{"subscription"} or newJString("") callback(id, arguments)
if callback =? subscriptions.getCallback(id): client.setMethodHandler("eth_subscription", subscriptionHandler)
callback(id, success(arguments))
subscriptions.setMethodHandler("eth_subscription", subscriptionHandler)
if resubscribeInterval > 0:
if resubscribeInterval >= 300:
warn "Resubscription interval greater than 300 seconds is useless for hardhat workaround", resubscribeInterval = resubscribeInterval
subscriptions.resubscribeFut = resubscribeWebsocketEventsOnTimeout(subscriptions)
subscriptions 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) =
without arguments =? argumentsResult, error: if blck =? Block.fromJson(arguments["result"]).catch:
onBlock(failure(Block, error.toErr(SubscriptionError))) onBlock(blck)
return
let res = Block.fromJson(arguments{"result"}).mapFailure(SubscriptionError)
onBlock(res)
convertErrorsToSubscriptionError:
withLock(subscriptions):
let id = await subscriptions.client.eth_subscribe("newHeads") let id = await subscriptions.client.eth_subscribe("newHeads")
subscriptions.callbacks[id] = callback subscriptions.callbacks[id] = callback
return id return id
@ -187,193 +77,92 @@ 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"]).catch:
onLog(failure(Log, error.toErr(SubscriptionError))) onLog(log)
return
let res = Log.fromJson(arguments{"result"}).mapFailure(SubscriptionError)
onLog(res)
convertErrorsToSubscriptionError:
withLock(subscriptions):
let id = await subscriptions.client.eth_subscribe("logs", filter) let id = await subscriptions.client.eth_subscribe("logs", filter)
subscriptions.callbacks[id] = callback subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
return id return id
method unsubscribe*(subscriptions: WebSocketSubscriptions, method unsubscribe(subscriptions: WebSocketSubscriptions,
id: JsonNode) id: JsonNode)
{.async: (raises: [CancelledError]).} = {.async.} =
try:
withLock(subscriptions):
subscriptions.callbacks.del(id) subscriptions.callbacks.del(id)
discard await subscriptions.client.eth_unsubscribe(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
type type
PollingSubscriptions* = ref object of JsonRpcSubscriptions PollingSubscriptions = ref object of JsonRpcSubscriptions
polling: Future[void] polling: Future[void]
# Used when filters are recreated to translate from the id that user
# originally got returned to new filter id
subscriptionMapping: Table[JsonNode, JsonNode]
proc new*(_: type JsonRpcSubscriptions, proc new*(_: type JsonRpcSubscriptions,
client: RpcHttpClient, client: RpcHttpClient,
pollingInterval = 4.seconds): JsonRpcSubscriptions = pollingInterval = 4.seconds): JsonRpcSubscriptions =
let subscriptions = PollingSubscriptions(client: client) let subscriptions = PollingSubscriptions(client: client)
proc resubscribe(id: JsonNode): Future[?!void] {.async: (raises: [CancelledError]).} = proc getChanges(id: JsonNode): Future[JsonNode] {.async.} =
try: try:
var newId: JsonNode return await subscriptions.client.eth_getFilterChanges(id)
# Log filters are stored in logFilters, block filters are not persisted except CatchableError:
# there is they do not need any specific data for their recreation. return newJArray()
# We use this to determine if the filter was log or block filter here.
if subscriptions.logFilters.hasKey(id):
let filter = subscriptions.logFilters[id]
newId = await subscriptions.client.eth_newFilter(filter)
else:
newId = await subscriptions.client.eth_newBlockFilter()
subscriptions.subscriptionMapping[id] = newId
except CancelledError as e:
raise e
except CatchableError as e:
return failure(void, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
return success() proc poll(id: JsonNode) {.async.} =
for change in await getChanges(id):
if callback =? subscriptions.getCallback(id):
callback(id, change)
proc getChanges(id: JsonNode): Future[?!JsonNode] {.async: (raises: [CancelledError]).} = proc poll {.async.} =
if mappedId =? subscriptions.subscriptionMapping.?[id]: untilCancelled:
try:
let changes = await subscriptions.client.eth_getFilterChanges(mappedId)
if changes.kind == JArray:
return success(changes)
except JsonRpcError as e:
if error =? (await resubscribe(id)).errorOption:
return failure(JsonNode, error)
# TODO: we could still miss some events between losing the subscription
# and resubscribing. We should probably adopt a strategy like ethers.js,
# whereby we keep track of the latest block number that we've seen
# filter changes for:
# https://github.com/ethers-io/ethers.js/blob/f97b92bbb1bde22fcc44100af78d7f31602863ab/packages/providers/src.ts/base-provider.ts#L977
if not ("filter not found" in e.msg):
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
except CancelledError as e:
raise e
except SubscriptionError as e:
return failure(JsonNode, e)
except CatchableError as e:
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
return success(newJArray())
proc poll(id: JsonNode) {.async: (raises: [CancelledError]).} =
without callback =? subscriptions.getCallback(id):
return
without changes =? (await getChanges(id)), error:
callback(id, failure(JsonNode, error))
return
for change in changes:
callback(id, success(change))
proc poll {.async: (raises: []).} =
try:
while true:
for id in toSeq subscriptions.callbacks.keys: for id in toSeq subscriptions.callbacks.keys:
await poll(id) await poll(id)
await sleepAsync(pollingInterval) await sleepAsync(pollingInterval)
except CancelledError:
discard
subscriptions.polling = poll() subscriptions.polling = poll()
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:[].} = proc callback(id, change: JsonNode) =
without change =? changeResult, e: if hash =? BlockHash.fromJson(change).catch:
onBlock(failure(Block, e.toErr(SubscriptionError)))
return
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
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).catch:
onLog(failure(Log, error.toErr(SubscriptionError))) onLog(log)
return
let res = Log.fromJson(arguments).mapFailure(SubscriptionError)
onLog(res)
convertErrorsToSubscriptionError:
let id = await subscriptions.client.eth_newFilter(filter) let id = await subscriptions.client.eth_newFilter(filter)
subscriptions.callbacks[id] = callback subscriptions.callbacks[id] = callback
subscriptions.logFilters[id] = filter
subscriptions.subscriptionMapping[id] = id
return id return id
method unsubscribe*(subscriptions: PollingSubscriptions, method unsubscribe(subscriptions: PollingSubscriptions,
id: JsonNode) id: JsonNode)
{.async: (raises: [CancelledError]).} = {.async.} =
try:
subscriptions.logFilters.del(id)
subscriptions.callbacks.del(id) subscriptions.callbacks.del(id)
if sub =? subscriptions.subscriptionMapping.?[id]: discard await subscriptions.client.eth_uninstallFilter(id)
subscriptions.subscriptionMapping.del(id)
discard await subscriptions.client.eth_uninstallFilter(sub)
except CancelledError as e:
raise e
except CatchableError:
# Ignore if uninstallation of the filter fails. If it's the last step in our
# cleanup, then filter changes for this filter will no longer be polled so
# if the filter continues to live on in geth for whatever reason then it
# doesn't matter.
discard

View File

@ -1,125 +1,51 @@
import pkg/questionable
import pkg/chronicles
import ./basics import ./basics
import ./errors
import ./provider import ./provider
export basics export basics
export errors
{.push raises: [].} type Signer* = ref object of RootObj
type SignerError* = object of EthersError
type template raiseSignerError(message: string) =
Signer* = ref object of RootObj raise newException(SignerError, message)
populateLock: AsyncLock
template raiseSignerError*(message: string, parent: ref CatchableError = nil) = method provider*(signer: Signer): Provider {.base, gcsafe.} =
raise newException(SignerError, message, parent)
template convertError(body) =
try:
body
except CancelledError as error:
raise error
except ProviderError as error:
raise error # do not convert provider errors
except CatchableError as error:
raiseSignerError(error.msg)
method provider*(
signer: Signer): Provider {.base, gcsafe, raises: [SignerError].} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getAddress*( method getAddress*(signer: Signer): Future[Address] {.base, gcsafe.} =
signer: Signer
): Future[Address] {.
base, async: (raises: [ProviderError, SignerError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method signMessage*( method signMessage*(signer: Signer,
signer: Signer, message: seq[byte] message: seq[byte]): Future[seq[byte]] {.base, async.} =
): Future[seq[byte]] {.base, async: (raises: [SignerError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*( method sendTransaction*(signer: Signer,
signer: Signer, transaction: Transaction transaction: Transaction): Future[TransactionResponse] {.base, async.} =
): Future[TransactionResponse] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getGasPrice*( method getGasPrice*(signer: Signer): Future[UInt256] {.base, gcsafe.} =
signer: Signer signer.provider.getGasPrice()
): Future[UInt256] {.
base, async: (raises: [ProviderError, SignerError, CancelledError])
.} =
return await signer.provider.getGasPrice()
method getMaxPriorityFeePerGas*( method getTransactionCount*(signer: Signer,
signer: Signer blockTag = BlockTag.latest):
): Future[UInt256] {.async: (raises: [SignerError, CancelledError]).} = Future[UInt256] {.base, async.} =
return await signer.provider.getMaxPriorityFeePerGas()
method getTransactionCount*(
signer: Signer, blockTag = BlockTag.latest
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
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,
signer: Signer, transaction: Transaction, blockTag = BlockTag.latest transaction: Transaction): Future[UInt256] {.base, async.} =
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
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)
method getChainId*( method getChainId*(signer: Signer): Future[UInt256] {.base, gcsafe.} =
signer: Signer signer.provider.getChainId()
): Future[UInt256] {.
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
return await signer.provider.getChainId()
method getNonce( method populateTransaction*(signer: Signer,
signer: Signer transaction: Transaction):
): Future[UInt256] {. Future[Transaction] {.base, async.} =
base, async: (raises: [SignerError, ProviderError, CancelledError])
.} =
return await signer.getTransactionCount(BlockTag.pending)
template withLock*(signer: Signer, body: untyped) = if sender =? transaction.sender and sender != await signer.getAddress():
if signer.populateLock.isNil:
signer.populateLock = newAsyncLock()
await signer.populateLock.acquire()
try:
body
finally:
try:
signer.populateLock.release()
except AsyncLockError as e:
raiseSignerError e.msg, e
method populateTransaction*(
signer: Signer,
transaction: Transaction): Future[Transaction]
{.base, async: (raises: [CancelledError, ProviderError, SignerError]).} =
## Populates a transaction with sender, chainId, gasPrice, nonce, and gasLimit.
## NOTE: to avoid async concurrency issues, this routine should be called with
## a lock if it is followed by sendTransaction. For reference, see the `send`
## function in contract.nim.
var address: Address
convertError:
address = await signer.getAddress()
if sender =? transaction.sender and sender != address:
raiseSignerError("from address mismatch") raiseSignerError("from address mismatch")
if chainId =? transaction.chainId and chainId != await signer.getChainId(): if chainId =? transaction.chainId and chainId != await signer.getChainId():
raiseSignerError("chain id mismatch") raiseSignerError("chain id mismatch")
@ -127,68 +53,14 @@ method populateTransaction*(
var populated = transaction var populated = transaction
if transaction.sender.isNone: if transaction.sender.isNone:
populated.sender = some(address) populated.sender = some(await signer.getAddress())
if transaction.nonce.isNone:
populated.nonce = some(await signer.getTransactionCount(BlockTag.pending))
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:
# when both nonce and gasLimit are not populated, we must ensure getNonce is
# followed by an estimateGas so we can determine if there was an error. If
# there is an error, the nonce must be decreased to prevent nonce gaps and
# stuck transactions
populated.nonce = some(await signer.getNonce())
try:
populated.gasLimit = some(await signer.estimateGas(populated, BlockTag.pending))
except EstimateGasError as e:
raise e
except ProviderError as e:
raiseSignerError(e.msg)
else:
if transaction.nonce.isNone:
let nonce = await signer.getNonce()
populated.nonce = some nonce
if transaction.gasLimit.isNone: if transaction.gasLimit.isNone:
populated.gasLimit = some(await signer.estimateGas(populated, BlockTag.pending)) populated.gasLimit = some(await signer.estimateGas(populated))
doAssert populated.nonce.isSome, "nonce not populated!"
return populated return populated
method cancelTransaction*(
signer: Signer,
tx: Transaction
): Future[TransactionResponse] {.base, async: (raises: [SignerError, CancelledError, ProviderError]).} =
# cancels a transaction by sending with a 0-valued transaction to ourselves
# with the failed tx's nonce
without sender =? tx.sender:
raiseSignerError "transaction must have sender"
without nonce =? tx.nonce:
raiseSignerError "transaction must have nonce"
withLock(signer):
convertError:
var cancelTx = Transaction(to: sender, value: 0.u256, nonce: some nonce)
cancelTx = await signer.populateTransaction(cancelTx)
return await signer.sendTransaction(cancelTx)

View File

@ -1,3 +0,0 @@
import ../providers/jsonrpc
export provider, getAddress, signMessage, sendTransaction

View File

@ -1,89 +0,0 @@
import eth/keys
import ../basics
import ../provider
import ../transaction
import ../signer
import ./wallet/error
import ./wallet/signing
export keys
export WalletError
export signing
{.push raises: [].}
var rng {.threadvar.}: ref HmacDrbgContext
proc getRng: ref HmacDrbgContext =
if rng.isNil:
rng = newRng()
rng
type Wallet* = ref object of Signer
privateKey*: PrivateKey
publicKey*: PublicKey
address*: Address
provider*: ?Provider
proc new*(_: type Wallet, privateKey: PrivateKey): Wallet =
let publicKey = privateKey.toPublicKey()
let address = Address(publicKey.toCanonicalAddress())
Wallet(privateKey: privateKey, publicKey: publicKey, address: address)
proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet =
let wallet = Wallet.new(privateKey)
wallet.provider = some provider
wallet
proc new*(_: type Wallet, privateKey: string): ?!Wallet =
let keyResult = PrivateKey.fromHex(privateKey)
if keyResult.isErr:
return failure newException(WalletError, "invalid key: " & $keyResult.error)
success Wallet.new(keyResult.get())
proc new*(_: type Wallet, privateKey: string, provider: Provider): ?!Wallet =
let wallet = ? Wallet.new(privateKey)
wallet.provider = some provider
success wallet
proc connect*(wallet: Wallet, provider: Provider) =
wallet.provider = some provider
proc createRandom*(_: type Wallet): Wallet =
result = Wallet()
result.privateKey = PrivateKey.random(getRng()[])
result.publicKey = result.privateKey.toPublicKey()
result.address = Address(result.publicKey.toCanonicalAddress())
proc createRandom*(_: type Wallet, provider: Provider): Wallet =
result = Wallet()
result.privateKey = PrivateKey.random(getRng()[])
result.publicKey = result.privateKey.toPublicKey()
result.address = Address(result.publicKey.toCanonicalAddress())
result.provider = some provider
method provider*(wallet: Wallet): Provider {.gcsafe, raises: [SignerError].} =
without provider =? wallet.provider:
raiseWalletError "Wallet has no provider"
provider
method getAddress*(
wallet: Wallet): Future[Address]
{.async: (raises:[ProviderError, SignerError, CancelledError]).} =
return wallet.address
proc signTransaction*(wallet: Wallet,
transaction: Transaction): Future[seq[byte]] {.async: (raises:[WalletError]).} =
if sender =? transaction.sender and sender != wallet.address:
raiseWalletError "from address mismatch"
return wallet.privateKey.sign(transaction)
method sendTransaction*(
wallet: Wallet,
transaction: Transaction): Future[TransactionResponse]
{.async: (raises:[SignerError, ProviderError, CancelledError]).} =
let signed = await signTransaction(wallet, transaction)
return await provider(wallet).sendTransaction(signed)

View File

@ -1,15 +0,0 @@
import ../../signer
type
WalletError* = object of SignerError
func raiseWalletError*(message: string) {.raises: [WalletError].}=
raise newException(WalletError, message)
template convertError*(body) =
try:
body
except CancelledError as error:
raise error
except CatchableError as error:
raiseWalletError(error.msg)

View File

@ -1,9 +1,8 @@
import std/strutils import std/strutils
import ./provider import ./provider
import ./signer
proc revertReason*(emsg: string): string = proc revertReason*(e: ref ProviderError): string =
var msg = emsg var msg = e.msg
const revertPrefixes = @[ const revertPrefixes = @[
# hardhat # hardhat
"Error: VM Exception while processing transaction: reverted with " & "Error: VM Exception while processing transaction: reverted with " &
@ -16,10 +15,6 @@ proc revertReason*(emsg: string): string =
msg = msg.replace("\'") msg = msg.replace("\'")
return msg return msg
proc revertReason*(e: ref EthersError): string =
var msg = e.msg
msg.revertReason
proc reverts*[T](call: Future[T]): Future[bool] {.async.} = proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
try: try:
when T is void: when T is void:
@ -27,7 +22,7 @@ proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
else: else:
discard await call discard await call
return false return false
except ProviderError, SignerError, EstimateGasError: except ProviderError:
return true return true
proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} = proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} =
@ -37,12 +32,5 @@ proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} =
else: else:
discard await call discard await call
return false return false
except ProviderError, SignerError, EstimateGasError: except ProviderError as error:
let e = getCurrentException() return reason == error.revertReason
var passed = reason == (ref EthersError)(e).revertReason
if not passed and
not e.parent.isNil and
e.parent of (ref EthersError):
let revertReason = (ref EthersError)(e.parent).revertReason
passed = reason == revertReason
return passed

View File

@ -1,24 +1,17 @@
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ./basics import ./basics
type type Transaction* = object
TransactionType* = enum sender*: ?Address
Legacy = 0,
AccessList = 1,
Dynamic = 2
Transaction* {.serialize.} = object
sender* {.serialize("from").}: ?Address
to*: Address to*: Address
data*: seq[byte] data*: seq[byte]
value*: UInt256 value*: UInt256
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
func `$`*(transaction: Transaction): string = func `$`*(transaction: Transaction): string =
result = "(" result = "("
@ -26,15 +19,13 @@ func `$`*(transaction: Transaction): string =
result &= "from: " & $sender & ", " result &= "from: " & $sender & ", "
result &= "to: " & $transaction.to & ", " result &= "to: " & $transaction.to & ", "
result &= "value: " & $transaction.value & ", " result &= "value: " & $transaction.value & ", "
result &= "data: 0x" & $(transaction.data.toHex) result &= "data: 0x" & $transaction.data.toHex
if nonce =? transaction.nonce: if nonce =? transaction.nonce:
result &= ", nonce: " & $nonce result &= ", nonce: 0x" & $nonce.toHex
if chainId =? transaction.chainId: if chainId =? transaction.chainId:
result &= ", chainId: " & $chainId result &= ", chainId: " & $chainId
if gasPrice =? transaction.gasPrice: if gasPrice =? transaction.gasPrice:
result &= ", gasPrice: " & $gasPrice result &= ", gasPrice: 0x" & $gasPrice.toHex
if gasLimit =? transaction.gasLimit: if gasLimit =? transaction.gasLimit:
result &= ", gasLimit: " & $gasLimit result &= ", gasLimit: 0x" & $gasLimit.toHex
if txType =? transaction.transactionType:
result &= ", type: " & $txType
result &= ")" result &= ")"

View File

@ -1,3 +1,73 @@
import ./signers/wallet import eth/keys
import ./basics
import ./provider
import ./transaction
import ./signer
import ./wallet/error
import ./wallet/signing
export wallet export keys
export WalletError
var rng {.threadvar.}: ref HmacDrbgContext
proc getRng: ref HmacDrbgContext =
if rng.isNil:
rng = newRng()
rng
type Wallet* = ref object of Signer
privateKey*: PrivateKey
publicKey*: PublicKey
address*: Address
provider*: ?Provider
proc new*(_: type Wallet, privateKey: PrivateKey): Wallet =
let publicKey = privateKey.toPublicKey()
let address = Address.init(publicKey.toCanonicalAddress())
Wallet(privateKey: privateKey, publicKey: publicKey, address: address)
proc new*(_: type Wallet, privateKey: PrivateKey, provider: Provider): Wallet =
let wallet = Wallet.new(privateKey)
wallet.provider = some provider
wallet
proc new*(_: type Wallet, privateKey: string): ?!Wallet =
let keyResult = PrivateKey.fromHex(privateKey)
if keyResult.isErr:
return failure newException(WalletError, "invalid key: " & $keyResult.error)
success Wallet.new(keyResult.get())
proc new*(_: type Wallet, privateKey: string, provider: Provider): ?!Wallet =
let wallet = ? Wallet.new(privateKey)
wallet.provider = some provider
success wallet
proc connect*(wallet: Wallet, provider: Provider) =
wallet.provider = some provider
proc createRandom*(_: type Wallet): Wallet =
result = Wallet()
result.privateKey = PrivateKey.random(getRng()[])
result.publicKey = result.privateKey.toPublicKey()
result.address = Address.init(result.publicKey.toCanonicalAddress())
proc createRandom*(_: type Wallet, provider: Provider): Wallet =
result = Wallet()
result.privateKey = PrivateKey.random(getRng()[])
result.publicKey = result.privateKey.toPublicKey()
result.address = Address.init(result.publicKey.toCanonicalAddress())
result.provider = some provider
method provider*(wallet: Wallet): Provider =
without provider =? wallet.provider:
raiseWalletError "Wallet has no provider"
provider
method getAddress(wallet: Wallet): Future[Address] {.async.} =
return wallet.address
proc signTransaction*(wallet: Wallet,
transaction: Transaction): Future[seq[byte]] {.async.} =
if sender =? transaction.sender and sender != wallet.address:
raiseWalletError "from address mismatch"
return wallet.privateKey.sign(transaction)
method sendTransaction*(wallet: Wallet, transaction: Transaction): Future[TransactionResponse] {.async.} =
let signed = await signTransaction(wallet, transaction)
return await provider(wallet).sendTransaction(signed)

7
ethers/wallet/error.nim Normal file
View File

@ -0,0 +1,7 @@
import ../basics
type
WalletError* = object of EthersError
func raiseWalletError*(message: string) =
raise newException(WalletError, message)

View File

@ -1,13 +1,9 @@
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 ../basics
import pkg/eth/common/eth_hash import ../transaction as ethers
import ../../basics
import ../../transaction as ethers
import ../../provider
import ./error import ./error
from pkg/eth/common/eth_types import EthAddress
type type
Transaction = ethers.Transaction Transaction = ethers.Transaction
@ -26,18 +22,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,11 +43,22 @@ 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] =
key.sign(transaction.toSignableTransaction()) key.sign(transaction.toSignableTransaction())
func toTransactionHash*(bytes: seq[byte]): TransactionHash =
TransactionHash(bytes.keccakHash.data)

View File

@ -1,4 +1,3 @@
-d:"chronicles_log_level=INFO" -d:"chronicles_log_level=INFO"
-d:"json_rpc_websocket_package=websock" -d:"json_rpc_websocket_package=websock"
--warning[LockLevel]:off --warning[LockLevel]:off
--warning[DotLikeOps]:off

View File

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

View File

@ -1,4 +1,4 @@
import pkg/serde import std/json
import pkg/ethers/basics import pkg/ethers/basics
type Deployment* = object type Deployment* = object

View File

@ -1,14 +0,0 @@
import pkg/ethers
import ./hardhat
type
TestHelpers* = ref object of Contract
method doRevert*(
self: TestHelpers,
revertReason: string
): Confirmable {.base, contract.}
proc new*(_: type TestHelpers, signer: Signer): TestHelpers =
let deployment = readDeployment()
TestHelpers.new(!deployment.address(TestHelpers), signer)

View File

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

View File

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

View File

@ -1,15 +1,6 @@
import std/strutils
import std/unittest import std/unittest
import pkg/ethers/provider import pkg/ethers/provider
import pkg/ethers/providers/jsonrpc/conversions import pkg/ethers/providers/jsonrpc/conversions
import pkg/questionable
import pkg/questionable/results
import pkg/serde
import pkg/stew/byteutils
func flatten(s: string): string =
s.replace(" ")
.replace("\n")
suite "JSON Conversions": suite "JSON Conversions":
@ -20,13 +11,14 @@ suite "JSON Conversions":
"timestamp":"0x6285c293" "timestamp":"0x6285c293"
} }
let blk1 = !Block.fromJson(json) var blk = Block.fromJson(json)
check blk1.number.isNone check blk.number.isNone
json["number"] = newJString("") json["number"] = newJString("")
let blk2 = !Block.fromJson(json) blk = Block.fromJson(json)
check blk2.number.isNone check blk.number.isSome
check blk.number.get.isZero
test "missing block hash in Block isNone": test "missing block hash in Block isNone":
@ -39,13 +31,12 @@ suite "JSON Conversions":
} }
} }
without blk =? Block.fromJson(blkJson["result"]): var blk = Block.fromJson(blkJson["result"])
unittest.fail
check blk.hash.isNone check blk.hash.isNone
test "missing block number in TransactionReceipt isNone": test "missing block number in TransactionReceipt isNone":
var json = %*{ var json = %*{
"from": newJNull(), "sender": newJNull(),
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"contractAddress": newJNull(), "contractAddress": newJNull(),
"transactionIndex": "0x0", "transactionIndex": "0x0",
@ -65,23 +56,20 @@ suite "JSON Conversions":
], ],
"blockNumber": newJNull(), "blockNumber": newJNull(),
"cumulativeGasUsed": "0x10db1", "cumulativeGasUsed": "0x10db1",
"status": "0x1", "status": "0000000000000001"
"effectiveGasPrice": "0x3b9aca08",
"type": "0x0"
} }
without receipt1 =? TransactionReceipt.fromJson(json): var receipt = TransactionReceipt.fromJson(json)
unittest.fail check receipt.blockNumber.isNone
check receipt1.blockNumber.isNone
json["blockNumber"] = newJString("") json["blockNumber"] = newJString("")
without receipt2 =? TransactionReceipt.fromJson(json): receipt = TransactionReceipt.fromJson(json)
unittest.fail check receipt.blockNumber.isSome
check receipt2.blockNumber.isNone check receipt.blockNumber.get.isZero
test "missing block hash in TransactionReceipt isNone": test "missing block hash in TransactionReceipt isNone":
let json = %*{ let json = %*{
"from": newJNull(), "sender": newJNull(),
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"contractAddress": newJNull(), "contractAddress": newJNull(),
"transactionIndex": "0x0", "transactionIndex": "0x0",
@ -101,154 +89,8 @@ suite "JSON Conversions":
], ],
"blockNumber": newJNull(), "blockNumber": newJNull(),
"cumulativeGasUsed": "0x10db1", "cumulativeGasUsed": "0x10db1",
"status": "0x1", "status": "0000000000000001"
"effectiveGasPrice": "0x3b9aca08",
"type": "0x0"
} }
without receipt =? TransactionReceipt.fromJson(json): let receipt = TransactionReceipt.fromJson(json)
unittest.fail
check receipt.blockHash.isNone check receipt.blockHash.isNone
test "correctly deserializes PastTransaction":
let json = %*{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
"blockNumber":"0x22e",
"from":"0xe00b677c29ff8d8fe6068530e2bc36158c54dd34",
"gas":"0x4d4bb",
"gasPrice":"0x3b9aca07",
"hash":"0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790",
"input":"0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3",
"value":"0x0",
"type":"0x0",
"chainId":"0xc0de4",
"v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
}
without tx =? PastTransaction.fromJson(json):
unittest.fail
check tx.blockHash == BlockHash.fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922")
check tx.blockNumber == 0x22e.u256
check tx.sender == Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get
check tx.gas == 0x4d4bb.u256
check tx.gasPrice == 0x3b9aca07.u256
check tx.hash == TransactionHash(array[32, byte].fromHex("0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790"))
check tx.input == hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000")
check tx.nonce == 0x3.u256
check tx.to == Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e").get
check tx.transactionIndex == 0x3.u256
check tx.value == 0.u256
check tx.transactionType == some TransactionType.Legacy
check tx.chainId == some 0xc0de4.u256
check tx.v == 0x181bec.u256
check tx.r == UInt256.fromBytesBE(hexToSeqByte("0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a"))
check tx.s == UInt256.fromBytesBE(hexToSeqByte("0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"))
test "PastTransaction serializes correctly":
let tx = PastTransaction(
blockHash: BlockHash.fromHex("0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922"),
blockNumber: 0x22e.u256,
sender: Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34").get,
gas: 0x4d4bb.u256,
gasPrice: 0x3b9aca07.u256,
hash: TransactionHash(array[32, byte].fromHex("0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790")),
input: hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"),
nonce: 0x3.u256,
to: Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e").get,
transactionIndex: 0x3.u256,
value: 0.u256,
v: 0x181bec.u256,
r: UInt256.fromBytesBE(hexToSeqByte("0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a")),
s: UInt256.fromBytesBE(hexToSeqByte("0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2")),
transactionType: some TransactionType.Legacy,
chainId: some 0xc0de4.u256
)
let expected = """
{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
"blockNumber":"0x22e",
"from":"0xe00b677c29ff8d8fe6068530e2bc36158c54dd34",
"gas":"0x4d4bb",
"gasPrice":"0x3b9aca07",
"hash":"0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790",
"input":"0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3",
"type":"0x0",
"chainId":"0xc0de4",
"value":"0x0",
"v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
}""".flatten
check $(%tx) == expected
test "correctly converts PastTransaction to Transaction":
let json = %*{
"blockHash":"0x595bffbe897e025ea2df3213c4cc52c3f3d69bc04b49011d558f1b0e70038922",
"blockNumber":"0x22e",
"from":"0xe00b677c29ff8d8fe6068530e2bc36158c54dd34",
"gas":"0x52277",
"gasPrice":"0x3b9aca07",
"hash":"0xa31608907c338d6497b0c6ec81049d845c7d409490ebf78171f35143897ca790",
"input":"0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000",
"nonce":"0x3",
"to":"0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e",
"transactionIndex":"0x3",
"value":"0x0",
"type":"0x0",
"chainId":"0xc0de4",
"v":"0x181bec",
"r":"0x57ba18460934526333b80b0fea08737c363f3cd5fbec4a25a8a25e3e8acb362a",
"s":"0x33aa50bc8bd719b6b17ad0bf52006bf8943999198f2bf731eb33c118091000f2"
}
without past =? PastTransaction.fromJson(json):
unittest.fail
check %past.toTransaction == %*{
"to": !Address.init("0x92f09aa59dccb892a9f5406ddd9c0b98f02ea57e"),
"data": hexToSeqByte("0x6368a471d26ff5c7f835c1a8203235e88846ce1a196d6e79df0eaedd1b8ed3deec2ae5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000012a00000000000000000000000000000000000000000000000000000000000000"),
"value": "0x0",
"from": !Address.init("0xe00b677c29ff8d8fe6068530e2bc36158c54dd34"),
"nonce": 0x3.u256,
"chainId": 0xc0de4.u256,
"gasPrice": 0x3b9aca07.u256,
"gas": 0x52277.u256
}
test "correctly deserializes BlockTag":
check !BlockTag.fromJson(newJString("earliest")) == BlockTag.earliest
check !BlockTag.fromJson(newJString("latest")) == BlockTag.latest
check !BlockTag.fromJson(newJString("pending")) == BlockTag.pending
check !BlockTag.fromJson(newJString("0x1")) == BlockTag.init(1.u256)
test "fails to deserialize BlockTag from an empty string":
let res = BlockTag.fromJson(newJString(""))
check res.error of SerializationError
check res.error.msg == "Failed to convert '\"\"' to BlockTag: must be one of 'earliest', 'latest', 'pending'"
test "correctly deserializes TransactionType":
check !TransactionType.fromJson(newJString("0x0")) == TransactionType.Legacy
check !TransactionType.fromJson(newJString("0x1")) == TransactionType.AccessList
check !TransactionType.fromJson(newJString("0x2")) == TransactionType.Dynamic
test "correctly serializes TransactionType":
check TransactionType.Legacy.toJson == "\"0x0\""
check TransactionType.AccessList.toJson == "\"0x1\""
check TransactionType.Dynamic.toJson == "\"0x2\""
test "correctly deserializes TransactionStatus":
check !TransactionStatus.fromJson(newJString("0x0")) == TransactionStatus.Failure
check !TransactionStatus.fromJson(newJString("0x1")) == TransactionStatus.Success
check !TransactionStatus.fromJson(newJString("0x2")) == TransactionStatus.Invalid
test "correctly serializes TransactionStatus":
check TransactionStatus.Failure.toJson == "\"0x0\""
check TransactionStatus.Success.toJson == "\"0x1\""
check TransactionStatus.Invalid.toJson == "\"0x2\""

View File

@ -1,27 +0,0 @@
import std/unittest
import pkg/serde
import pkg/questionable
import pkg/ethers/providers/jsonrpc/errors
suite "JSON RPC errors":
test "converts JSON RPC error to Nim error":
let error = %*{ "message": "some error" }
check JsonRpcProviderError.new(error).msg == "some error"
test "converts error data to bytes":
let error = %*{
"message": "VM Exception: reverted with 'some error'",
"data": "0xabcd"
}
check JsonRpcProviderError.new(error).data == some @[0xab'u8, 0xcd'u8]
test "converts nested error data to bytes":
let error = %*{
"message": "VM Exception: reverted with 'some error'",
"data": {
"message": "VM Exception: reverted with 'some error'",
"data": "0xabcd"
}
}
check JsonRpcProviderError.new(error).data == some @[0xab'u8, 0xcd'u8]

View File

@ -1,5 +1,5 @@
import std/os import std/json
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
@ -7,8 +7,7 @@ import pkg/stew/byteutils
import ../../examples import ../../examples
import ../../miner import ../../miner
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545") for url in ["ws://localhost:8545", "http://localhost:8545"]:
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
suite "JsonRpcProvider (" & url & ")": suite "JsonRpcProvider (" & url & ")":
@ -30,7 +29,7 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
test "sends raw messages to the provider": test "sends raw messages to the provider":
let response = await provider.send("evm_mine") let response = await provider.send("evm_mine")
check response == %"0" check response == %"0x0"
test "returns block number": test "returns block number":
let blocknumber1 = await provider.getBlockNumber() let blocknumber1 = await provider.getBlockNumber()
@ -49,7 +48,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
@ -68,13 +67,13 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
check UInt256.fromHex("0x" & txResp.hash.toHex) > 0 check UInt256.fromHex("0x" & txResp.hash.toHex) > 0
test "can wait for a transaction to be confirmed": test "can wait for a transaction to be confirmed":
for confirmations in 1..3: for confirmations in 0..3:
let signer = provider.getSigner() let signer = provider.getSigner()
let transaction = Transaction.example let transaction = Transaction.example
let populated = await signer.populateTransaction(transaction) let populated = await signer.populateTransaction(transaction)
let confirming = signer.sendTransaction(populated).confirm(confirmations) let confirming = signer.sendTransaction(populated).confirm(confirmations)
await sleepAsync(100.millis) # wait for tx to be mined await sleepAsync(100.millis) # wait for tx to be mined
await provider.mineBlocks(confirmations) await provider.mineBlocks(confirmations - 1)
let receipt = await confirming let receipt = await confirming
check receipt.blockNumber.isSome check receipt.blockNumber.isSome
@ -82,7 +81,6 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
let hash = TransactionHash.example let hash = TransactionHash.example
let tx = TransactionResponse(provider: provider, hash: hash) let tx = TransactionResponse(provider: provider, hash: hash)
let confirming = tx.confirm(confirmations = 2, timeout = 5) let confirming = tx.confirm(confirmations = 2, timeout = 5)
await sleepAsync(100.millis) # wait for confirm to subscribe to new blocks
await provider.mineBlocks(5) await provider.mineBlocks(5)
expect EthersError: expect EthersError:
discard await confirming discard await confirming
@ -98,11 +96,6 @@ 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)
test "syncing":
let isSyncing = await provider.isSyncing()
check not isSyncing

View File

@ -1,5 +1,4 @@
import std/os import pkg/asynctest
import pkg/asynctest/chronos/unittest
import pkg/ethers import pkg/ethers
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../examples import ../../examples
@ -8,10 +7,9 @@ suite "JsonRpcSigner":
var provider: JsonRpcProvider var provider: JsonRpcProvider
var accounts: seq[Address] var accounts: seq[Address]
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup: setup:
provider = JsonRpcProvider.new("http://" & providerUrl) provider = JsonRpcProvider.new()
accounts = await provider.listAccounts() accounts = await provider.listAccounts()
teardown: teardown:
@ -55,27 +53,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,15 +1,9 @@
import std/os import std/json
import std/importutils import pkg/asynctest
import pkg/asynctest/chronos/unittest
import pkg/serde
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import pkg/json_rpc/rpcserver
import ethers/provider import ethers/provider
import ethers/providers/jsonrpc/subscriptions import ethers/providers/jsonrpc/subscriptions
import ../../examples
import ./rpc_mock
suite "JsonRpcSubscriptions": suite "JsonRpcSubscriptions":
test "can be instantiated with an http client": test "can be instantiated with an http client":
@ -26,8 +20,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,8 +31,7 @@ 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())
@ -49,19 +42,9 @@ 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())
@ -79,9 +62,8 @@ suite "Web socket subscriptions":
setup: setup:
client = newRpcWebSocketClient() client = newRpcWebSocketClient()
await client.connect("ws://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")) await client.connect("ws://localhost:8545")
subscriptions = JsonRpcSubscriptions.new(client) subscriptions = JsonRpcSubscriptions.new(client)
subscriptions.start()
teardown: teardown:
await subscriptions.close() await subscriptions.close()
@ -96,123 +78,12 @@ suite "HTTP polling subscriptions":
setup: setup:
client = newRpcHttpClient() client = newRpcHttpClient()
await client.connect("http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")) await client.connect("http://localhost:8545")
subscriptions = JsonRpcSubscriptions.new(client, subscriptions = JsonRpcSubscriptions.new(client,
pollingInterval = 100.millis) pollingInterval = 100.millis)
subscriptions.start()
teardown: teardown:
await subscriptions.close() await subscriptions.close()
await client.close() await client.close()
subscriptionTests(subscriptions, client) subscriptionTests(subscriptions, client)
suite "HTTP polling subscriptions - mock tests":
var subscriptions: PollingSubscriptions
var client: RpcHttpClient
var mockServer: MockRpcHttpServer
privateAccess(PollingSubscriptions)
privateAccess(JsonRpcSubscriptions)
proc startServer() {.async.} =
mockServer = MockRpcHttpServer.new()
mockServer.start()
await client.connect("http://" & $mockServer.localAddress()[0])
proc stopServer() {.async.} =
await mockServer.stop()
setup:
client = newRpcHttpClient()
await startServer()
subscriptions = PollingSubscriptions(
JsonRpcSubscriptions.new(
client,
pollingInterval = 1.millis))
subscriptions.start()
teardown:
await subscriptions.close()
await client.close()
await mockServer.stop()
test "filter not found error recreates log filter":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
check subscriptions.logFilters.len == 0
check subscriptions.subscriptionMapping.len == 0
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
check subscriptions.logFilters[id] == filter
check subscriptions.subscriptionMapping[id] == id
check subscriptions.logFilters.len == 1
check subscriptions.subscriptionMapping.len == 1
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
test "recreated log filter can be still unsubscribed using the original id":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
await subscriptions.unsubscribe(id)
check not subscriptions.logFilters.hasKey id
check not subscriptions.subscriptionMapping.hasKey id
test "filter not found error recreates block filter":
let emptyHandler = proc(blck: ?!Block) = discard
check subscriptions.subscriptionMapping.len == 0
let id = await subscriptions.subscribeBlocks(emptyHandler)
check subscriptions.subscriptionMapping[id] == id
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
test "recreated block filter can be still unsubscribed using the original id":
let emptyHandler = proc(blck: ?!Block) = discard
let id = await subscriptions.subscribeBlocks(emptyHandler)
mockServer.invalidateFilter(id)
check eventually subscriptions.subscriptionMapping[id] != id
await subscriptions.unsubscribe(id)
check not subscriptions.subscriptionMapping.hasKey id
test "polling continues with new filter after temporary error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
let emptyHandler = proc(log: ?!Log) = discard
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
await stopServer()
mockServer.invalidateFilter(id)
await sleepAsync(50.milliseconds)
await startServer()
check eventually subscriptions.subscriptionMapping[id] != id
test "calls callback with failed result on error":
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
var failedResultReceived = false
proc handler(log: ?!Log) =
if log.isErr:
failedResultReceived = true
let id = await subscriptions.subscribeLogs(filter, handler)
await sleepAsync(50.milliseconds)
mockServer.nextGetChangesReturnsError = true
check eventually failedResultReceived

View File

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

View File

@ -1,8 +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
{.warning[UnusedImport]:off.} {.warning[UnusedImport]:off.}

View File

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

View File

@ -3,7 +3,8 @@ 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"
requires "questionable >= 0.10.3 & < 0.11.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,24 +1,20 @@
import pkg/serde import std/json
import std/os import pkg/asynctest
import std/options
import pkg/asynctest/chronos/unittest
import pkg/questionable import pkg/questionable
import pkg/stint import pkg/stint
import pkg/ethers import pkg/ethers
import pkg/ethers/erc20 import pkg/ethers/erc20
import ./hardhat import ./hardhat
import ./helpers
import ./miner import ./miner
import ./mocks import ./mocks
type type
TestToken = ref object of Erc20Token TestToken = ref object of Erc20Token
method mint(token: TestToken, holder: Address, amount: UInt256): Confirmable {.base, contract.} method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.}
method myBalance(token: TestToken): UInt256 {.base, contract, view.} method myBalance(token: TestToken): UInt256 {.base, contract, view.}
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545") for url in ["ws://localhost:8545", "http://localhost:8545"]:
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
suite "Contracts (" & url & ")": suite "Contracts (" & url & ")":
@ -64,7 +60,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":
@ -73,14 +69,24 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
await token.mint(accounts[1], 100.u256) await token.mint(accounts[1], 100.u256)
check (await balanceOf(token, accounts[1])) == 100.u256 check (await balanceOf(token, accounts[1])) == 100.u256
test "can call non-constant functions with a ?TransactionResponse return type":
token = TestToken.new(token.address, provider.getSigner())
proc mint(token: TestToken,
holder: Address,
amount: UInt256): ?TransactionResponse {.contract.}
let txResp = await token.mint(accounts[1], 100.u256)
check txResp is (?TransactionResponse)
check txResp.isSome
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,
amount: UInt256): Confirmable {.contract.} amount: UInt256): Confirmable {.contract.}
let confirmable = await token.mint(accounts[1], 100.u256) let txResp = await token.mint(accounts[1], 100.u256)
check confirmable is Confirmable check txResp is Confirmable
check confirmable.response.isSome check txResp.isSome
test "fails to compile when function has an implementation": test "fails to compile when function has an implementation":
let works = compiles: let works = compiles:
@ -107,17 +113,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":
@ -132,35 +138,10 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
check (await token.balanceOf(accounts[0], beforeMint)) == 0 check (await token.balanceOf(accounts[0], beforeMint)) == 0
check (await token.balanceOf(accounts[0], afterMint)) == 100 check (await token.balanceOf(accounts[0], afterMint)) == 100
test "can simulate transactions for different block heights":
let block1 = await provider.getBlockNumber()
let signer = provider.getSigner(accounts[0])
discard await token.connect(signer).mint(accounts[0], 100.u256)
let block2 = await provider.getBlockNumber()
let beforeMint = CallOverrides(blockTag: some BlockTag.init(block1))
let afterMint = CallOverrides(blockTag: some BlockTag.init(block2))
expect ProviderError:
discard await token.transfer(accounts[1], 50.u256, beforeMint)
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,8 +163,7 @@ 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])
@ -255,73 +235,3 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
check logs == @[ check logs == @[
Transfer(receiver: accounts[0], value: 100.u256) Transfer(receiver: accounts[0], value: 100.u256)
] ]
test "concurrent transactions with first failing increment nonce correctly":
let signer = provider.getSigner()
let token = TestToken.new(token.address, signer)
let helpersContract = TestHelpers.new(signer)
# emulate concurrent populateTransaction calls, where the first one fails
let futs = await allFinished(
helpersContract.doRevert("some reason"),
token.mint(accounts[0], 100.u256)
)
check futs[0].error of EstimateGasError
let receipt = await futs[1].confirm(1)
check receipt.status == TransactionStatus.Success
test "non-concurrent transactions with first failing increment nonce correctly":
let signer = provider.getSigner()
let token = TestToken.new(token.address, signer)
let helpersContract = TestHelpers.new(signer)
expect EstimateGasError:
discard await helpersContract.doRevert("some reason")
let receipt = await token
.mint(accounts[0], 100.u256)
.confirm(1)
check receipt.status == TransactionStatus.Success
test "can cancel procs that execute transactions":
let signer = provider.getSigner()
let token = TestToken.new(token.address, signer)
let countBefore = await signer.getTransactionCount(BlockTag.pending)
proc executeTx {.async.} =
discard await token.mint(accounts[0], 100.u256)
await executeTx().cancelAndWait()
let countAfter = await signer.getTransactionCount(BlockTag.pending)
check countBefore == countAfter
test "concurrent transactions succeed even if one is cancelled":
let signer = provider.getSigner()
let token = TestToken.new(token.address, signer)
let balanceBefore = await token.myBalance()
proc executeTx: Future[Confirmable] {.async.} =
return await token.mint(accounts[0], 100.u256)
proc executeTxWithCancellation: Future[Confirmable] {.async.} =
let fut = token.mint(accounts[0], 100.u256)
fut.cancelSoon()
return await fut
# emulate concurrent populateTransaction/sendTransaction calls, where the
# first one fails
let futs = await allFinished(
executeTxWithCancellation(),
executeTx(),
executeTx()
)
let receipt1 = await futs[1].confirm(1)
let receipt2 = await futs[2].confirm(1)
check receipt1.status == TransactionStatus.Success
check receipt2.status == TransactionStatus.Success
let balanceAfter = await token.myBalance()
check balanceAfter == balanceBefore + 200.u256

View File

@ -1,161 +0,0 @@
import std/os
import pkg/serde
import pkg/asynctest/chronos/unittest
import pkg/ethers
import ./hardhat
suite "Contract custom errors":
type
TestCustomErrors = ref object of Contract
SimpleError = object of SolidityError
ErrorWithArguments = object of SolidityError
arguments: tuple[one: UInt256, two: bool]
ErrorWithStaticStruct = object of SolidityError
arguments: tuple[one: Static, two: Static]
ErrorWithDynamicStruct = object of SolidityError
arguments: tuple[one: Dynamic, two: Dynamic]
ErrorWithDynamicAndStaticStruct = object of SolidityError
arguments: tuple[one: Dynamic, two: Static]
Static = (UInt256, UInt256)
Dynamic = (string, UInt256)
var contract: TestCustomErrors
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment()
let address = !deployment.address(TestCustomErrors)
contract = TestCustomErrors.new(address, provider)
teardown:
discard await provider.send("evm_revert", @[snapshot])
await provider.close()
test "handles simple errors":
proc revertsSimpleError(contract: TestCustomErrors)
{.contract, pure, errors:[SimpleError].}
expect SimpleError:
await contract.revertsSimpleError()
test "handles error with arguments":
proc revertsErrorWithArguments(contract: TestCustomErrors)
{.contract, pure, errors:[ErrorWithArguments].}
try:
await contract.revertsErrorWithArguments()
fail()
except ErrorWithArguments as error:
check error.arguments.one == 1
check error.arguments.two == true
test "handles error with static struct arguments":
proc revertsErrorWithStaticStruct(contract: TestCustomErrors)
{.contract, pure, errors:[ErrorWithStaticStruct].}
try:
await contract.revertsErrorWithStaticStruct()
fail()
except ErrorWithStaticStruct as error:
check error.arguments.one == (1.u256, 2.u256)
check error.arguments.two == (3.u256, 4.u256)
test "handles error with dynamic struct arguments":
proc revertsErrorWithDynamicStruct(contract: TestCustomErrors)
{.contract, pure, errors:[ErrorWithDynamicStruct].}
try:
await contract.revertsErrorWithDynamicStruct()
fail()
except ErrorWithDynamicStruct as error:
check error.arguments.one == ("1", 2.u256)
check error.arguments.two == ("3", 4.u256)
test "handles error with dynamic and static struct arguments":
proc revertsErrorWithDynamicAndStaticStruct(contract: TestCustomErrors)
{.contract, pure, errors:[ErrorWithDynamicAndStaticStruct].}
try:
await contract.revertsErrorWithDynamicAndStaticStruct()
fail()
except ErrorWithDynamicAndStaticStruct as error:
check error.arguments.one == ("1", 2.u256)
check error.arguments.two == (3.u256, 4.u256)
test "handles multiple error types":
proc revertsMultipleErrors(contract: TestCustomErrors, simple: bool)
{.contract, errors:[SimpleError, ErrorWithArguments].}
let contract = contract.connect(provider.getSigner())
expect SimpleError:
await contract.revertsMultipleErrors(simple = true)
expect ErrorWithArguments:
await contract.revertsMultipleErrors(simple = false)
test "handles gas estimation errors when calling a contract function":
proc revertsTransaction(contract: TestCustomErrors)
{.contract, errors:[ErrorWithArguments].}
let contract = contract.connect(provider.getSigner())
try:
await contract.revertsTransaction()
fail()
except ErrorWithArguments as error:
check error.arguments.one == 1.u256
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":
proc revertsTransaction(contract: TestCustomErrors)
{.contract, errors:[ErrorWithArguments].}
# skip gas estimation
let overrides = TransactionOverrides(gasLimit: some 1000000.u256)
let contract = contract.connect(provider.getSigner())
try:
await contract.revertsTransaction(overrides = overrides)
fail()
except ErrorWithArguments as error:
check error.arguments.one == 1.u256
check error.arguments.two == true
test "handles transaction confirmation errors":
proc revertsTransaction(contract: TestCustomErrors): Confirmable
{.contract, errors:[ErrorWithArguments].}
# skip gas estimation
let overrides = TransactionOverrides(gasLimit: some 1000000.u256)
# ensure that transaction is not immediately checked by hardhat
discard await provider.send("evm_setAutomine", @[%false])
let contract = contract.connect(provider.getSigner())
try:
let future = contract.revertsTransaction(overrides = overrides).confirm(1)
await sleepAsync(100.millis) # wait for transaction to be submitted
discard await provider.send("evm_mine", @[]) # mine the transaction
discard await future # wait for confirmation
fail()
except ErrorWithArguments as error:
check error.arguments.one == 1.u256
check error.arguments.two == true
# re-enable auto mining
discard await provider.send("evm_setAutomine", @[%true])

View File

@ -1,7 +1,5 @@
import std/os import pkg/asynctest
import pkg/asynctest/chronos/unittest
import pkg/ethers import pkg/ethers
import pkg/serde
import ./hardhat import ./hardhat
type type
@ -15,10 +13,9 @@ suite "Contract enum parameters and return values":
var contract: TestEnums var contract: TestEnums
var provider: JsonRpcProvider var provider: JsonRpcProvider
var snapshot: JsonNode var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup: setup:
provider = JsonRpcProvider.new("http://" & providerUrl) provider = JsonRpcProvider.new()
snapshot = await provider.send("evm_snapshot") snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment() let deployment = readDeployment()
contract = TestEnums.new(!deployment.address(TestEnums), provider) contract = TestEnums.new(!deployment.address(TestEnums), provider)

View File

@ -1,6 +1,5 @@
import std/os import std/json
import pkg/serde import pkg/asynctest
import pkg/asynctest/chronos/unittest
import pkg/questionable import pkg/questionable
import pkg/stint import pkg/stint
import pkg/ethers import pkg/ethers
@ -10,10 +9,9 @@ import ./hardhat
type type
TestToken = ref object of Erc20Token TestToken = ref object of Erc20Token
method mint(token: TestToken, holder: Address, amount: UInt256): Confirmable {.base, contract.} method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.}
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545") for url in ["ws://localhost:8545", "http://localhost:8545"]:
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
suite "ERC20 (" & url & ")": suite "ERC20 (" & url & ")":
@ -71,32 +69,6 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]:
check (await token.balanceOf(accounts[0])) == 100.u256 check (await token.balanceOf(accounts[0])) == 100.u256
check (await token.balanceOf(accounts[1])) == 0.u256 check (await token.balanceOf(accounts[1])) == 0.u256
test "increase/decrease allowance":
discard await testToken.mint(accounts[0], 100.u256)
check (await token.allowance(accounts[0], accounts[1])) == 0.u256
check (await token.balanceOf(accounts[0])) == 100.u256
check (await token.balanceOf(accounts[1])) == 0.u256
discard await token.increaseAllowance(accounts[1], 50.u256)
check (await token.allowance(accounts[0], accounts[1])) == 50.u256
check (await token.balanceOf(accounts[0])) == 100.u256
check (await token.balanceOf(accounts[1])) == 0.u256
discard await token.increaseAllowance(accounts[1], 50.u256)
check (await token.allowance(accounts[0], accounts[1])) == 100.u256
check (await token.balanceOf(accounts[0])) == 100.u256
check (await token.balanceOf(accounts[1])) == 0.u256
discard await token.decreaseAllowance(accounts[1], 50.u256)
check (await token.allowance(accounts[0], accounts[1])) == 50.u256
check (await token.balanceOf(accounts[0])) == 100.u256
check (await token.balanceOf(accounts[1])) == 0.u256
test "transferFrom tokens": test "transferFrom tokens":
let senderAccount = accounts[0] let senderAccount = accounts[0]
let receiverAccount = accounts[1] let receiverAccount = accounts[1]

View File

@ -1,56 +0,0 @@
import std/unittest
import std/strutils
import pkg/questionable/results
import pkg/contractabi
import pkg/ethers/errors
import pkg/ethers/contracts/errors/encoding
suite "Decoding of custom errors":
type
SimpleError = object of SolidityError
ErrorWithArguments = object of SolidityError
arguments: tuple[one: UInt256, two: bool]
test "decodes a simple error":
let decoded = SimpleError.decode(@[0xc2'u8, 0xbb, 0x94, 0x7c])
check decoded is ?!(ref SimpleError)
check decoded.isSuccess
check (!decoded).msg.contains("SimpleError()")
test "decodes error with arguments":
let expected = (ref ErrorWithArguments)(arguments: (1.u256, true))
let encoded = AbiEncoder.encode(expected)
let decoded = ErrorWithArguments.decode(encoded)
check decoded.isSuccess
check (!decoded).arguments.one == 1.u256
check (!decoded).arguments.two == true
check (!decoded).msg.contains("ErrorWithArguments(one: 1, two: true)")
test "returns failure when decoding fails":
let invalid = @[0xc2'u8, 0xbb, 0x94, 0x0] # last byte is wrong
let decoded = SimpleError.decode(invalid)
check decoded.isFailure
test "returns failure when data is less than 4 bytes":
let invalid = @[0xc2'u8, 0xbb, 0x94]
let decoded = SimpleError.decode(invalid)
check decoded.isFailure
test "returns failure when there are trailing bytes":
let invalid = @[0xc2'u8, 0xbb, 0x94, 0x7c, 0x0] # one byte too many
let decoded = SimpleError.decode(invalid)
check decoded.isFailure
test "returns failure when there are trailing bytes after arguments":
let error = (ref ErrorWithArguments)(arguments: (1.u256, true))
let encoded = AbiEncoder.encode(error)
let invalid = encoded & @[0x0'u8] # one byte too many
let decoded = ErrorWithArguments.decode(invalid)
check decoded.isFailure
test "decoding only works for SolidityErrors":
type InvalidError = ref object of CatchableError
const works = compiles:
InvalidError.decode(@[0x1'u8, 0x2, 0x3, 0x4])
check not works

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,77 +0,0 @@
import std/os
import pkg/asynctest/chronos/unittest
import pkg/ethers
import pkg/serde
import ./hardhat
type
TestGasEstimation = ref object of Contract
proc getTime(contract: TestGasEstimation): UInt256 {.contract, view.}
proc checkTimeEquals(contract: TestGasEstimation, expected: UInt256) {.contract.}
suite "gas estimation":
var contract: TestGasEstimation
var provider: JsonRpcProvider
var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup:
provider = JsonRpcProvider.new("http://" & providerUrl)
snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment()
let signer = provider.getSigner()
contract = TestGasEstimation.new(!deployment.address(TestGasEstimation), signer)
teardown:
discard await provider.send("evm_revert", @[snapshot])
await provider.close()
test "contract function calls use pending block for gas estimations":
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
# 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)
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,7 +1,5 @@
import std/os import pkg/asynctest
import pkg/asynctest/chronos/unittest
import pkg/ethers import pkg/ethers
import pkg/serde
import ./hardhat import ./hardhat
type type
@ -14,10 +12,9 @@ suite "Contract return values":
var contract: TestReturns var contract: TestReturns
var provider: JsonRpcProvider var provider: JsonRpcProvider
var snapshot: JsonNode var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup: setup:
provider = JsonRpcProvider.new("http://" & providerUrl) provider = JsonRpcProvider.new()
snapshot = await provider.send("evm_snapshot") snapshot = await provider.send("evm_snapshot")
let deployment = readDeployment() let deployment = readDeployment()
contract = TestReturns.new(!deployment.address(TestReturns), provider) contract = TestReturns.new(!deployment.address(TestReturns), provider)
@ -55,11 +52,3 @@ suite "Contract return values":
let values = await contract.getDynamics() let values = await contract.getDynamics()
check values.a == ("1", 2.u256) check values.a == ("1", 2.u256)
check values.b == ("3", 4.u256) check values.b == ("3", 4.u256)
test "handles static size struct as a public state variable":
proc staticVariable(contract: TestReturns): Static {.contract, getter.}
check (await contract.staticVariable()) == (1.u256, 2.u256)
test "handles dynamic size struct as a public state variable":
proc dynamicVariable(contract: TestReturns): Dynamic {.contract, getter.}
check (await contract.dynamicVariable()) == ("3", 4.u256)

View File

@ -1,11 +1,9 @@
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
import pkg/serde import ./hardhat
import ./helpers
suite "Testing helpers": suite "Testing helpers":
@ -15,13 +13,13 @@ suite "Testing helpers":
test "checks that call reverts": test "checks that call reverts":
proc call() {.async.} = proc call() {.async.} =
raise newException(EstimateGasError, $rpcResponse) raise newException(ProviderError, $rpcResponse)
check await call().reverts() check await call().reverts()
test "checks reason for revert": test "checks reason for revert":
proc call() {.async.} = proc call() {.async.} =
raise newException(EstimateGasError, $rpcResponse) raise newException(ProviderError, $rpcResponse)
check await call().reverts(revertReason) check await call().reverts(revertReason)
@ -30,37 +28,19 @@ suite "Testing helpers":
check not await call().reverts() check not await call().reverts()
test "reverts only checks ProviderErrors, EstimateGasErrors": test "reverts only checks ProviderErrors":
proc callProviderError() {.async.} = proc call() {.async.} =
raise newException(ProviderError, "test") raise newException(ContractError, "test")
proc callSignerError() {.async.} =
raise newException(SignerError, "test")
proc callEstimateGasError() {.async.} =
raise newException(EstimateGasError, "test")
proc callEthersError() {.async.} =
raise newException(EthersError, "test")
check await callProviderError().reverts() expect ContractError:
check await callSignerError().reverts() check await call().reverts()
check await callEstimateGasError().reverts()
expect EthersError:
check await callEthersError().reverts()
test "reverts with reason only checks ProviderErrors, EstimateGasErrors": test "reverts with reason only checks ProviderErrors":
proc callProviderError() {.async.} = proc call() {.async.} =
raise newException(ProviderError, revertReason) raise newException(ContractError, "test")
proc callSignerError() {.async.} =
raise newException(SignerError, revertReason)
proc callEstimateGasError() {.async.} =
raise newException(EstimateGasError, revertReason)
proc callEthersError() {.async.} =
raise newException(EthersError, revertReason)
check await callProviderError().reverts(revertReason) expect ContractError:
check await callSignerError().reverts(revertReason) check await call().reverts(revertReason)
check await callEstimateGasError().reverts(revertReason)
expect EthersError:
check await callEthersError().reverts(revertReason)
test "reverts with reason is false when there is no revert": test "reverts with reason is false when there is no revert":
proc call() {.async.} = discard proc call() {.async.} = discard
@ -69,50 +49,46 @@ suite "Testing helpers":
test "reverts is false when the revert reason doesn't match": test "reverts is false when the revert reason doesn't match":
proc call() {.async.} = proc call() {.async.} =
raise newException(EstimateGasError, "other reason") raise newException(ProviderError, "other reason")
check not await call().reverts(revertReason) check not await call().reverts(revertReason)
test "revert handles non-standard revert prefix": test "revert handles non-standard revert prefix":
let nonStdMsg = fmt"Provider VM Exception: reverted with {revertReason}" let nonStdMsg = fmt"Provider VM Exception: reverted with {revertReason}"
proc call() {.async.} = proc call() {.async.} =
raise newException(EstimateGasError, nonStdMsg) raise newException(ProviderError, nonStdMsg)
check await call().reverts(nonStdMsg) check await call().reverts(nonStdMsg)
test "works with functions that return a value": test "works with functions that return a value":
proc call(): Future[int] {.async.} = return 42 proc call(): Future[int] {.async.} = return 42
check not await call().reverts() check not await call().reverts()
check not await call().reverts(revertReason) check not await call().reverts("some reason")
type
TestHelpers* = ref object of Contract
suite "Testing helpers - contracts": method revertsWith*(self: TestHelpers,
revertReason: string) {.base, contract, view.}
suite "Testing helpers - provider":
var helpersContract: TestHelpers var helpersContract: TestHelpers
var provider: JsonRpcProvider var provider: JsonRpcProvider
var snapshot: JsonNode var snapshot: JsonNode
var accounts: seq[Address] var accounts: seq[Address]
let revertReason = "revert reason" let revertReason = "revert reason"
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup: setup:
provider = JsonRpcProvider.new("ws://" & providerUrl) provider = JsonRpcProvider.new("ws://127.0.0.1:8545")
snapshot = await provider.send("evm_snapshot") snapshot = await provider.send("evm_snapshot")
accounts = await provider.listAccounts() accounts = await provider.listAccounts()
helpersContract = TestHelpers.new(provider.getSigner()) let deployment = readDeployment()
helpersContract = TestHelpers.new(!deployment.address(TestHelpers), provider)
teardown: teardown:
discard await provider.send("evm_revert", @[snapshot]) discard await provider.send("evm_revert", @[snapshot])
await provider.close() await provider.close()
test "revert reason can be retrieved when transaction fails": test "revert works with provider":
let txResp = helpersContract.doRevert( check await helpersContract.revertsWith(revertReason).reverts(revertReason)
revertReason,
# override gasLimit to skip estimating gas
TransactionOverrides(gasLimit: some 10000000.u256)
)
check await txResp.confirm(1).reverts(revertReason)
test "revert reason can be retrieved when estimate gas fails":
let txResp = helpersContract.doRevert(revertReason)
check await txResp.reverts(revertReason)

View File

@ -1,6 +1,4 @@
import std/os import pkg/asynctest
import pkg/asynctest/chronos/unittest
import pkg/serde
import pkg/stew/byteutils import pkg/stew/byteutils
import ../ethers import ../ethers
@ -13,10 +11,9 @@ proc transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.contract.}
suite "Wallet": suite "Wallet":
var provider: JsonRpcProvider var provider: JsonRpcProvider
var snapshot: JsonNode var snapshot: JsonNode
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
setup: setup:
provider = JsonRpcProvider.new("http://" & providerUrl) provider = JsonRpcProvider.new()
snapshot = await provider.send("evm_snapshot") snapshot = await provider.send("evm_snapshot")
teardown: teardown:
@ -80,8 +77,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 +112,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)

View File

@ -1,58 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract TestCustomErrors {
error SimpleError();
error ErrorWithArguments(uint256 one, bool two);
error ErrorWithStaticStruct(StaticStruct one, StaticStruct two);
error ErrorWithDynamicStruct(DynamicStruct one, DynamicStruct two);
error ErrorWithDynamicAndStaticStruct(DynamicStruct one, StaticStruct two);
struct StaticStruct {
uint256 a;
uint256 b;
}
struct DynamicStruct {
string a;
uint256 b;
}
function revertsSimpleError() public pure {
revert SimpleError();
}
function revertsErrorWithArguments() public pure {
revert ErrorWithArguments(1, true);
}
function revertsErrorWithStaticStruct() public pure {
revert ErrorWithStaticStruct(StaticStruct(1, 2), StaticStruct(3, 4));
}
function revertsErrorWithDynamicStruct() public pure {
revert ErrorWithDynamicStruct(DynamicStruct("1", 2), DynamicStruct("3", 4));
}
function revertsErrorWithDynamicAndStaticStruct() public pure {
revert ErrorWithDynamicAndStaticStruct(
DynamicStruct("1", 2),
StaticStruct(3, 4)
);
}
function revertsMultipleErrors(bool simple) public pure {
if (simple) {
revert SimpleError();
}
revert ErrorWithArguments(1, false);
}
string private state;
function revertsTransaction() public {
state = "updated state";
revert ErrorWithArguments(1, true);
}
}

View File

@ -1,22 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestGasEstimation {
uint lastCheckedTime;
// this function returns a different value depending on whether
// it is called on the latest block, or on the pending block
function getTime() public view returns (uint) {
return block.timestamp;
}
// this function is designed to require a different amount of
// gas, depending on whether the parameter matches the block
// timestamp
function checkTimeEquals(uint expected) public {
if (expected == block.timestamp) {
lastCheckedTime = block.timestamp;
}
}
}

View File

@ -3,8 +3,7 @@ pragma solidity ^0.8.0;
contract TestHelpers { contract TestHelpers {
function doRevert(string calldata reason) public pure { function revertsWith(string calldata revertReason) public pure {
// Revert every tx with given reason require(false, revertReason);
require(false, reason);
} }
} }

View File

@ -11,9 +11,6 @@ contract TestReturns {
uint256 b; uint256 b;
} }
StaticStruct public staticVariable = StaticStruct(1, 2);
DynamicStruct public dynamicVariable = DynamicStruct("3", 4);
function getStatic() external pure returns (StaticStruct memory) { function getStatic() external pure returns (StaticStruct memory) {
return StaticStruct(1, 2); return StaticStruct(1, 2);
} }

View File

@ -1,6 +0,0 @@
module.exports = async ({ deployments, getNamedAccounts }) => {
const { deployer } = await getNamedAccounts();
await deployments.deploy("TestCustomErrors", { from: deployer });
};
module.exports.tags = ["TestCustomErrors"];

View File

@ -1,6 +0,0 @@
module.exports = async ({ deployments, getNamedAccounts }) => {
const { deployer } = await getNamedAccounts();
await deployments.deploy("TestGasEstimation", { from: deployer });
};
module.exports.tags = ["TestGasEstimation"];

View File

@ -2,7 +2,7 @@ require("hardhat-deploy")
require("hardhat-deploy-ethers") require("hardhat-deploy-ethers")
module.exports = { module.exports = {
solidity: "0.8.24", solidity: "0.8.11",
namedAccounts: { namedAccounts: {
deployer: { default: 0 } deployer: { default: 0 }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,10 @@
"name": "hardhat-project", "name": "hardhat-project",
"devDependencies": { "devDependencies": {
"@openzeppelin/contracts": "^4.4.2", "@openzeppelin/contracts": "^4.4.2",
"ethers": "^6.11.1", "ethers": "^5.5.3",
"hardhat": "^2.22.1", "hardhat": "^2.8.3",
"hardhat-deploy": "^0.11.34", "hardhat-deploy": "^0.9.24",
"hardhat-deploy-ethers": "^0.4.1" "hardhat-deploy-ethers": "^0.3.0-beta.13"
}, },
"scripts": { "scripts": {
"start": "hardhat node --export 'deployment.json'" "start": "hardhat node --export 'deployment.json'"