mirror of
https://github.com/logos-storage/nim-ethers.git
synced 2026-01-09 09:03:08 +00:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
965b8cd752 | ||
|
|
30871c7b1d | ||
|
|
bbced46733 | ||
|
|
c85192ae34 | ||
|
|
f9d115ae75 | ||
|
|
a29e86bfc8 | ||
|
|
4441050c3d | ||
|
|
e37f454761 | ||
|
|
def12bfdc1 | ||
|
|
51aa7bc1b3 | ||
|
|
518afa3e4c | ||
|
|
af3d7379c8 | ||
|
|
7081e6922f | ||
|
|
5d07b5dbcf | ||
|
|
b505ef1ab8 | ||
|
|
d2b11a8657 | ||
|
|
26342d3e27 | ||
|
|
0f98528758 | ||
|
|
c7c57113ce | ||
|
|
037bef0256 | ||
|
|
04d3548553 | ||
|
|
2808a05488 | ||
|
|
5c93971f97 | ||
|
|
c0cc437aa2 | ||
|
|
4642545309 | ||
|
|
d88e4614b1 | ||
|
|
04c00e2d91 | ||
|
|
1ae2cd4a35 | ||
|
|
e9d862ceca | ||
|
|
35aebdb46f | ||
|
|
f15d55f513 | ||
|
|
c6a59b5187 | ||
|
|
5a9895b792 | ||
|
|
c9275b1f6c | ||
|
|
40dee9b525 | ||
|
|
0ce6abf0fe | ||
|
|
80b2ead97c | ||
|
|
d60cedbb98 | ||
|
|
4607817057 | ||
|
|
6523e70eaf | ||
|
|
765379a662 | ||
|
|
b68bea9909 | ||
|
|
507ac6a4cc | ||
|
|
53e596e75a | ||
|
|
e15974eb1f | ||
|
|
17c6e9a8c5 | ||
|
|
2da59a86c3 | ||
|
|
131316de08 | ||
|
|
ab10354910 | ||
|
|
cdb230d30f | ||
|
|
067e0f2eb7 | ||
|
|
a6f136afdd | ||
|
|
9cb033e865 | ||
|
|
241ce6e8f3 | ||
|
|
1ce9824738 | ||
|
|
80fcb246f6 | ||
|
|
ce63c375f7 | ||
|
|
955ac2d58f | ||
|
|
6d6777e8c3 | ||
|
|
9c76803302 | ||
|
|
74f15fca9c | ||
|
|
6b57e56a39 | ||
|
|
875900b493 | ||
|
|
52d7d3dbed | ||
|
|
027b5c37ad | ||
|
|
958d7b45d1 | ||
|
|
6393546ad6 | ||
|
|
4ad5b6065e | ||
|
|
bcb539148a | ||
|
|
14a7485a88 | ||
|
|
b78463a299 | ||
|
|
942fe034fc | ||
|
|
4a57089ed2 | ||
|
|
877ff82ef6 | ||
|
|
af5a0f5fb4 | ||
|
|
d46f5a10d3 | ||
|
|
7911ac6c57 | ||
|
|
e8196b3c82 | ||
|
|
43500c63d7 | ||
|
|
fd16d71ea5 | ||
|
|
c25de86656 | ||
|
|
04b91d9f65 | ||
|
|
abe8585f53 | ||
|
|
16b28f4535 | ||
|
|
2428b756d6 | ||
|
|
7eac8410af | ||
|
|
620b402a7d | ||
|
|
f0303473f6 | ||
|
|
8fff63102a | ||
|
|
15ed76ebed | ||
|
|
43041e7948 | ||
|
|
81ec482fca | ||
|
|
2ec0313dd3 | ||
|
|
9327294044 | ||
|
|
2b6f7b7a0d | ||
|
|
99c225caa1 | ||
|
|
9f4f762e21 | ||
|
|
12d7a35203 | ||
|
|
c49311fca2 | ||
|
|
5f820fc971 | ||
|
|
2b181aa0f7 | ||
|
|
5ed3f15706 | ||
|
|
d7b7f67afb | ||
|
|
842bf4d0a2 | ||
|
|
f1a1221d14 | ||
|
|
c89701016a | ||
|
|
5127991117 | ||
|
|
e086b71b42 | ||
|
|
310b06dfe8 | ||
|
|
cd32dffc73 | ||
|
|
09810e73ff | ||
|
|
4e4a55b13e | ||
|
|
cb95cbc15a | ||
|
|
0674548ecc | ||
|
|
82f6449374 | ||
|
|
738c6a87e2 | ||
|
|
a27c2de41c | ||
|
|
f8cac08cde | ||
|
|
ceedf03c82 | ||
|
|
738d028fe3 | ||
|
|
7e346914c0 | ||
|
|
2481bda6e4 | ||
|
|
0aea16047c | ||
|
|
76bd3090d1 | ||
|
|
1b151d589d | ||
|
|
88d60b14b0 | ||
|
|
0322ae1451 | ||
|
|
50cfd9d9dd | ||
|
|
3a76fa74f1 | ||
|
|
beac903a3f | ||
|
|
6a034870f8 | ||
|
|
127c9c9b0d | ||
|
|
16fa0cfcf8 | ||
|
|
a7dc0ac9eb | ||
|
|
67c2d631d7 | ||
|
|
f0ac7065ed | ||
|
|
0b951ce146 | ||
|
|
34b7a82565 | ||
|
|
0321e6d7bd | ||
|
|
18e225607c | ||
|
|
5a4f786757 | ||
|
|
1ca90d0b3c | ||
|
|
3c12a65769 | ||
|
|
577e02b8a2 | ||
|
|
e462649aec | ||
|
|
e8592bb922 | ||
|
|
7d2acd65e8 | ||
|
|
a62ea4fb8f | ||
|
|
c5a40e5f9d | ||
|
|
f545169331 | ||
|
|
cac6026b34 | ||
|
|
d001ee8e01 | ||
|
|
fc3cc9c577 | ||
|
|
bbabee3727 | ||
|
|
8a484299e6 | ||
|
|
4d7e40eb0e | ||
|
|
01d277f801 | ||
|
|
31ffc8992f | ||
|
|
ae2d33aacd | ||
|
|
0adf56c65b | ||
|
|
e1a1a3805b | ||
|
|
5fe41a76ab | ||
|
|
f8ba91a297 | ||
|
|
e0ac15b3ba |
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@ -1,31 +1,53 @@
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
nim: [1.2.16, stable]
|
||||
nim: [2.0.14, 2.2.2]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install Nim
|
||||
uses: iffy/install-nim@v3
|
||||
uses: iffy/install-nim@v4
|
||||
with:
|
||||
version: ${{ matrix.nim }}
|
||||
|
||||
- name: Install NodeJS
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: 18
|
||||
|
||||
- name: Install test node
|
||||
working-directory: testnode
|
||||
run: npm install
|
||||
|
||||
- name: Run test node
|
||||
working-directory: testnode
|
||||
run: npm start &
|
||||
|
||||
- name: Build
|
||||
run: nimble install -y
|
||||
run: nimble install -y --maximumtaggedversions=2
|
||||
|
||||
- name: Test
|
||||
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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,3 +3,7 @@
|
||||
!*.*
|
||||
nimble.develop
|
||||
nimble.paths
|
||||
.idea
|
||||
.nimble
|
||||
.envrc
|
||||
nimbledeps
|
||||
|
||||
112
Readme.md
112
Readme.md
@ -14,7 +14,13 @@ Use the [Nimble][2] package manager to add `ethers` to an existing
|
||||
project. Add the following to its .nimble file:
|
||||
|
||||
```nim
|
||||
requires "ethers >= 0.2.1 & < 0.3.0"
|
||||
requires "ethers >= 2.0.0 & < 3.0.0"
|
||||
```
|
||||
|
||||
To avoid conflicts with previous versions of `contractabi`, use the following command to install dependencies:
|
||||
|
||||
```bash
|
||||
nimble install --maximumtaggedversions=2
|
||||
```
|
||||
|
||||
Usage
|
||||
@ -90,6 +96,12 @@ await writableToken.transfer(accounts[7], 42.u256)
|
||||
|
||||
Which transfers 42 tokens from account 3 to account 7
|
||||
|
||||
And lastly, don't forget to close the provider when you're done:
|
||||
|
||||
```nim
|
||||
await provider.close()
|
||||
```
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
@ -106,18 +118,41 @@ type Transfer = object of Event
|
||||
Notice that `Transfer` inherits from `Event`, and that some event parameters are
|
||||
marked with `{.indexed.}` to match the definition in Solidity.
|
||||
|
||||
Note that valid types of indexed parameters are:
|
||||
```nim
|
||||
uint8 | uint16 | uint32 | uint64 | UInt256 | UInt128 |
|
||||
int8 | int16 | int32 | int64 | Int256 | Int128 |
|
||||
bool | Address | array[ 1..32, byte]
|
||||
```
|
||||
Distinct types of valid types are also supported for indexed fields, eg:
|
||||
```nim
|
||||
type
|
||||
DistinctAlias = distinct array[32, byte]
|
||||
MyEvent = object of Event
|
||||
a {.indexed.}: DistinctAlias
|
||||
b: DistinctAlias # also allowed for non-indexed fields
|
||||
```
|
||||
|
||||
You can now subscribe to Transfer events by calling `subscribe` on the contract
|
||||
instance.
|
||||
|
||||
```nim
|
||||
proc handleTransfer(transfer: Transfer) =
|
||||
echo "received transfer: ", transfer
|
||||
proc handleTransfer(transferResult: ?!Transfer) =
|
||||
if transferResult.isOk:
|
||||
echo "received transfer: ", transferResult.value
|
||||
else:
|
||||
echo "error during transfer: ", transferResult.error.msg
|
||||
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
```
|
||||
|
||||
When a Transfer event is emitted, the `handleTransfer` proc that you just
|
||||
defined will be called.
|
||||
defined will be called with a [Result](https://github.com/arnetheduck/nim-results) type
|
||||
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:
|
||||
|
||||
@ -125,8 +160,71 @@ When you're no longer interested in these events, you can unsubscribe:
|
||||
await subscription.unsubscribe()
|
||||
```
|
||||
|
||||
Subscriptions are currently only supported when using a JSON RPC provider that
|
||||
is created with a websockets URL such as `ws://localhost:8545`.
|
||||
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
|
||||
---------
|
||||
|
||||
This library ships with some optional modules that provides convenience utilities for you such as:
|
||||
|
||||
- `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
|
||||
------------
|
||||
|
||||
If you want to run the tests, then before running `nimble test`, you have to
|
||||
have installed NodeJS and started a testing node:
|
||||
|
||||
```shell
|
||||
$ cd testnode
|
||||
$ npm ci
|
||||
$ 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
|
||||
------
|
||||
@ -138,3 +236,5 @@ affiliation) and [nim-web3][1] developers.
|
||||
[1]: https://github.com/status-im/nim-web3
|
||||
[2]: https://github.com/nim-lang/nimble
|
||||
[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/
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
--styleCheck:usages
|
||||
--styleCheck:error
|
||||
|
||||
# begin Nimble config (version 1)
|
||||
when fileExists("nimble.paths"):
|
||||
include "nimble.paths"
|
||||
# end Nimble config
|
||||
|
||||
when (NimMajor, NimMinor) >= (2, 0):
|
||||
--mm:refc
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ./ethers/provider
|
||||
import ./ethers/signer
|
||||
import ./ethers/providers/jsonrpc
|
||||
import ./ethers/contract
|
||||
import ./ethers/contracts
|
||||
import ./ethers/wallet
|
||||
|
||||
export provider
|
||||
export signer
|
||||
export jsonrpc
|
||||
export contract
|
||||
export wallet
|
||||
export contracts
|
||||
export wallet
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
version = "0.2.1"
|
||||
version = "2.1.0"
|
||||
author = "Nim Ethers Authors"
|
||||
description = "library for interacting with Ethereum"
|
||||
license = "MIT"
|
||||
|
||||
requires "chronos >= 3.0.0 & < 4.0.0"
|
||||
requires "contractabi >= 0.4.5 & < 0.5.0"
|
||||
requires "nim >= 2.0.14"
|
||||
requires "chronicles >= 0.10.3 & < 0.13.0"
|
||||
requires "chronos >= 4.0.4 & < 4.1.0"
|
||||
requires "contractabi >= 0.7.2 & < 0.8.0"
|
||||
requires "questionable >= 0.10.2 & < 0.11.0"
|
||||
requires "upraises >= 0.1.0 & < 0.2.0"
|
||||
requires "json_rpc"
|
||||
requires "stint"
|
||||
requires "stew"
|
||||
requires "eth"
|
||||
requires "json_rpc >= 0.5.0 & < 0.6.0"
|
||||
requires "serde >= 1.2.1 & < 1.3.0"
|
||||
requires "stint >= 0.8.1 & < 0.9.0"
|
||||
requires "stew >= 0.2.0"
|
||||
requires "eth >= 0.6.0 & < 0.10.0"
|
||||
|
||||
task test, "Run the test suite":
|
||||
exec "nimble install -d -y"
|
||||
# exec "nimble install -d -y"
|
||||
withDir "testmodule":
|
||||
exec "nimble test"
|
||||
|
||||
@ -2,14 +2,12 @@ import pkg/chronos
|
||||
import pkg/questionable
|
||||
import pkg/questionable/results
|
||||
import pkg/stint
|
||||
import pkg/upraises
|
||||
import pkg/contractabi/address
|
||||
|
||||
export chronos
|
||||
export questionable
|
||||
export results
|
||||
export stint
|
||||
export upraises
|
||||
export address
|
||||
|
||||
type
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import pkg/stint
|
||||
import pkg/upraises
|
||||
import pkg/questionable
|
||||
|
||||
push: {.upraises: [].}
|
||||
{.push raises:[].}
|
||||
|
||||
type
|
||||
BlockTagKind = enum
|
||||
@ -35,3 +35,17 @@ func `$`*(blockTag: BlockTag): string =
|
||||
blockTag.stringValue
|
||||
of numberBlockTag:
|
||||
"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
|
||||
|
||||
@ -1,217 +0,0 @@
|
||||
import std/macros
|
||||
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* = object
|
||||
nonce*: ?UInt256
|
||||
chainId*: ?UInt256
|
||||
gasPrice*: ?UInt256
|
||||
maxFee*: ?UInt256
|
||||
maxPriorityFee*: ?UInt256
|
||||
gasLimit*: ?UInt256
|
||||
|
||||
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.default): 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(contract: Contract,
|
||||
function: string,
|
||||
parameters: tuple,
|
||||
blockTag = BlockTag.latest) {.async.} =
|
||||
let transaction = createTransaction(contract, function, parameters)
|
||||
discard await contract.provider.call(transaction, blockTag)
|
||||
|
||||
proc call(contract: Contract,
|
||||
function: string,
|
||||
parameters: tuple,
|
||||
ReturnType: type,
|
||||
returnMultiple: static bool,
|
||||
blockTag = BlockTag.latest): Future[ReturnType] {.async.} =
|
||||
let transaction = createTransaction(contract, function, parameters)
|
||||
let response = await contract.provider.call(transaction, blockTag)
|
||||
return decodeResponse(ReturnType, returnMultiple, response)
|
||||
|
||||
proc send(contract: Contract,
|
||||
function: string,
|
||||
parameters: tuple,
|
||||
overrides = TransactionOverrides.default):
|
||||
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.default
|
||||
)
|
||||
)
|
||||
|
||||
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`)
|
||||
else:
|
||||
quote:
|
||||
return await call(
|
||||
`contract`, `function`, `parameters`, `returnType`, `returnMultiple`)
|
||||
|
||||
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 = Filter(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)
|
||||
31
ethers/contracts.nim
Normal file
31
ethers/contracts.nim
Normal file
@ -0,0 +1,31 @@
|
||||
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)
|
||||
)
|
||||
45
ethers/contracts/confirmation.nim
Normal file
45
ethers/contracts/confirmation.nim
Normal file
@ -0,0 +1,45 @@
|
||||
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
|
||||
)
|
||||
|
||||
36
ethers/contracts/contract.nim
Normal file
36
ethers/contracts/contract.nim
Normal file
@ -0,0 +1,36 @@
|
||||
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
|
||||
|
||||
35
ethers/contracts/contractcall.nim
Normal file
35
ethers/contracts/contractcall.nim
Normal file
@ -0,0 +1,35 @@
|
||||
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
|
||||
29
ethers/contracts/errors.nim
Normal file
29
ethers/contracts/errors.nim
Normal file
@ -0,0 +1,29 @@
|
||||
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
|
||||
15
ethers/contracts/errors/conversion.nim
Normal file
15
ethers/contracts/errors/conversion.nim
Normal file
@ -0,0 +1,15 @@
|
||||
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
|
||||
37
ethers/contracts/errors/encoding.nim
Normal file
37
ethers/contracts/errors/encoding.nim
Normal file
@ -0,0 +1,37 @@
|
||||
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)
|
||||
|
||||
49
ethers/contracts/events.nim
Normal file
49
ethers/contracts/events.nim
Normal file
@ -0,0 +1,49 @@
|
||||
import std/macros
|
||||
import pkg/contractabi
|
||||
import ../basics
|
||||
import ../provider
|
||||
|
||||
type
|
||||
Event* = object of RootObj
|
||||
|
||||
{.push raises:[].}
|
||||
|
||||
template indexed* {.pragma.}
|
||||
|
||||
func decode*[E: Event](decoder: var AbiDecoder, _: type E): ?!E =
|
||||
var event: E
|
||||
decoder.startTuple()
|
||||
for field in event.fields:
|
||||
if not field.hasCustomPragma(indexed):
|
||||
field = ?decoder.read(typeof(field))
|
||||
decoder.finishTuple()
|
||||
success event
|
||||
|
||||
func fitsInIndexedField(T: type): bool {.compileTime.} =
|
||||
const supportedTypes = [
|
||||
"uint8", "uint16", "uint32", "uint64", "uint256", "uint128",
|
||||
"int8", "int16", "int32", "int64", "int256", "int128",
|
||||
"bool", "address",
|
||||
"bytes1", "bytes2", "bytes3", "bytes4",
|
||||
"bytes5", "bytes6", "bytes7", "bytes8",
|
||||
"bytes9", "bytes10", "bytes11", "bytes12",
|
||||
"bytes13", "bytes14", "bytes15", "bytes16",
|
||||
"bytes17", "bytes18", "bytes19", "bytes20",
|
||||
"bytes21", "bytes22", "bytes23", "bytes24",
|
||||
"bytes25", "bytes26", "bytes27", "bytes28",
|
||||
"bytes29", "bytes30", "bytes31", "bytes32"
|
||||
]
|
||||
|
||||
solidityType(T) in supportedTypes
|
||||
|
||||
func decode*[E: Event](_: type E, data: seq[byte], topics: seq[Topic]): ?!E =
|
||||
var event = ?AbiDecoder.decode(data, E)
|
||||
var i = 1
|
||||
for field in event.fields:
|
||||
if field.hasCustomPragma(indexed):
|
||||
if i >= topics.len:
|
||||
return failure "indexed event parameter not found"
|
||||
when typeof(field).fitsInIndexedField:
|
||||
field = ?AbiDecoder.decode(@(topics[i]), typeof(field))
|
||||
inc i
|
||||
success event
|
||||
78
ethers/contracts/filters.nim
Normal file
78
ethers/contracts/filters.nim
Normal file
@ -0,0 +1,78 @@
|
||||
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)
|
||||
60
ethers/contracts/function.nim
Normal file
60
ethers/contracts/function.nim
Normal file
@ -0,0 +1,60 @@
|
||||
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()
|
||||
|
||||
51
ethers/contracts/gas.nim
Normal file
51
ethers/contracts/gas.nim
Normal file
@ -0,0 +1,51 @@
|
||||
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()
|
||||
13
ethers/contracts/overrides.nim
Normal file
13
ethers/contracts/overrides.nim
Normal file
@ -0,0 +1,13 @@
|
||||
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
|
||||
76
ethers/contracts/syntax.nim
Normal file
76
ethers/contracts/syntax.nim
Normal file
@ -0,0 +1,76 @@
|
||||
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)
|
||||
71
ethers/contracts/transactions.nim
Normal file
71
ethers/contracts/transactions.nim
Normal file
@ -0,0 +1,71 @@
|
||||
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
|
||||
|
||||
80
ethers/erc20.nim
Normal file
80
ethers/erc20.nim
Normal file
@ -0,0 +1,80 @@
|
||||
import pkg/stint
|
||||
import pkg/ethers
|
||||
|
||||
export stint
|
||||
export ethers
|
||||
|
||||
type
|
||||
Erc20Token* = ref object of Contract
|
||||
|
||||
Transfer* = object of Event
|
||||
sender* {.indexed.}: Address
|
||||
receiver* {.indexed.}: Address
|
||||
value*: UInt256
|
||||
|
||||
Approval* = object of Event
|
||||
owner* {.indexed.}: Address
|
||||
spender* {.indexed.}: Address
|
||||
value*: UInt256
|
||||
|
||||
method name*(token: Erc20Token): string {.base, contract, view.}
|
||||
## Returns the name of the token.
|
||||
|
||||
method symbol*(token: Erc20Token): string {.base, contract, view.}
|
||||
## Returns the symbol of the token, usually a shorter version of the name.
|
||||
|
||||
method decimals*(token: Erc20Token): uint8 {.base, contract, view.}
|
||||
## Returns the number of decimals used to get its user representation.
|
||||
## For example, if `decimals` equals `2`, a balance of `505` tokens should
|
||||
## be displayed to a user as `5.05` (`505 / 10 ** 2`).
|
||||
|
||||
method totalSupply*(token: Erc20Token): UInt256 {.base, contract, view.}
|
||||
## Returns the amount of tokens in existence.
|
||||
|
||||
method balanceOf*(token: Erc20Token,
|
||||
account: Address): UInt256 {.base, contract, view.}
|
||||
## Returns the amount of tokens owned by `account`.
|
||||
|
||||
method allowance*(token: Erc20Token,
|
||||
owner: Address,
|
||||
spender: Address): UInt256 {.base, contract, view.}
|
||||
## Returns the remaining number of tokens that `spender` will be allowed
|
||||
## to spend on behalf of `owner` through {transferFrom}. This is zero by
|
||||
## default.
|
||||
##
|
||||
## This value changes when {approve} or {transferFrom} are called.
|
||||
|
||||
method transfer*(token: Erc20Token,
|
||||
recipient: Address,
|
||||
amount: UInt256): Confirmable {.base, contract.}
|
||||
## Moves `amount` tokens from the caller's account to `recipient`.
|
||||
|
||||
method approve*(token: Erc20Token,
|
||||
spender: Address,
|
||||
amount: UInt256): Confirmable {.base, contract.}
|
||||
## 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,
|
||||
spender: Address,
|
||||
recipient: Address,
|
||||
amount: UInt256): Confirmable {.base, contract.}
|
||||
## Moves `amount` tokens from `spender` to `recipient` using the allowance
|
||||
## mechanism. `amount` is then deducted from the caller's allowance.
|
||||
18
ethers/errors.nim
Normal file
18
ethers/errors.nim
Normal file
@ -0,0 +1,18 @@
|
||||
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)
|
||||
@ -1,46 +0,0 @@
|
||||
import std/macros
|
||||
import pkg/contractabi
|
||||
import ./basics
|
||||
import ./provider
|
||||
|
||||
type
|
||||
Event* = object of RootObj
|
||||
ValueType = uint8 | uint16 | uint32 | uint64 | UInt256 | UInt128 |
|
||||
int8 | int16 | int32 | int64 | Int256 | Int128 |
|
||||
bool | Address
|
||||
SmallByteArray = array[ 1, byte] | array[ 2, byte] | array[ 3, byte] |
|
||||
array[ 4, byte] | array[ 5, byte] | array[ 6, byte] |
|
||||
array[ 7, byte] | array[ 8, byte] | array[ 9, byte] |
|
||||
array[10, byte] | array[11, byte] | array[12, byte] |
|
||||
array[13, byte] | array[14, byte] | array[15, byte] |
|
||||
array[16, byte] | array[17, byte] | array[18, byte] |
|
||||
array[19, byte] | array[20, byte] | array[21, byte] |
|
||||
array[22, byte] | array[23, byte] | array[24, byte] |
|
||||
array[25, byte] | array[26, byte] | array[27, byte] |
|
||||
array[28, byte] | array[29, byte] | array[30, byte] |
|
||||
array[31, byte] | array[32, byte]
|
||||
|
||||
push: {.upraises: [].}
|
||||
|
||||
template indexed* {.pragma.}
|
||||
|
||||
func decode*[E: Event](decoder: var AbiDecoder, _: type E): ?!E =
|
||||
var event: E
|
||||
decoder.startTuple()
|
||||
for field in event.fields:
|
||||
if not field.hasCustomPragma(indexed):
|
||||
field = ?decoder.read(typeof(field))
|
||||
decoder.finishTuple()
|
||||
success event
|
||||
|
||||
func decode*[E: Event](_: type E, data: seq[byte], topics: seq[Topic]): ?!E =
|
||||
var event = ?Abidecoder.decode(data, E)
|
||||
var i = 1
|
||||
for field in event.fields:
|
||||
if field.hasCustomPragma(indexed):
|
||||
if i >= topics.len:
|
||||
return failure "indexed event parameter not found"
|
||||
if typeof(field) is ValueType or typeof(field) is SmallByteArray:
|
||||
field = ?AbiDecoder.decode(@(topics[i]), typeof(field))
|
||||
inc i
|
||||
success event
|
||||
49
ethers/nimshims/hashes.nim
Normal file
49
ethers/nimshims/hashes.nim
Normal file
@ -0,0 +1,49 @@
|
||||
## 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
|
||||
@ -1,21 +1,36 @@
|
||||
import pkg/chronicles
|
||||
import pkg/serde
|
||||
import pkg/questionable
|
||||
import ./basics
|
||||
import ./transaction
|
||||
import ./blocktag
|
||||
import ./errors
|
||||
|
||||
export basics
|
||||
export transaction
|
||||
export blocktag
|
||||
export errors
|
||||
|
||||
push: {.upraises: [].}
|
||||
{.push raises: [].}
|
||||
|
||||
type
|
||||
Provider* = ref object of RootObj
|
||||
EstimateGasError* = object of ProviderError
|
||||
transaction*: Transaction
|
||||
Subscription* = ref object of RootObj
|
||||
Filter* = object
|
||||
EventFilter* {.serialize.} = ref object of RootObj
|
||||
address*: Address
|
||||
topics*: seq[Topic]
|
||||
Log* = object
|
||||
Filter* {.serialize.} = ref object of EventFilter
|
||||
fromBlock*: BlockTag
|
||||
toBlock*: BlockTag
|
||||
FilterByBlockHash* {.serialize.} = ref object of EventFilter
|
||||
blockHash*: BlockHash
|
||||
Log* {.serialize.} = object
|
||||
blockNumber*: UInt256
|
||||
data*: seq[byte]
|
||||
logIndex*: UInt256
|
||||
removed*: bool
|
||||
topics*: seq[Topic]
|
||||
TransactionHash* = array[32, byte]
|
||||
BlockHash* = array[32, byte]
|
||||
@ -25,9 +40,9 @@ type
|
||||
Invalid = 2
|
||||
TransactionResponse* = object
|
||||
provider*: Provider
|
||||
hash*: TransactionHash
|
||||
TransactionReceipt* = object
|
||||
sender*: ?Address
|
||||
hash* {.serialize.}: TransactionHash
|
||||
TransactionReceipt* {.serialize.} = object
|
||||
sender* {.serialize("from"), deserialize("from").}: ?Address
|
||||
to*: ?Address
|
||||
contractAddress*: ?Address
|
||||
transactionIndex*: UInt256
|
||||
@ -38,172 +53,270 @@ type
|
||||
logs*: seq[Log]
|
||||
blockNumber*: ?UInt256
|
||||
cumulativeGasUsed*: UInt256
|
||||
effectiveGasPrice*: ?UInt256
|
||||
status*: TransactionStatus
|
||||
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
|
||||
BlockHandler* = proc(blck: Block): Future[void] {.gcsafe, upraises:[].}
|
||||
transactionType* {.serialize("type"), deserialize("type").}: TransactionType
|
||||
LogHandler* = proc(log: ?!Log) {.gcsafe, raises:[].}
|
||||
BlockHandler* = proc(blck: ?!Block) {.gcsafe, raises:[].}
|
||||
Topic* = array[32, byte]
|
||||
Block* = object
|
||||
Block* {.serialize.} = object
|
||||
number*: ?UInt256
|
||||
timestamp*: UInt256
|
||||
hash*: array[32, byte]
|
||||
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 EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks
|
||||
|
||||
method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} =
|
||||
logScope:
|
||||
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"
|
||||
|
||||
method getBlock*(provider: Provider, tag: BlockTag): Future[?Block] {.base.} =
|
||||
method getBlock*(
|
||||
provider: Provider, tag: BlockTag
|
||||
): Future[?Block] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method call*(provider: Provider,
|
||||
tx: Transaction,
|
||||
blockTag = BlockTag.latest): Future[seq[byte]] {.base.} =
|
||||
method call*(
|
||||
provider: Provider, tx: Transaction, blockTag = BlockTag.latest
|
||||
): Future[seq[byte]] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method getGasPrice*(provider: Provider): Future[UInt256] {.base.} =
|
||||
method getGasPrice*(
|
||||
provider: Provider
|
||||
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method getTransactionCount*(provider: Provider,
|
||||
address: Address,
|
||||
blockTag = BlockTag.latest):
|
||||
Future[UInt256] {.base.} =
|
||||
method getMaxPriorityFeePerGas*(
|
||||
provider: Provider
|
||||
): Future[UInt256] {.base, async: (raises: [CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method getTransactionReceipt*(provider: Provider,
|
||||
txHash: TransactionHash):
|
||||
Future[?TransactionReceipt] {.base.} =
|
||||
method getTransactionCount*(
|
||||
provider: Provider, address: Address, blockTag = BlockTag.latest
|
||||
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method sendTransaction*(provider: Provider,
|
||||
rawTransaction: seq[byte]):
|
||||
Future[TransactionResponse] {.base.} =
|
||||
method getTransaction*(
|
||||
provider: Provider, txHash: TransactionHash
|
||||
): Future[?PastTransaction] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method estimateGas*(provider: Provider,
|
||||
transaction: Transaction): Future[UInt256] {.base.} =
|
||||
method getTransactionReceipt*(
|
||||
provider: Provider, txHash: TransactionHash
|
||||
): Future[?TransactionReceipt] {.
|
||||
base, async: (raises: [ProviderError, CancelledError])
|
||||
.} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method getChainId*(provider: Provider): Future[UInt256] {.base.} =
|
||||
method sendTransaction*(
|
||||
provider: Provider, rawTransaction: seq[byte]
|
||||
): Future[TransactionResponse] {.
|
||||
base, async: (raises: [ProviderError, CancelledError])
|
||||
.} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method subscribe*(provider: Provider,
|
||||
filter: Filter,
|
||||
callback: LogHandler):
|
||||
Future[Subscription] {.base.} =
|
||||
method getLogs*(
|
||||
provider: Provider, filter: EventFilter
|
||||
): Future[seq[Log]] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method subscribe*(provider: Provider,
|
||||
callback: BlockHandler):
|
||||
Future[Subscription] {.base.} =
|
||||
method estimateGas*(
|
||||
provider: Provider, transaction: Transaction, blockTag = BlockTag.latest
|
||||
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method unsubscribe*(subscription: Subscription) {.base, async.} =
|
||||
method getChainId*(
|
||||
provider: Provider
|
||||
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
# Removed from `confirm` closure and exported so it can be tested.
|
||||
# Likely there is a better way
|
||||
func confirmations*(receiptBlk, atBlk: UInt256): UInt256 =
|
||||
## Calculates the number of confirmations between two blocks
|
||||
if atBlk < receiptBlk:
|
||||
return 0.u256
|
||||
else:
|
||||
return (atBlk - receiptBlk) + 1 # add 1 for current block
|
||||
method subscribe*(
|
||||
provider: Provider, filter: EventFilter, callback: LogHandler
|
||||
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
# Removed from `confirm` closure and exported so it can be tested.
|
||||
# Likely there is a better way
|
||||
func hasBeenMined*(receipt: TransactionReceipt,
|
||||
atBlock: UInt256,
|
||||
wantedConfirms: int): bool =
|
||||
## Returns true if the transaction receipt has been returned from the node
|
||||
## with a valid block number and block hash and the specified number of
|
||||
## blocks have passed since the tx was mined (confirmations)
|
||||
method subscribe*(
|
||||
provider: Provider, callback: BlockHandler
|
||||
): Future[Subscription] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
if number =? receipt.blockNumber and
|
||||
number > 0 and
|
||||
# from ethers.js: "geth-etc" returns receipts before they are ready
|
||||
receipt.blockHash.isSome:
|
||||
method unsubscribe*(
|
||||
subscription: Subscription
|
||||
) {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
return number.confirmations(atBlock) >= wantedConfirms.u256
|
||||
method isSyncing*(provider: Provider): Future[bool] {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
return false
|
||||
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,
|
||||
timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt]
|
||||
{.async: (raises: [CancelledError, ProviderError, SubscriptionError, EthersError]).} =
|
||||
|
||||
proc confirm*(tx: TransactionResponse,
|
||||
wantedConfirms: Positive = EthersDefaultConfirmations,
|
||||
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
||||
Future[TransactionReceipt]
|
||||
{.async, upraises: [EthersError].} = # raises for clarity
|
||||
## Waits for a transaction to be mined and for the specified number of blocks
|
||||
## to pass since it was mined (confirmations).
|
||||
## to pass since it was mined (confirmations). The number of 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
|
||||
## blocks have passed without the tx having been mined.
|
||||
|
||||
var subscription: Subscription
|
||||
let
|
||||
provider = tx.provider
|
||||
retFut = newFuture[TransactionReceipt]("wait")
|
||||
assert confirmations > 0
|
||||
|
||||
# used to check for block timeouts
|
||||
let startBlock = await provider.getBlockNumber()
|
||||
var blockNumber: UInt256
|
||||
|
||||
proc newBlock(blk: Block) {.async.} =
|
||||
## subscription callback, called every time a new block event is sent from
|
||||
## the node
|
||||
## 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()
|
||||
|
||||
# if ethereum node doesn't include blockNumber in the event
|
||||
without blkNum =? blk.number:
|
||||
proc updateBlockNumber {.async: (raises: []).} =
|
||||
try:
|
||||
let number = await tx.provider.getBlockNumber()
|
||||
if number > blockNumber:
|
||||
blockNumber = number
|
||||
blockEvent.fire()
|
||||
except ProviderError, CancelledError:
|
||||
# there's nothing we can do here
|
||||
discard
|
||||
|
||||
proc onBlock(blckResult: ?!Block) =
|
||||
blockSubscriptionResult = blckResult
|
||||
|
||||
if blckResult.isErr:
|
||||
blockEvent.fire()
|
||||
return
|
||||
|
||||
if receipt =? (await provider.getTransactionReceipt(tx.hash)) and
|
||||
receipt.hasBeenMined(blkNum, wantedConfirms):
|
||||
# fire and forget
|
||||
discard subscription.unsubscribe()
|
||||
if not retFut.finished:
|
||||
retFut.complete(receipt)
|
||||
# ignore block parameter; hardhat may call this with pending blocks
|
||||
asyncSpawn updateBlockNumber()
|
||||
|
||||
elif timeoutInBlocks > 0:
|
||||
let blocksPassed = (blkNum - startBlock) + 1
|
||||
if blocksPassed >= timeoutInBlocks.u256:
|
||||
discard subscription.unsubscribe()
|
||||
if not retFut.finished:
|
||||
let message =
|
||||
"Transaction was not mined in " & $timeoutInBlocks & " blocks"
|
||||
retFut.fail(newException(EthersError, message))
|
||||
await updateBlockNumber()
|
||||
let subscription = await tx.provider.subscribe(onBlock)
|
||||
|
||||
# If our tx is already mined, return the receipt. Otherwise, check each
|
||||
# new block to see if the tx has been mined
|
||||
if receipt =? (await provider.getTransactionReceipt(tx.hash)) and
|
||||
receipt.hasBeenMined(startBlock, wantedConfirms):
|
||||
return receipt
|
||||
else:
|
||||
subscription = await provider.subscribe(newBlock)
|
||||
return (await retFut)
|
||||
let finish = blockNumber + timeout.u256
|
||||
var receipt: ?TransactionReceipt
|
||||
|
||||
proc confirm*(tx: Future[TransactionResponse],
|
||||
wantedConfirms: Positive = EthersDefaultConfirmations,
|
||||
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
||||
Future[TransactionReceipt] {.async.} =
|
||||
while true:
|
||||
await blockEvent.wait()
|
||||
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:
|
||||
await subscription.unsubscribe()
|
||||
raise newException(EthersError, "tx not mined before timeout")
|
||||
|
||||
if receipt.?blockNumber.isNone:
|
||||
receipt = await tx.provider.getTransactionReceipt(tx.hash)
|
||||
|
||||
without receipt =? receipt and txBlockNumber =? receipt.blockNumber:
|
||||
continue
|
||||
|
||||
if txBlockNumber + confirmations.u256 <= blockNumber + 1:
|
||||
await subscription.unsubscribe()
|
||||
await tx.provider.ensureSuccess(receipt)
|
||||
return receipt
|
||||
|
||||
proc confirm*(
|
||||
tx: Future[TransactionResponse],
|
||||
confirmations: int = EthersDefaultConfirmations,
|
||||
timeout: int = EthersReceiptTimeoutBlks): Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} =
|
||||
## Convenience method that allows wait to be chained to a sendTransaction
|
||||
## call, eg:
|
||||
## `await signer.sendTransaction(populated).confirm(3)`
|
||||
|
||||
let txResp = await tx
|
||||
return await txResp.confirm(wantedConfirms, timeoutInBlocks)
|
||||
|
||||
proc confirm*(tx: Future[?TransactionResponse],
|
||||
wantedConfirms: Positive = EthersDefaultConfirmations,
|
||||
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
||||
Future[TransactionReceipt] {.async.} =
|
||||
## Convenience method that allows wait to be chained to a contract
|
||||
## transaction, eg:
|
||||
## `await token.connect(signer0)
|
||||
## .mint(accounts[1], 100.u256)
|
||||
## .confirm(3)`
|
||||
|
||||
without txResp =? (await tx):
|
||||
try:
|
||||
let txResp = await tx
|
||||
return await txResp.confirm(confirmations, timeout)
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
except EthersError as e:
|
||||
raise e
|
||||
except CatchableError as e:
|
||||
raise newException(
|
||||
EthersError,
|
||||
"Transaction hash required. Possibly was a call instead of a send?"
|
||||
"Error when trying to confirm the provider transaction: " & e.msg
|
||||
)
|
||||
|
||||
return await txResp.confirm(wantedConfirms, timeoutInBlocks)
|
||||
method close*(
|
||||
provider: Provider
|
||||
) {.base, async: (raises: [ProviderError, CancelledError]).} =
|
||||
discard
|
||||
|
||||
@ -1,94 +1,121 @@
|
||||
import std/json
|
||||
import std/tables
|
||||
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/errors
|
||||
import pkg/serde
|
||||
import ../basics
|
||||
import ../provider
|
||||
import ../signer
|
||||
import ./jsonrpc/rpccalls
|
||||
import ./jsonrpc/conversions
|
||||
import ./jsonrpc/subscriptions
|
||||
import ./jsonrpc/errors
|
||||
|
||||
export basics
|
||||
export provider
|
||||
export conversions
|
||||
export chronicles
|
||||
export errors.JsonRpcProviderError
|
||||
export subscriptions
|
||||
|
||||
push: {.upraises: [].}
|
||||
{.push raises: [].}
|
||||
|
||||
logScope:
|
||||
topics = "ethers jsonrpc"
|
||||
|
||||
type
|
||||
JsonRpcProvider* = ref object of Provider
|
||||
client: Future[RpcClient]
|
||||
subscriptions: Table[JsonNode, SubscriptionHandler]
|
||||
JsonRpcSubscription = ref object of Subscription
|
||||
provider: JsonRpcProvider
|
||||
subscriptions: Future[JsonRpcSubscriptions]
|
||||
maxPriorityFeePerGas: UInt256
|
||||
|
||||
JsonRpcSubscription* = ref object of Subscription
|
||||
subscriptions: JsonRpcSubscriptions
|
||||
id: JsonNode
|
||||
|
||||
# Signer
|
||||
JsonRpcSigner* = ref object of Signer
|
||||
provider: JsonRpcProvider
|
||||
address: ?Address
|
||||
JsonRpcProviderError* = object of EthersError
|
||||
SubscriptionHandler = proc(id, arguments: JsonNode): Future[void] {.gcsafe, upraises:[].}
|
||||
|
||||
template raiseProviderError(message: string) =
|
||||
raise newException(JsonRpcProviderError, message)
|
||||
|
||||
template convertError(body) =
|
||||
try:
|
||||
body
|
||||
except JsonRpcError as error:
|
||||
raiseProviderError(error.msg)
|
||||
JsonRpcSignerError* = object of SignerError
|
||||
|
||||
# Provider
|
||||
|
||||
const defaultUrl = "http://localhost:8545"
|
||||
const defaultPollingInterval = 4.seconds
|
||||
const defaultMaxPriorityFeePerGas = 1_000_000_000.u256
|
||||
|
||||
proc connect(_: type RpcClient, url: string): Future[RpcClient] {.async.} =
|
||||
case parseUri(url).scheme
|
||||
of "ws", "wss":
|
||||
let client = newRpcWebSocketClient()
|
||||
await client.connect(url)
|
||||
return client
|
||||
else:
|
||||
let client = newRpcHttpClient()
|
||||
await client.connect(url)
|
||||
return client
|
||||
proc jsonHeaders: seq[(string, string)] =
|
||||
@[("Content-Type", "application/json")]
|
||||
|
||||
proc connect(provider: JsonRpcProvider, url: string) =
|
||||
proc new*(
|
||||
_: type JsonRpcProvider,
|
||||
url=defaultUrl,
|
||||
pollingInterval=defaultPollingInterval,
|
||||
maxPriorityFeePerGas=defaultMaxPriorityFeePerGas): JsonRpcProvider {.raises: [JsonRpcProviderError].} =
|
||||
|
||||
proc getSubscriptionHandler(id: JsonNode): ?SubscriptionHandler =
|
||||
try:
|
||||
if provider.subscriptions.hasKey(id):
|
||||
provider.subscriptions[id].some
|
||||
var initialized: Future[void]
|
||||
var client: RpcClient
|
||||
var subscriptions: JsonRpcSubscriptions
|
||||
|
||||
proc initialize() {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
case parseUri(url).scheme
|
||||
of "ws", "wss":
|
||||
let websocket = newRpcWebSocketClient(getHeaders = jsonHeaders)
|
||||
await websocket.connect(url)
|
||||
client = websocket
|
||||
subscriptions = JsonRpcSubscriptions.new(websocket)
|
||||
else:
|
||||
SubscriptionHandler.none
|
||||
except Exception:
|
||||
SubscriptionHandler.none
|
||||
let http = newRpcHttpClient(getHeaders = jsonHeaders)
|
||||
await http.connect(url)
|
||||
client = http
|
||||
subscriptions = JsonRpcSubscriptions.new(http,
|
||||
pollingInterval = pollingInterval)
|
||||
subscriptions.start()
|
||||
|
||||
proc handleSubscription(arguments: JsonNode) {.upraises: [].} =
|
||||
if id =? arguments["subscription"].catch and
|
||||
handler =? getSubscriptionHandler(id):
|
||||
# fire and forget
|
||||
discard handler(id, arguments)
|
||||
proc awaitClient(): Future[RpcClient] {.
|
||||
async: (raises: [JsonRpcProviderError, CancelledError])
|
||||
.} =
|
||||
convertError:
|
||||
await initialized
|
||||
return client
|
||||
|
||||
proc subscribe: Future[RpcClient] {.async.} =
|
||||
let client = await RpcClient.connect(url)
|
||||
client.setMethodHandler("eth_subscription", handleSubscription)
|
||||
return client
|
||||
proc awaitSubscriptions(): Future[JsonRpcSubscriptions] {.
|
||||
async: (raises: [JsonRpcProviderError, CancelledError])
|
||||
.} =
|
||||
convertError:
|
||||
await initialized
|
||||
return subscriptions
|
||||
|
||||
provider.client = subscribe()
|
||||
initialized = initialize()
|
||||
return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions(), maxPriorityFeePerGas: maxPriorityFeePerGas)
|
||||
|
||||
proc new*(_: type JsonRpcProvider, url=defaultUrl): JsonRpcProvider =
|
||||
let provider = JsonRpcProvider()
|
||||
provider.connect(url)
|
||||
provider
|
||||
proc callImpl(
|
||||
client: RpcClient, call: string, args: JsonNode
|
||||
): Future[JsonNode] {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
|
||||
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.} =
|
||||
proc send*(
|
||||
provider: JsonRpcProvider, call: string, arguments: seq[JsonNode] = @[]
|
||||
): Future[JsonNode] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.call(call, %arguments)
|
||||
return await client.callImpl(call, %arguments)
|
||||
|
||||
proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] {.async.} =
|
||||
proc listAccounts*(
|
||||
provider: JsonRpcProvider
|
||||
): Future[seq[Address]] {.async: (raises: [JsonRpcProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_accounts()
|
||||
@ -99,59 +126,119 @@ proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner =
|
||||
proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
|
||||
JsonRpcSigner(provider: provider, address: some address)
|
||||
|
||||
method getBlockNumber*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
|
||||
method getBlockNumber*(
|
||||
provider: JsonRpcProvider
|
||||
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_blockNumber()
|
||||
|
||||
method getBlock*(provider: JsonRpcProvider,
|
||||
tag: BlockTag): Future[?Block] {.async.} =
|
||||
method getBlock*(
|
||||
provider: JsonRpcProvider, tag: BlockTag
|
||||
): Future[?Block] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_getBlockByNumber(tag, false)
|
||||
|
||||
method call*(provider: JsonRpcProvider,
|
||||
tx: Transaction,
|
||||
blockTag = BlockTag.latest): Future[seq[byte]] {.async.} =
|
||||
method call*(
|
||||
provider: JsonRpcProvider, tx: Transaction, blockTag = BlockTag.latest
|
||||
): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_call(tx, blockTag)
|
||||
|
||||
method getGasPrice*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
|
||||
method getGasPrice*(
|
||||
provider: JsonRpcProvider
|
||||
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_gasprice()
|
||||
return await client.eth_gasPrice()
|
||||
|
||||
method getTransactionCount*(provider: JsonRpcProvider,
|
||||
address: Address,
|
||||
blockTag = BlockTag.latest):
|
||||
Future[UInt256] {.async.} =
|
||||
method getMaxPriorityFeePerGas*(
|
||||
provider: JsonRpcProvider
|
||||
): Future[UInt256] {.async: (raises: [CancelledError]).} =
|
||||
try:
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_maxPriorityFeePerGas()
|
||||
except JsonRpcProviderError:
|
||||
# If the provider does not provide the implementation
|
||||
# let's just remove the manual value
|
||||
return provider.maxPriorityFeePerGas
|
||||
|
||||
method getTransactionCount*(
|
||||
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest
|
||||
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_getTransactionCount(address, blockTag)
|
||||
|
||||
method getTransactionReceipt*(provider: JsonRpcProvider,
|
||||
txHash: TransactionHash):
|
||||
Future[?TransactionReceipt] {.async.} =
|
||||
method getTransaction*(
|
||||
provider: JsonRpcProvider, txHash: TransactionHash
|
||||
): Future[?PastTransaction] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
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:
|
||||
let client = await provider.client
|
||||
return await client.eth_getTransactionReceipt(txHash)
|
||||
|
||||
method estimateGas*(provider: JsonRpcProvider,
|
||||
transaction: Transaction): Future[UInt256] {.async.} =
|
||||
method getLogs*(
|
||||
provider: JsonRpcProvider, filter: EventFilter
|
||||
): Future[seq[Log]] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_estimateGas(transaction)
|
||||
let logsJson =
|
||||
if filter of Filter:
|
||||
await client.eth_getLogs(Filter(filter))
|
||||
elif filter of FilterByBlockHash:
|
||||
await client.eth_getLogs(FilterByBlockHash(filter))
|
||||
else:
|
||||
await client.eth_getLogs(filter)
|
||||
|
||||
method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
|
||||
var logs: seq[Log] = @[]
|
||||
for logJson in logsJson.getElems:
|
||||
if log =? Log.fromJson(logJson):
|
||||
logs.add log
|
||||
|
||||
return logs
|
||||
|
||||
method estimateGas*(
|
||||
provider: JsonRpcProvider,
|
||||
transaction: Transaction,
|
||||
blockTag = BlockTag.latest,
|
||||
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
try:
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
return await client.eth_estimateGas(transaction, blockTag)
|
||||
except ProviderError as error:
|
||||
raise (ref EstimateGasError)(
|
||||
msg: "Estimate gas failed: " & error.msg,
|
||||
data: error.data,
|
||||
transaction: transaction,
|
||||
parent: error,
|
||||
)
|
||||
|
||||
method getChainId*(
|
||||
provider: JsonRpcProvider
|
||||
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let client = await provider.client
|
||||
try:
|
||||
return await client.eth_chainId()
|
||||
except CancelledError as error:
|
||||
raise error
|
||||
except CatchableError:
|
||||
return parse(await client.net_version(), UInt256)
|
||||
|
||||
method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): Future[TransactionResponse] {.async.} =
|
||||
method sendTransaction*(
|
||||
provider: JsonRpcProvider, rawTransaction: seq[byte]
|
||||
): Future[TransactionResponse] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let
|
||||
client = await provider.client
|
||||
@ -159,54 +246,76 @@ method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): F
|
||||
|
||||
return TransactionResponse(hash: hash, provider: provider)
|
||||
|
||||
proc subscribe(provider: JsonRpcProvider,
|
||||
name: string,
|
||||
filter: ?Filter,
|
||||
handler: SubscriptionHandler): Future[Subscription] {.async.} =
|
||||
method subscribe*(
|
||||
provider: JsonRpcProvider, filter: EventFilter, onLog: LogHandler
|
||||
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let subscriptions = await provider.subscriptions
|
||||
let id = await subscriptions.subscribeLogs(filter, onLog)
|
||||
return JsonRpcSubscription(subscriptions: subscriptions, id: id)
|
||||
|
||||
method subscribe*(
|
||||
provider: JsonRpcProvider, onBlock: BlockHandler
|
||||
): Future[Subscription] {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let subscriptions = await provider.subscriptions
|
||||
let id = await subscriptions.subscribeBlocks(onBlock)
|
||||
return JsonRpcSubscription(subscriptions: subscriptions, id: id)
|
||||
|
||||
method unsubscribe*(
|
||||
subscription: JsonRpcSubscription
|
||||
) {.async: (raises: [ProviderError, CancelledError]).} =
|
||||
convertError:
|
||||
let subscriptions = subscription.subscriptions
|
||||
let id = subscription.id
|
||||
await subscriptions.unsubscribe(id)
|
||||
|
||||
method isSyncing*(
|
||||
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:
|
||||
let client = await provider.client
|
||||
doAssert client of RpcWebSocketClient, "subscriptions require websockets"
|
||||
|
||||
var id: JsonNode
|
||||
if filter =? filter:
|
||||
id = await client.eth_subscribe(name, filter)
|
||||
else:
|
||||
id = await client.eth_subscribe(name)
|
||||
|
||||
provider.subscriptions[id] = handler
|
||||
|
||||
return JsonRpcSubscription(id: id, provider: provider)
|
||||
|
||||
method subscribe*(provider: JsonRpcProvider,
|
||||
filter: Filter,
|
||||
callback: LogHandler):
|
||||
Future[Subscription] {.async.} =
|
||||
proc handler(id, arguments: JsonNode) {.async.} =
|
||||
if log =? Log.fromJson(arguments["result"]).catch:
|
||||
callback(log)
|
||||
return await provider.subscribe("logs", filter.some, handler)
|
||||
|
||||
method subscribe*(provider: JsonRpcProvider,
|
||||
callback: BlockHandler):
|
||||
Future[Subscription] {.async.} =
|
||||
proc handler(id, arguments: JsonNode) {.async.} =
|
||||
if blck =? Block.fromJson(arguments["result"]).catch:
|
||||
await callback(blck)
|
||||
return await provider.subscribe("newHeads", Filter.none, handler)
|
||||
|
||||
method unsubscribe*(subscription: JsonRpcSubscription) {.async.} =
|
||||
convertError:
|
||||
let provider = subscription.provider
|
||||
provider.subscriptions.del(subscription.id)
|
||||
let client = await provider.client
|
||||
discard await client.eth_unsubscribe(subscription.id)
|
||||
let subscriptions = await provider.subscriptions
|
||||
await subscriptions.close()
|
||||
await client.close()
|
||||
|
||||
# Signer
|
||||
|
||||
method provider*(signer: JsonRpcSigner): Provider =
|
||||
proc raiseJsonRpcSignerError(
|
||||
message: string) {.raises: [JsonRpcSignerError].} =
|
||||
|
||||
var message = message
|
||||
if json =? JsonNode.fromJson(message):
|
||||
if "message" in json:
|
||||
message = json{"message"}.getStr
|
||||
raise newException(JsonRpcSignerError, message)
|
||||
|
||||
template convertSignerError(body) =
|
||||
try:
|
||||
body
|
||||
except CancelledError as error:
|
||||
raise error
|
||||
except JsonRpcError as error:
|
||||
raiseJsonRpcSignerError(error.msg)
|
||||
except CatchableError as error:
|
||||
raise newException(JsonRpcSignerError, error.msg)
|
||||
|
||||
method provider*(signer: JsonRpcSigner): Provider
|
||||
{.gcsafe, raises: [SignerError].} =
|
||||
|
||||
signer.provider
|
||||
|
||||
method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
|
||||
method getAddress*(
|
||||
signer: JsonRpcSigner
|
||||
): Future[Address] {.async: (raises: [ProviderError, SignerError, CancelledError]).} =
|
||||
if address =? signer.address:
|
||||
return address
|
||||
|
||||
@ -214,17 +323,21 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
|
||||
if accounts.len > 0:
|
||||
return accounts[0]
|
||||
|
||||
raiseProviderError "no address found"
|
||||
raiseJsonRpcSignerError "no address found"
|
||||
|
||||
method signMessage*(signer: JsonRpcSigner,
|
||||
message: seq[byte]): Future[seq[byte]] {.async.} =
|
||||
convertError:
|
||||
method signMessage*(
|
||||
signer: JsonRpcSigner, message: seq[byte]
|
||||
): Future[seq[byte]] {.async: (raises: [SignerError, CancelledError]).} =
|
||||
convertSignerError:
|
||||
let client = await signer.provider.client
|
||||
let address = await signer.getAddress()
|
||||
return await client.eth_sign(address, message)
|
||||
return await client.personal_sign(message, address)
|
||||
|
||||
method sendTransaction*(signer: JsonRpcSigner,
|
||||
transaction: Transaction): Future[TransactionResponse] {.async.} =
|
||||
method sendTransaction*(
|
||||
signer: JsonRpcSigner, transaction: Transaction
|
||||
): Future[TransactionResponse] {.
|
||||
async: (raises: [SignerError, ProviderError, CancelledError])
|
||||
.} =
|
||||
convertError:
|
||||
let
|
||||
client = await signer.provider.client
|
||||
|
||||
@ -1,56 +1,62 @@
|
||||
import std/json
|
||||
import std/strformat
|
||||
import std/strutils
|
||||
import pkg/json_rpc/jsonmarshal
|
||||
import pkg/chronicles except fromJson, `%`, `%*`, toJson
|
||||
import pkg/json_rpc/jsonmarshal except toJson
|
||||
import pkg/questionable/results
|
||||
import pkg/serde
|
||||
import pkg/stew/byteutils
|
||||
import ../../basics
|
||||
import ../../transaction
|
||||
import ../../blocktag
|
||||
import ../../provider
|
||||
|
||||
export jsonmarshal
|
||||
export jsonmarshal except toJson
|
||||
export serde
|
||||
export chronicles except fromJson, `%`, `%*`, toJson
|
||||
|
||||
func fromJson*(T: type, json: JsonNode, name = ""): T =
|
||||
fromJson(json, name, result)
|
||||
{.push raises: [].}
|
||||
|
||||
# byte sequence
|
||||
proc getOrRaise*[T, E](self: ?!T, exc: typedesc[E]): T {.raises: [E].} =
|
||||
let val = self.valueOr:
|
||||
raise newException(E, self.error.msg)
|
||||
val
|
||||
|
||||
func `%`*(bytes: seq[byte]): JsonNode =
|
||||
%("0x" & bytes.toHex)
|
||||
template mapFailure*[T, V, E](
|
||||
exp: Result[T, V],
|
||||
exc: typedesc[E],
|
||||
): Result[T, ref CatchableError] =
|
||||
## Convert `Result[T, E]` to `Result[E, ref CatchableError]`
|
||||
##
|
||||
|
||||
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)
|
||||
exp.mapErr(proc (e: V): ref CatchableError = (ref exc)(msg: e.msg))
|
||||
|
||||
# Address
|
||||
|
||||
func `%`*(address: Address): JsonNode =
|
||||
%($address)
|
||||
|
||||
func fromJson*(json: JsonNode, name: string, result: var Address) =
|
||||
if address =? Address.init(json.getStr()):
|
||||
result = address
|
||||
else:
|
||||
raise newException(ValueError, "\"" & name & "\"is not an Address")
|
||||
func fromJson(_: type Address, json: JsonNode): ?!Address =
|
||||
expectJsonKind(Address, JString, json)
|
||||
without address =? Address.init(json.getStr), error:
|
||||
return failure newException(SerializationError,
|
||||
"Failed to convert '" & $json & "' to Address: " & error.msg)
|
||||
success address
|
||||
|
||||
# UInt256
|
||||
|
||||
func `%`*(integer: UInt256): JsonNode =
|
||||
%("0x" & toHex(integer))
|
||||
|
||||
func fromJson*(json: JsonNode, name: string, result: var UInt256) =
|
||||
result = UInt256.fromHex(json.getStr())
|
||||
|
||||
# Transaction
|
||||
|
||||
# TODO: add option that ignores none Option[T]
|
||||
# TODO: add name option (gasLimit => gas, sender => from)
|
||||
func `%`*(transaction: Transaction): JsonNode =
|
||||
result = %{ "to": %transaction.to, "data": %transaction.data }
|
||||
result = %*{
|
||||
"to": transaction.to,
|
||||
"data": %transaction.data,
|
||||
"value": %transaction.value
|
||||
}
|
||||
if sender =? transaction.sender:
|
||||
result["from"] = %sender
|
||||
if nonce =? transaction.nonce:
|
||||
@ -64,23 +70,53 @@ func `%`*(transaction: Transaction): JsonNode =
|
||||
|
||||
# BlockTag
|
||||
|
||||
func `%`*(blockTag: BlockTag): JsonNode =
|
||||
%($blockTag)
|
||||
func `%`*(tag: BlockTag): JsonNode =
|
||||
% $tag
|
||||
|
||||
# Log
|
||||
func fromJson*(_: type BlockTag, json: JsonNode): ?!BlockTag =
|
||||
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)
|
||||
|
||||
func fromJson*(json: JsonNode, name: string, result: var Log) =
|
||||
var data: seq[byte]
|
||||
var topics: seq[Topic]
|
||||
fromJson(json["data"], "data", data)
|
||||
fromJson(json["topics"], "topics", topics)
|
||||
result = Log(data: data, topics: topics)
|
||||
case jsonVal:
|
||||
of "earliest": return success BlockTag.earliest
|
||||
of "latest": return success BlockTag.latest
|
||||
of "pending": return success BlockTag.pending
|
||||
else: return failure newException(SerializationError,
|
||||
"Failed to convert '" & $json &
|
||||
"' to BlockTag: must be one of 'earliest', 'latest', 'pending'")
|
||||
|
||||
# TransactionStatus
|
||||
# TransactionStatus | TransactionType
|
||||
type TransactionEnums = TransactionStatus | TransactionType
|
||||
|
||||
func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) =
|
||||
let val = fromHex[int](json.getStr)
|
||||
result = TransactionStatus(val)
|
||||
func `%`*(e: TransactionEnums): JsonNode =
|
||||
% ("0x" & e.int8.toHex(1))
|
||||
|
||||
func `%`*(status: TransactionStatus): JsonNode =
|
||||
%(status.int.toHex)
|
||||
proc fromJson*(
|
||||
T: type TransactionEnums,
|
||||
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)
|
||||
|
||||
49
ethers/providers/jsonrpc/errors.nim
Normal file
49
ethers/providers/jsonrpc/errors.nim
Normal file
@ -0,0 +1,49 @@
|
||||
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)
|
||||
|
||||
6
ethers/providers/jsonrpc/looping.nim
Normal file
6
ethers/providers/jsonrpc/looping.nim
Normal file
@ -0,0 +1,6 @@
|
||||
template untilCancelled*(body) =
|
||||
try:
|
||||
while true:
|
||||
body
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
@ -1,16 +1,24 @@
|
||||
proc net_version(): string
|
||||
proc personal_sign(message: seq[byte], account: Address): seq[byte]
|
||||
proc eth_accounts: seq[Address]
|
||||
proc eth_blockNumber: UInt256
|
||||
proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte]
|
||||
proc eth_gasPrice(): UInt256
|
||||
proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
|
||||
proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode
|
||||
proc eth_getTransactionByHash(hash: TransactionHash): ?PastTransaction
|
||||
proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block
|
||||
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256
|
||||
proc eth_estimateGas(transaction: Transaction): UInt256
|
||||
proc eth_estimateGas(transaction: Transaction, blockTag: BlockTag): UInt256
|
||||
proc eth_chainId(): UInt256
|
||||
proc eth_sendTransaction(transaction: Transaction): TransactionHash
|
||||
proc eth_sendRawTransaction(data: seq[byte]): TransactionHash
|
||||
proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt
|
||||
proc eth_sign(account: Address, message: seq[byte]): seq[byte]
|
||||
proc eth_subscribe(name: string, filter: Filter): JsonNode
|
||||
proc eth_subscribe(name: string, filter: EventFilter): JsonNode
|
||||
proc eth_subscribe(name: string): JsonNode
|
||||
proc eth_unsubscribe(id: JsonNode): bool
|
||||
proc eth_newBlockFilter(): JsonNode
|
||||
proc eth_newFilter(filter: EventFilter): JsonNode
|
||||
proc eth_getFilterChanges(id: JsonNode): JsonNode
|
||||
proc eth_uninstallFilter(id: JsonNode): bool
|
||||
proc eth_maxPriorityFeePerGas(): UInt256
|
||||
|
||||
379
ethers/providers/jsonrpc/subscriptions.nim
Normal file
379
ethers/providers/jsonrpc/subscriptions.nim
Normal file
@ -0,0 +1,379 @@
|
||||
import std/tables
|
||||
import std/sequtils
|
||||
import std/strutils
|
||||
import pkg/chronos
|
||||
import pkg/questionable
|
||||
import pkg/json_rpc/rpcclient
|
||||
import pkg/serde
|
||||
import ../../basics
|
||||
import ../../errors
|
||||
import ../../provider
|
||||
include ../../nimshims/hashes
|
||||
import ./rpccalls
|
||||
import ./conversions
|
||||
|
||||
export serde
|
||||
|
||||
type
|
||||
JsonRpcSubscriptions* = ref object of RootObj
|
||||
client: RpcClient
|
||||
callbacks: Table[JsonNode, SubscriptionCallback]
|
||||
methodHandlers: Table[string, MethodHandler]
|
||||
# Used by both PollingSubscriptions and WebsocketSubscriptions to store
|
||||
# subscription filters so the subscriptions can be recreated. With
|
||||
# PollingSubscriptions, the RPC node might prune/forget about them, and with
|
||||
# WebsocketSubscriptions, when using hardhat, subscriptions are dropped after 5
|
||||
# minutes.
|
||||
logFilters: Table[JsonNode, EventFilter]
|
||||
MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [].}
|
||||
SubscriptionCallback = proc(id: JsonNode, arguments: ?!JsonNode) {.gcsafe, raises:[].}
|
||||
|
||||
{.push raises:[].}
|
||||
|
||||
template convertErrorsToSubscriptionError(body) =
|
||||
try:
|
||||
body
|
||||
except CancelledError as error:
|
||||
raise error
|
||||
except CatchableError as error:
|
||||
raise error.toErr(SubscriptionError)
|
||||
|
||||
template `or`(a: JsonNode, b: typed): JsonNode =
|
||||
if a.isNil: b else: a
|
||||
|
||||
func start*(subscriptions: JsonRpcSubscriptions) =
|
||||
subscriptions.client.onProcessMessage =
|
||||
proc(client: RpcClient,
|
||||
line: string): Result[bool, string] {.gcsafe, raises: [].} =
|
||||
if json =? JsonNode.fromJson(line):
|
||||
if "method" in json:
|
||||
let methodName = json{"method"}.getStr()
|
||||
if methodName in subscriptions.methodHandlers:
|
||||
let handler = subscriptions.methodHandlers.getOrDefault(methodName)
|
||||
if not handler.isNil:
|
||||
handler(json{"params"} or newJArray())
|
||||
# false = do not continue processing message using json_rpc's
|
||||
# default processing handler
|
||||
return ok false
|
||||
|
||||
# true = continue processing message using json_rpc's default message handler
|
||||
return ok true
|
||||
|
||||
proc setMethodHandler(
|
||||
subscriptions: JsonRpcSubscriptions,
|
||||
`method`: string,
|
||||
handler: MethodHandler
|
||||
) =
|
||||
subscriptions.methodHandlers[`method`] = handler
|
||||
|
||||
method subscribeBlocks*(subscriptions: JsonRpcSubscriptions,
|
||||
onBlock: BlockHandler):
|
||||
Future[JsonNode]
|
||||
{.async: (raises: [SubscriptionError, CancelledError]), base,.} =
|
||||
raiseAssert "not implemented"
|
||||
|
||||
method subscribeLogs*(subscriptions: JsonRpcSubscriptions,
|
||||
filter: EventFilter,
|
||||
onLog: LogHandler):
|
||||
Future[JsonNode]
|
||||
{.async: (raises: [SubscriptionError, CancelledError]), base.} =
|
||||
raiseAssert "not implemented"
|
||||
|
||||
method unsubscribe*(subscriptions: JsonRpcSubscriptions,
|
||||
id: JsonNode)
|
||||
{.async: (raises: [CancelledError]), base.} =
|
||||
raiseAssert "not implemented "
|
||||
|
||||
method close*(subscriptions: JsonRpcSubscriptions) {.async: (raises: []), base.} =
|
||||
let ids = toSeq subscriptions.callbacks.keys
|
||||
for id in ids:
|
||||
try:
|
||||
await subscriptions.unsubscribe(id)
|
||||
except CatchableError as e:
|
||||
error "JsonRpc unsubscription failed", error = e.msg, id = id
|
||||
|
||||
proc getCallback(subscriptions: JsonRpcSubscriptions,
|
||||
id: JsonNode): ?SubscriptionCallback {. raises:[].} =
|
||||
try:
|
||||
if not id.isNil and id in subscriptions.callbacks:
|
||||
return subscriptions.callbacks[id].some
|
||||
except: discard
|
||||
|
||||
# Web sockets
|
||||
|
||||
# Default re-subscription period is seconds
|
||||
const WsResubscribe {.intdefine.}: int = 0
|
||||
|
||||
type
|
||||
WebSocketSubscriptions = ref object of JsonRpcSubscriptions
|
||||
logFiltersLock: AsyncLock
|
||||
resubscribeFut: Future[void]
|
||||
resubscribeInterval: int
|
||||
|
||||
template withLock*(subscriptions: WebSocketSubscriptions, body: untyped) =
|
||||
if subscriptions.logFiltersLock.isNil:
|
||||
subscriptions.logFiltersLock = newAsyncLock()
|
||||
|
||||
await subscriptions.logFiltersLock.acquire()
|
||||
try:
|
||||
body
|
||||
finally:
|
||||
subscriptions.logFiltersLock.release()
|
||||
|
||||
# This is a workaround to manage the 5 minutes limit due to hardhat.
|
||||
# See https://github.com/NomicFoundation/hardhat/issues/2053#issuecomment-1061374064
|
||||
proc resubscribeWebsocketEventsOnTimeout*(subscriptions: WebsocketSubscriptions) {.async: (raises: [CancelledError]).} =
|
||||
while true:
|
||||
await sleepAsync(subscriptions.resubscribeInterval.seconds)
|
||||
try:
|
||||
withLock(subscriptions):
|
||||
for id, callback in subscriptions.callbacks:
|
||||
|
||||
var newId: JsonNode
|
||||
if id in subscriptions.logFilters:
|
||||
let filter = subscriptions.logFilters[id]
|
||||
newId = await subscriptions.client.eth_subscribe("logs", filter)
|
||||
subscriptions.logFilters[newId] = filter
|
||||
subscriptions.logFilters.del(id)
|
||||
else:
|
||||
newId = await subscriptions.client.eth_subscribe("newHeads")
|
||||
|
||||
subscriptions.callbacks[newId] = callback
|
||||
subscriptions.callbacks.del(id)
|
||||
discard await subscriptions.client.eth_unsubscribe(id)
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
except CatchableError as e:
|
||||
error "WS resubscription failed" , error = e.msg
|
||||
|
||||
proc new*(_: type JsonRpcSubscriptions,
|
||||
client: RpcWebSocketClient,
|
||||
resubscribeInterval = WsResubscribe): JsonRpcSubscriptions =
|
||||
let subscriptions = WebSocketSubscriptions(client: client, resubscribeInterval: resubscribeInterval)
|
||||
|
||||
proc subscriptionHandler(arguments: JsonNode) {.raises:[].} =
|
||||
let id = arguments{"subscription"} or newJString("")
|
||||
if callback =? subscriptions.getCallback(id):
|
||||
callback(id, success(arguments))
|
||||
subscriptions.setMethodHandler("eth_subscription", subscriptionHandler)
|
||||
|
||||
if resubscribeInterval > 0:
|
||||
if resubscribeInterval >= 300:
|
||||
warn "Resubscription interval greater than 300 seconds is useless for hardhat workaround", resubscribeInterval = resubscribeInterval
|
||||
|
||||
subscriptions.resubscribeFut = resubscribeWebsocketEventsOnTimeout(subscriptions)
|
||||
|
||||
subscriptions
|
||||
|
||||
method subscribeBlocks(subscriptions: WebSocketSubscriptions,
|
||||
onBlock: BlockHandler):
|
||||
Future[JsonNode]
|
||||
{.async: (raises: [SubscriptionError, CancelledError]).} =
|
||||
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) {.raises: [].} =
|
||||
without arguments =? argumentsResult, error:
|
||||
onBlock(failure(Block, error.toErr(SubscriptionError)))
|
||||
return
|
||||
|
||||
let res = Block.fromJson(arguments{"result"}).mapFailure(SubscriptionError)
|
||||
onBlock(res)
|
||||
|
||||
convertErrorsToSubscriptionError:
|
||||
withLock(subscriptions):
|
||||
let id = await subscriptions.client.eth_subscribe("newHeads")
|
||||
subscriptions.callbacks[id] = callback
|
||||
return id
|
||||
|
||||
method subscribeLogs(subscriptions: WebSocketSubscriptions,
|
||||
filter: EventFilter,
|
||||
onLog: LogHandler):
|
||||
Future[JsonNode]
|
||||
{.async: (raises: [SubscriptionError, CancelledError]).} =
|
||||
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) =
|
||||
without arguments =? argumentsResult, error:
|
||||
onLog(failure(Log, error.toErr(SubscriptionError)))
|
||||
return
|
||||
|
||||
let res = Log.fromJson(arguments{"result"}).mapFailure(SubscriptionError)
|
||||
onLog(res)
|
||||
|
||||
convertErrorsToSubscriptionError:
|
||||
withLock(subscriptions):
|
||||
let id = await subscriptions.client.eth_subscribe("logs", filter)
|
||||
subscriptions.callbacks[id] = callback
|
||||
subscriptions.logFilters[id] = filter
|
||||
return id
|
||||
|
||||
method unsubscribe*(subscriptions: WebSocketSubscriptions,
|
||||
id: JsonNode)
|
||||
{.async: (raises: [CancelledError]).} =
|
||||
try:
|
||||
withLock(subscriptions):
|
||||
subscriptions.callbacks.del(id)
|
||||
discard await subscriptions.client.eth_unsubscribe(id)
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
except CatchableError:
|
||||
# Ignore if uninstallation of the subscribiton fails.
|
||||
discard
|
||||
|
||||
method close*(subscriptions: WebSocketSubscriptions) {.async: (raises: []).} =
|
||||
await procCall JsonRpcSubscriptions(subscriptions).close()
|
||||
if not subscriptions.resubscribeFut.isNil:
|
||||
await subscriptions.resubscribeFut.cancelAndWait()
|
||||
|
||||
# Polling
|
||||
|
||||
type
|
||||
PollingSubscriptions* = ref object of JsonRpcSubscriptions
|
||||
polling: Future[void]
|
||||
|
||||
# Used when filters are recreated to translate from the id that user
|
||||
# originally got returned to new filter id
|
||||
subscriptionMapping: Table[JsonNode, JsonNode]
|
||||
|
||||
proc new*(_: type JsonRpcSubscriptions,
|
||||
client: RpcHttpClient,
|
||||
pollingInterval = 4.seconds): JsonRpcSubscriptions =
|
||||
|
||||
let subscriptions = PollingSubscriptions(client: client)
|
||||
|
||||
proc resubscribe(id: JsonNode): Future[?!void] {.async: (raises: [CancelledError]).} =
|
||||
try:
|
||||
var newId: JsonNode
|
||||
# Log filters are stored in logFilters, block filters are not persisted
|
||||
# there is they do not need any specific data for their recreation.
|
||||
# We use this to determine if the filter was log or block filter here.
|
||||
if subscriptions.logFilters.hasKey(id):
|
||||
let filter = subscriptions.logFilters[id]
|
||||
newId = await subscriptions.client.eth_newFilter(filter)
|
||||
else:
|
||||
newId = await subscriptions.client.eth_newBlockFilter()
|
||||
subscriptions.subscriptionMapping[id] = newId
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
except CatchableError as e:
|
||||
return failure(void, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
|
||||
|
||||
return success()
|
||||
|
||||
proc getChanges(id: JsonNode): Future[?!JsonNode] {.async: (raises: [CancelledError]).} =
|
||||
if mappedId =? subscriptions.subscriptionMapping.?[id]:
|
||||
try:
|
||||
let changes = await subscriptions.client.eth_getFilterChanges(mappedId)
|
||||
if changes.kind == JArray:
|
||||
return success(changes)
|
||||
except JsonRpcError as e:
|
||||
if error =? (await resubscribe(id)).errorOption:
|
||||
return failure(JsonNode, error)
|
||||
|
||||
# TODO: we could still miss some events between losing the subscription
|
||||
# and resubscribing. We should probably adopt a strategy like ethers.js,
|
||||
# whereby we keep track of the latest block number that we've seen
|
||||
# filter changes for:
|
||||
# https://github.com/ethers-io/ethers.js/blob/f97b92bbb1bde22fcc44100af78d7f31602863ab/packages/providers/src.ts/base-provider.ts#L977
|
||||
|
||||
if not ("filter not found" in e.msg):
|
||||
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
except SubscriptionError as e:
|
||||
return failure(JsonNode, e)
|
||||
except CatchableError as e:
|
||||
return failure(JsonNode, e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription changes: " & e.msg))
|
||||
return success(newJArray())
|
||||
|
||||
proc poll(id: JsonNode) {.async: (raises: [CancelledError]).} =
|
||||
without callback =? subscriptions.getCallback(id):
|
||||
return
|
||||
|
||||
without changes =? (await getChanges(id)), error:
|
||||
callback(id, failure(JsonNode, error))
|
||||
return
|
||||
|
||||
for change in changes:
|
||||
callback(id, success(change))
|
||||
|
||||
proc poll {.async: (raises: []).} =
|
||||
try:
|
||||
while true:
|
||||
for id in toSeq subscriptions.callbacks.keys:
|
||||
await poll(id)
|
||||
await sleepAsync(pollingInterval)
|
||||
except CancelledError:
|
||||
discard
|
||||
|
||||
subscriptions.polling = poll()
|
||||
asyncSpawn subscriptions.polling
|
||||
subscriptions
|
||||
|
||||
method close*(subscriptions: PollingSubscriptions) {.async: (raises: []).} =
|
||||
await subscriptions.polling.cancelAndWait()
|
||||
await procCall JsonRpcSubscriptions(subscriptions).close()
|
||||
|
||||
method subscribeBlocks(subscriptions: PollingSubscriptions,
|
||||
onBlock: BlockHandler):
|
||||
Future[JsonNode]
|
||||
{.async: (raises: [SubscriptionError, CancelledError]).} =
|
||||
|
||||
proc getBlock(hash: BlockHash) {.async: (raises:[]).} =
|
||||
try:
|
||||
if blck =? (await subscriptions.client.eth_getBlockByHash(hash, false)):
|
||||
onBlock(success(blck))
|
||||
except CancelledError:
|
||||
discard
|
||||
except CatchableError as e:
|
||||
let error = e.toErr(SubscriptionError, "HTTP polling: There was an exception while getting subscription's block: " & e.msg)
|
||||
onBlock(failure(Block, error))
|
||||
|
||||
proc callback(id: JsonNode, changeResult: ?!JsonNode) {.raises:[].} =
|
||||
without change =? changeResult, e:
|
||||
onBlock(failure(Block, e.toErr(SubscriptionError)))
|
||||
return
|
||||
|
||||
if hash =? BlockHash.fromJson(change):
|
||||
asyncSpawn getBlock(hash)
|
||||
|
||||
convertErrorsToSubscriptionError:
|
||||
let id = await subscriptions.client.eth_newBlockFilter()
|
||||
subscriptions.callbacks[id] = callback
|
||||
subscriptions.subscriptionMapping[id] = id
|
||||
return id
|
||||
|
||||
method subscribeLogs(subscriptions: PollingSubscriptions,
|
||||
filter: EventFilter,
|
||||
onLog: LogHandler):
|
||||
Future[JsonNode]
|
||||
{.async: (raises: [SubscriptionError, CancelledError]).} =
|
||||
|
||||
proc callback(id: JsonNode, argumentsResult: ?!JsonNode) =
|
||||
without arguments =? argumentsResult, error:
|
||||
onLog(failure(Log, error.toErr(SubscriptionError)))
|
||||
return
|
||||
|
||||
let res = Log.fromJson(arguments).mapFailure(SubscriptionError)
|
||||
onLog(res)
|
||||
|
||||
convertErrorsToSubscriptionError:
|
||||
let id = await subscriptions.client.eth_newFilter(filter)
|
||||
subscriptions.callbacks[id] = callback
|
||||
subscriptions.logFilters[id] = filter
|
||||
subscriptions.subscriptionMapping[id] = id
|
||||
return id
|
||||
|
||||
method unsubscribe*(subscriptions: PollingSubscriptions,
|
||||
id: JsonNode)
|
||||
{.async: (raises: [CancelledError]).} =
|
||||
try:
|
||||
subscriptions.logFilters.del(id)
|
||||
subscriptions.callbacks.del(id)
|
||||
if sub =? subscriptions.subscriptionMapping.?[id]:
|
||||
subscriptions.subscriptionMapping.del(id)
|
||||
discard await subscriptions.client.eth_uninstallFilter(sub)
|
||||
except CancelledError as e:
|
||||
raise e
|
||||
except CatchableError:
|
||||
# Ignore if uninstallation of the filter fails. If it's the last step in our
|
||||
# cleanup, then filter changes for this filter will no longer be polled so
|
||||
# if the filter continues to live on in geth for whatever reason then it
|
||||
# doesn't matter.
|
||||
discard
|
||||
@ -1,51 +1,125 @@
|
||||
import pkg/questionable
|
||||
import pkg/chronicles
|
||||
import ./basics
|
||||
import ./errors
|
||||
import ./provider
|
||||
|
||||
export basics
|
||||
export errors
|
||||
|
||||
type Signer* = ref object of RootObj
|
||||
type SignerError* = object of EthersError
|
||||
{.push raises: [].}
|
||||
|
||||
template raiseSignerError(message: string) =
|
||||
raise newException(SignerError, message)
|
||||
type
|
||||
Signer* = ref object of RootObj
|
||||
populateLock: AsyncLock
|
||||
|
||||
method provider*(signer: Signer): Provider {.base.} =
|
||||
template raiseSignerError*(message: string, parent: ref CatchableError = nil) =
|
||||
raise newException(SignerError, message, parent)
|
||||
|
||||
template convertError(body) =
|
||||
try:
|
||||
body
|
||||
except CancelledError as error:
|
||||
raise error
|
||||
except ProviderError as error:
|
||||
raise error # do not convert provider errors
|
||||
except CatchableError as error:
|
||||
raiseSignerError(error.msg)
|
||||
|
||||
method provider*(
|
||||
signer: Signer): Provider {.base, gcsafe, raises: [SignerError].} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method getAddress*(signer: Signer): Future[Address] {.base.} =
|
||||
method getAddress*(
|
||||
signer: Signer
|
||||
): Future[Address] {.
|
||||
base, async: (raises: [ProviderError, SignerError, CancelledError])
|
||||
.} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method signMessage*(signer: Signer,
|
||||
message: seq[byte]): Future[seq[byte]] {.base, async.} =
|
||||
method signMessage*(
|
||||
signer: Signer, message: seq[byte]
|
||||
): Future[seq[byte]] {.base, async: (raises: [SignerError, CancelledError]).} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method sendTransaction*(signer: Signer,
|
||||
transaction: Transaction): Future[TransactionResponse] {.base, async.} =
|
||||
method sendTransaction*(
|
||||
signer: Signer, transaction: Transaction
|
||||
): Future[TransactionResponse] {.
|
||||
base, async: (raises: [SignerError, ProviderError, CancelledError])
|
||||
.} =
|
||||
doAssert false, "not implemented"
|
||||
|
||||
method getGasPrice*(signer: Signer): Future[UInt256] {.base.} =
|
||||
signer.provider.getGasPrice()
|
||||
method getGasPrice*(
|
||||
signer: Signer
|
||||
): Future[UInt256] {.
|
||||
base, async: (raises: [ProviderError, SignerError, CancelledError])
|
||||
.} =
|
||||
return await signer.provider.getGasPrice()
|
||||
|
||||
method getTransactionCount*(signer: Signer,
|
||||
blockTag = BlockTag.latest):
|
||||
Future[UInt256] {.base, async.} =
|
||||
let address = await signer.getAddress()
|
||||
return await signer.provider.getTransactionCount(address, blockTag)
|
||||
method getMaxPriorityFeePerGas*(
|
||||
signer: Signer
|
||||
): Future[UInt256] {.async: (raises: [SignerError, CancelledError]).} =
|
||||
return await signer.provider.getMaxPriorityFeePerGas()
|
||||
|
||||
method estimateGas*(signer: Signer,
|
||||
transaction: Transaction): Future[UInt256] {.base, async.} =
|
||||
method getTransactionCount*(
|
||||
signer: Signer, blockTag = BlockTag.latest
|
||||
): Future[UInt256] {.
|
||||
base, async: (raises: [SignerError, ProviderError, CancelledError])
|
||||
.} =
|
||||
convertError:
|
||||
let address = await signer.getAddress()
|
||||
return await signer.provider.getTransactionCount(address, blockTag)
|
||||
|
||||
method estimateGas*(
|
||||
signer: Signer, transaction: Transaction, blockTag = BlockTag.latest
|
||||
): Future[UInt256] {.
|
||||
base, async: (raises: [SignerError, ProviderError, CancelledError])
|
||||
.} =
|
||||
var transaction = transaction
|
||||
transaction.sender = some(await signer.getAddress)
|
||||
return await signer.provider.estimateGas(transaction)
|
||||
transaction.sender = some(await signer.getAddress())
|
||||
return await signer.provider.estimateGas(transaction, blockTag)
|
||||
|
||||
method getChainId*(signer: Signer): Future[UInt256] {.base.} =
|
||||
signer.provider.getChainId()
|
||||
method getChainId*(
|
||||
signer: Signer
|
||||
): Future[UInt256] {.
|
||||
base, async: (raises: [SignerError, ProviderError, CancelledError])
|
||||
.} =
|
||||
return await signer.provider.getChainId()
|
||||
|
||||
method populateTransaction*(signer: Signer,
|
||||
transaction: Transaction):
|
||||
Future[Transaction] {.base, async.} =
|
||||
method getNonce(
|
||||
signer: Signer
|
||||
): Future[UInt256] {.
|
||||
base, async: (raises: [SignerError, ProviderError, CancelledError])
|
||||
.} =
|
||||
return await signer.getTransactionCount(BlockTag.pending)
|
||||
|
||||
if sender =? transaction.sender and sender != await signer.getAddress():
|
||||
template withLock*(signer: Signer, body: untyped) =
|
||||
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")
|
||||
if chainId =? transaction.chainId and chainId != await signer.getChainId():
|
||||
raiseSignerError("chain id mismatch")
|
||||
@ -53,14 +127,68 @@ method populateTransaction*(signer: Signer,
|
||||
var populated = transaction
|
||||
|
||||
if transaction.sender.isNone:
|
||||
populated.sender = some(await signer.getAddress())
|
||||
if transaction.nonce.isNone:
|
||||
populated.nonce = some(await signer.getTransactionCount(BlockTag.pending))
|
||||
populated.sender = some(address)
|
||||
if transaction.chainId.isNone:
|
||||
populated.chainId = some(await signer.getChainId())
|
||||
if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone):
|
||||
populated.gasPrice = some(await signer.getGasPrice())
|
||||
if transaction.gasLimit.isNone:
|
||||
populated.gasLimit = some(await signer.estimateGas(populated))
|
||||
|
||||
let blk = await signer.provider.getBlock(BlockTag.latest)
|
||||
|
||||
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:
|
||||
populated.gasLimit = some(await signer.estimateGas(populated, BlockTag.pending))
|
||||
|
||||
doAssert populated.nonce.isSome, "nonce not 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)
|
||||
|
||||
3
ethers/signers/jsonrpc.nim
Normal file
3
ethers/signers/jsonrpc.nim
Normal file
@ -0,0 +1,3 @@
|
||||
import ../providers/jsonrpc
|
||||
|
||||
export provider, getAddress, signMessage, sendTransaction
|
||||
89
ethers/signers/wallet.nim
Normal file
89
ethers/signers/wallet.nim
Normal file
@ -0,0 +1,89 @@
|
||||
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)
|
||||
15
ethers/signers/wallet/error.nim
Normal file
15
ethers/signers/wallet/error.nim
Normal file
@ -0,0 +1,15 @@
|
||||
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)
|
||||
58
ethers/signers/wallet/signing.nim
Normal file
58
ethers/signers/wallet/signing.nim
Normal file
@ -0,0 +1,58 @@
|
||||
import pkg/eth/keys
|
||||
import pkg/eth/rlp
|
||||
import pkg/eth/common/transaction as eth
|
||||
import pkg/eth/common/transaction_utils
|
||||
import pkg/eth/common/eth_hash
|
||||
import ../../basics
|
||||
import ../../transaction as ethers
|
||||
import ../../provider
|
||||
import ./error
|
||||
from pkg/eth/common/eth_types import EthAddress
|
||||
|
||||
type
|
||||
Transaction = ethers.Transaction
|
||||
SignableTransaction = eth.Transaction
|
||||
|
||||
func toSignableTransaction(transaction: Transaction): SignableTransaction =
|
||||
var signable: SignableTransaction
|
||||
|
||||
without nonce =? transaction.nonce:
|
||||
raiseWalletError "missing nonce"
|
||||
|
||||
without chainId =? transaction.chainId:
|
||||
raiseWalletError "missing chain id"
|
||||
|
||||
without gasLimit =? transaction.gasLimit:
|
||||
raiseWalletError "missing gas limit"
|
||||
|
||||
signable.nonce = nonce.truncate(uint64)
|
||||
signable.chainId = chainId
|
||||
signable.gasLimit = GasInt(gasLimit.truncate(uint64))
|
||||
|
||||
signable.to = Opt.some(EthAddress(transaction.to))
|
||||
signable.value = transaction.value
|
||||
signable.payload = transaction.data
|
||||
|
||||
if maxFeePerGas =? transaction.maxFeePerGas and
|
||||
maxPriorityFeePerGas =? transaction.maxPriorityFeePerGas:
|
||||
signable.txType = TxEip1559
|
||||
signable.maxFeePerGas = GasInt(maxFeePerGas.truncate(uint64))
|
||||
signable.maxPriorityFeePerGas = GasInt(maxPriorityFeePerGas.truncate(uint64))
|
||||
elif gasPrice =? transaction.gasPrice:
|
||||
signable.txType = TxLegacy
|
||||
signable.gasPrice = GasInt(gasPrice.truncate(uint64))
|
||||
else:
|
||||
raiseWalletError "missing gas price"
|
||||
|
||||
signable
|
||||
|
||||
func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] =
|
||||
var transaction = transaction
|
||||
transaction.signature = transaction.sign(key, true)
|
||||
rlp.encode(transaction)
|
||||
|
||||
func sign*(key: PrivateKey, transaction: Transaction): seq[byte] =
|
||||
key.sign(transaction.toSignableTransaction())
|
||||
|
||||
func toTransactionHash*(bytes: seq[byte]): TransactionHash =
|
||||
TransactionHash(bytes.keccakHash.data)
|
||||
48
ethers/testing.nim
Normal file
48
ethers/testing.nim
Normal file
@ -0,0 +1,48 @@
|
||||
import std/strutils
|
||||
import ./provider
|
||||
import ./signer
|
||||
|
||||
proc revertReason*(emsg: string): string =
|
||||
var msg = emsg
|
||||
const revertPrefixes = @[
|
||||
# hardhat
|
||||
"Error: VM Exception while processing transaction: reverted with " &
|
||||
"reason string ",
|
||||
# ganache
|
||||
"VM Exception while processing transaction: revert "
|
||||
]
|
||||
for prefix in revertPrefixes.items:
|
||||
msg = msg.replace(prefix)
|
||||
msg = msg.replace("\'")
|
||||
return msg
|
||||
|
||||
proc revertReason*(e: ref EthersError): string =
|
||||
var msg = e.msg
|
||||
msg.revertReason
|
||||
|
||||
proc reverts*[T](call: Future[T]): Future[bool] {.async.} =
|
||||
try:
|
||||
when T is void:
|
||||
await call
|
||||
else:
|
||||
discard await call
|
||||
return false
|
||||
except ProviderError, SignerError, EstimateGasError:
|
||||
return true
|
||||
|
||||
proc reverts*[T](call: Future[T], reason: string): Future[bool] {.async.} =
|
||||
try:
|
||||
when T is void:
|
||||
await call
|
||||
else:
|
||||
discard await call
|
||||
return false
|
||||
except ProviderError, SignerError, EstimateGasError:
|
||||
let e = getCurrentException()
|
||||
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
|
||||
@ -1,29 +1,40 @@
|
||||
import pkg/serde
|
||||
import pkg/stew/byteutils
|
||||
import ./basics
|
||||
|
||||
type Transaction* = object
|
||||
sender*: ?Address
|
||||
to*: Address
|
||||
data*: seq[byte]
|
||||
nonce*: ?UInt256
|
||||
chainId*: ?UInt256
|
||||
gasPrice*: ?UInt256
|
||||
maxFee*: ?UInt256
|
||||
maxPriorityFee*: ?UInt256
|
||||
gasLimit*: ?UInt256
|
||||
type
|
||||
TransactionType* = enum
|
||||
Legacy = 0,
|
||||
AccessList = 1,
|
||||
Dynamic = 2
|
||||
Transaction* {.serialize.} = object
|
||||
sender* {.serialize("from").}: ?Address
|
||||
to*: Address
|
||||
data*: seq[byte]
|
||||
value*: UInt256
|
||||
nonce*: ?UInt256
|
||||
chainId*: ?UInt256
|
||||
gasPrice*: ?UInt256
|
||||
maxPriorityFeePerGas*: ?UInt256
|
||||
maxFeePerGas*: ?UInt256
|
||||
gasLimit*: ?UInt256
|
||||
transactionType* {.serialize("type").}: ?TransactionType
|
||||
|
||||
func `$`*(transaction: Transaction): string =
|
||||
result = "("
|
||||
if sender =? transaction.sender:
|
||||
result &= "from: " & $sender & ", "
|
||||
result &= "to: " & $transaction.to & ", "
|
||||
result &= "data: 0x" & $transaction.data.toHex
|
||||
result &= "value: " & $transaction.value & ", "
|
||||
result &= "data: 0x" & $(transaction.data.toHex)
|
||||
if nonce =? transaction.nonce:
|
||||
result &= ", nonce: 0x" & $nonce.toHex
|
||||
result &= ", nonce: " & $nonce
|
||||
if chainId =? transaction.chainId:
|
||||
result &= ", chainId: " & $chainId
|
||||
if gasPrice =? transaction.gasPrice:
|
||||
result &= ", gasPrice: 0x" & $gasPrice.toHex
|
||||
result &= ", gasPrice: " & $gasPrice
|
||||
if gasLimit =? transaction.gasLimit:
|
||||
result &= ", gasLimit: 0x" & $gasLimit.toHex
|
||||
result &= ", gasLimit: " & $gasLimit
|
||||
if txType =? transaction.transactionType:
|
||||
result &= ", type: " & $txType
|
||||
result &= ")"
|
||||
|
||||
@ -1,113 +1,3 @@
|
||||
import eth/keys
|
||||
import eth/rlp
|
||||
import eth/common
|
||||
import eth/common/transaction as ct
|
||||
import ./provider
|
||||
import ./transaction
|
||||
import ./signer
|
||||
import ./signers/wallet
|
||||
|
||||
export keys
|
||||
|
||||
var rng {.threadvar.}: ref HmacDrbgContext
|
||||
|
||||
proc getRng: ref HmacDrbgContext =
|
||||
if rng.isNil:
|
||||
rng = newRng()
|
||||
rng
|
||||
|
||||
type SignableTransaction = common.Transaction
|
||||
|
||||
type WalletError* = object of EthersError
|
||||
type Wallet* = ref object of Signer
|
||||
privateKey*: PrivateKey
|
||||
publicKey*: PublicKey
|
||||
address*: Address
|
||||
provider*: ?Provider
|
||||
|
||||
proc new*(_: type Wallet, pk: string, provider: Provider): Wallet =
|
||||
result = Wallet()
|
||||
result.privateKey = PrivateKey.fromHex(pk).value
|
||||
result.publicKey = result.privateKey.toPublicKey()
|
||||
result.address = Address.init(result.publicKey.toCanonicalAddress())
|
||||
result.provider = some provider
|
||||
proc new*(_: type Wallet, pk: string): Wallet =
|
||||
result = Wallet()
|
||||
result.privateKey = PrivateKey.fromHex(pk).value
|
||||
result.publicKey = result.privateKey.toPublicKey()
|
||||
result.address = Address.init(result.publicKey.toCanonicalAddress())
|
||||
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:
|
||||
raise newException(WalletError, "Wallet has no provider")
|
||||
provider
|
||||
|
||||
method getAddress(wallet: Wallet): Future[Address] {.async.} =
|
||||
return wallet.address
|
||||
|
||||
proc signTransaction(tr: var SignableTransaction, pk: PrivateKey) =
|
||||
let h = tr.txHashNoSignature
|
||||
let s = sign(pk, SkMessage(h.data))
|
||||
|
||||
let r = toRaw(s)
|
||||
let v = r[64]
|
||||
|
||||
tr.R = fromBytesBe(UInt256, r.toOpenArray(0, 31))
|
||||
tr.S = fromBytesBE(UInt256, r.toOpenArray(32, 63))
|
||||
|
||||
case tr.txType:
|
||||
of TxLegacy:
|
||||
#tr.V = int64(v) + int64(tr.chainId)*2 + 35 #TODO does not work, not sure why. Sending the tx results in error of too little funds. Maybe something wrong with signature and a wrong sender gets encoded?
|
||||
tr.V = int64(v) + 27
|
||||
of TxEip1559:
|
||||
tr.V = int64(v)
|
||||
else:
|
||||
raise newException(WalletError, "Transaction type not supported")
|
||||
|
||||
proc signTransaction*(wallet: Wallet, tx: transaction.Transaction): Future[seq[byte]] {.async.} =
|
||||
if sender =? tx.sender and sender != wallet.address:
|
||||
raise newException(WalletError, "from address mismatch")
|
||||
|
||||
without nonce =? tx.nonce and chainId =? tx.chainId and gasLimit =? tx.gasLimit:
|
||||
raise newException(WalletError, "Transaction is properly populated")
|
||||
|
||||
var s: SignableTransaction
|
||||
|
||||
if maxFee =? tx.maxFee and maxPriorityFee =? tx.maxPriorityFee:
|
||||
s.txType = TxEip1559
|
||||
s.maxFee = GasInt(maxFee.truncate(uint64))
|
||||
s.maxPriorityFee = GasInt(maxPriorityFee.truncate(uint64))
|
||||
elif gasPrice =? tx.gasPrice:
|
||||
s.txType = TxLegacy
|
||||
s.gasPrice = GasInt(gasPrice.truncate(uint64))
|
||||
else:
|
||||
raise newException(WalletError, "Transaction is properly populated")
|
||||
|
||||
s.chainId = ChainId(chainId.truncate(uint64))
|
||||
s.gasLimit = GasInt(gasLimit.truncate(uint64))
|
||||
s.nonce = nonce.truncate(uint64)
|
||||
s.to = some EthAddress(tx.to)
|
||||
s.payload = tx.data
|
||||
signTransaction(s, wallet.privateKey)
|
||||
|
||||
return rlp.encode(s)
|
||||
|
||||
method sendTransaction*(wallet: Wallet, tx: transaction.Transaction): Future[TransactionResponse] {.async.} =
|
||||
let rawTX = await signTransaction(wallet, tx)
|
||||
return await provider(wallet).sendTransaction(rawTX)
|
||||
|
||||
#TODO add functionality to sign messages
|
||||
|
||||
#TODO add functionality to create wallets from Mnemoniks or Keystores
|
||||
export wallet
|
||||
1
nim.cfg
1
nim.cfg
@ -1,3 +1,4 @@
|
||||
-d:"chronicles_log_level=INFO"
|
||||
-d:"json_rpc_websocket_package=websock"
|
||||
--warning[LockLevel]:off
|
||||
--warning[DotLikeOps]:off
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
switch("path", "..")
|
||||
when (NimMajor, NimMinor) >= (1, 4):
|
||||
switch("hint", "XCannotRaiseY:off")
|
||||
when (NimMajor, NimMinor, NimPatch) >= (1, 6, 11):
|
||||
switch("warning", "BareExcept:off")
|
||||
|
||||
--define:"chronicles_enabled:off"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import std/json
|
||||
import pkg/serde
|
||||
import pkg/ethers/basics
|
||||
|
||||
type Deployment* = object
|
||||
|
||||
14
testmodule/helpers.nim
Normal file
14
testmodule/helpers.nim
Normal file
@ -0,0 +1,14 @@
|
||||
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)
|
||||
@ -11,10 +11,15 @@ func new*(_: type MockSigner, provider: Provider): MockSigner =
|
||||
method provider*(signer: MockSigner): Provider =
|
||||
signer.provider
|
||||
|
||||
method getAddress*(signer: MockSigner): Future[Address] {.async.} =
|
||||
method getAddress*(
|
||||
signer: MockSigner): Future[Address]
|
||||
{.async: (raises:[ProviderError, SignerError, CancelledError]).} =
|
||||
|
||||
return signer.address
|
||||
|
||||
method sendTransaction*(signer: MockSigner,
|
||||
transaction: Transaction):
|
||||
Future[TransactionResponse] {.async.} =
|
||||
method sendTransaction*(
|
||||
signer: MockSigner,
|
||||
transaction: Transaction): Future[TransactionResponse]
|
||||
{.async: (raises:[SignerError, ProviderError, CancelledError]).} =
|
||||
|
||||
signer.transactions.add(transaction)
|
||||
|
||||
56
testmodule/providers/jsonrpc/rpc_mock.nim
Normal file
56
testmodule/providers/jsonrpc/rpc_mock.nim
Normal file
@ -0,0 +1,56 @@
|
||||
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()
|
||||
254
testmodule/providers/jsonrpc/testConversions.nim
Normal file
254
testmodule/providers/jsonrpc/testConversions.nim
Normal file
@ -0,0 +1,254 @@
|
||||
import std/strutils
|
||||
import std/unittest
|
||||
import pkg/ethers/provider
|
||||
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":
|
||||
|
||||
test "missing block number in Block isNone":
|
||||
var json = %*{
|
||||
"number": newJNull(),
|
||||
"hash":"0x2d7d68c8f48b4213d232a1f12cab8c9fac6195166bb70a5fb21397984b9fe1c7",
|
||||
"timestamp":"0x6285c293"
|
||||
}
|
||||
|
||||
let blk1 = !Block.fromJson(json)
|
||||
check blk1.number.isNone
|
||||
|
||||
json["number"] = newJString("")
|
||||
|
||||
let blk2 = !Block.fromJson(json)
|
||||
check blk2.number.isNone
|
||||
|
||||
test "missing block hash in Block isNone":
|
||||
|
||||
var blkJson = %*{
|
||||
"subscription": "0x20",
|
||||
"result":{
|
||||
"number": "0x1",
|
||||
"hash": newJNull(),
|
||||
"timestamp": "0x6285c293"
|
||||
}
|
||||
}
|
||||
|
||||
without blk =? Block.fromJson(blkJson["result"]):
|
||||
unittest.fail
|
||||
check blk.hash.isNone
|
||||
|
||||
test "missing block number in TransactionReceipt isNone":
|
||||
var json = %*{
|
||||
"from": newJNull(),
|
||||
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
|
||||
"contractAddress": newJNull(),
|
||||
"transactionIndex": "0x0",
|
||||
"gasUsed": "0x10db1",
|
||||
"logsBloom": "0x00000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000840020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000020000000000000000000000000000000000001000000000000000000000000000000",
|
||||
"blockHash": "0x7b00154e06fe4f27a87208eba220efb4dbc52f7429549a39a17bba2e0d98b960",
|
||||
"transactionHash": "0xa64f07b370cbdcce381ec9bfb6c8004684341edfb6848fd418189969d4b9139c",
|
||||
"logs": [
|
||||
{
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000000064",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blockNumber": newJNull(),
|
||||
"cumulativeGasUsed": "0x10db1",
|
||||
"status": "0x1",
|
||||
"effectiveGasPrice": "0x3b9aca08",
|
||||
"type": "0x0"
|
||||
}
|
||||
|
||||
without receipt1 =? TransactionReceipt.fromJson(json):
|
||||
unittest.fail
|
||||
check receipt1.blockNumber.isNone
|
||||
|
||||
json["blockNumber"] = newJString("")
|
||||
without receipt2 =? TransactionReceipt.fromJson(json):
|
||||
unittest.fail
|
||||
check receipt2.blockNumber.isNone
|
||||
|
||||
test "missing block hash in TransactionReceipt isNone":
|
||||
let json = %*{
|
||||
"from": newJNull(),
|
||||
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
|
||||
"contractAddress": newJNull(),
|
||||
"transactionIndex": "0x0",
|
||||
"gasUsed": "0x10db1",
|
||||
"logsBloom": "0x00000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000840020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000020000000000000000000000000000000000001000000000000000000000000000000",
|
||||
"blockHash": newJNull(),
|
||||
"transactionHash": "0xa64f07b370cbdcce381ec9bfb6c8004684341edfb6848fd418189969d4b9139c",
|
||||
"logs": [
|
||||
{
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000000064",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blockNumber": newJNull(),
|
||||
"cumulativeGasUsed": "0x10db1",
|
||||
"status": "0x1",
|
||||
"effectiveGasPrice": "0x3b9aca08",
|
||||
"type": "0x0"
|
||||
}
|
||||
|
||||
without receipt =? TransactionReceipt.fromJson(json):
|
||||
unittest.fail
|
||||
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\""
|
||||
27
testmodule/providers/jsonrpc/testErrors.nim
Normal file
27
testmodule/providers/jsonrpc/testErrors.nim
Normal file
@ -0,0 +1,27 @@
|
||||
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]
|
||||
108
testmodule/providers/jsonrpc/testJsonRpcProvider.nim
Normal file
108
testmodule/providers/jsonrpc/testJsonRpcProvider.nim
Normal file
@ -0,0 +1,108 @@
|
||||
import std/os
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/chronos
|
||||
import pkg/ethers
|
||||
import pkg/ethers/providers/jsonrpc/conversions
|
||||
import pkg/stew/byteutils
|
||||
import ../../examples
|
||||
import ../../miner
|
||||
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
|
||||
|
||||
suite "JsonRpcProvider (" & url & ")":
|
||||
|
||||
var provider: JsonRpcProvider
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new(url, pollingInterval = 100.millis)
|
||||
|
||||
|
||||
teardown:
|
||||
await provider.close()
|
||||
|
||||
test "can be instantiated with a default URL":
|
||||
discard JsonRpcProvider.new()
|
||||
|
||||
test "lists all accounts":
|
||||
let accounts = await provider.listAccounts()
|
||||
check accounts.len > 0
|
||||
|
||||
test "sends raw messages to the provider":
|
||||
let response = await provider.send("evm_mine")
|
||||
check response == %"0"
|
||||
|
||||
test "returns block number":
|
||||
let blocknumber1 = await provider.getBlockNumber()
|
||||
discard await provider.send("evm_mine")
|
||||
let blocknumber2 = await provider.getBlockNumber()
|
||||
check blocknumber2 > blocknumber1
|
||||
|
||||
test "returns block":
|
||||
let block1 = !await provider.getBlock(BlockTag.earliest)
|
||||
let block2 = !await provider.getBlock(BlockTag.latest)
|
||||
check block1.hash != block2.hash
|
||||
check !block1.number < !block2.number
|
||||
check block1.timestamp < block2.timestamp
|
||||
|
||||
test "subscribes to new blocks":
|
||||
let oldBlock = !await provider.getBlock(BlockTag.latest)
|
||||
discard await provider.send("evm_mine")
|
||||
var newBlock: Block
|
||||
let blockHandler = proc(blck: ?!Block) {.raises:[].}= newBlock = blck.value
|
||||
let subscription = await provider.subscribe(blockHandler)
|
||||
discard await provider.send("evm_mine")
|
||||
check eventually newBlock.number.isSome
|
||||
check !newBlock.number > !oldBlock.number
|
||||
check newBlock.timestamp > oldBlock.timestamp
|
||||
check newBlock.hash != oldBlock.hash
|
||||
await subscription.unsubscribe()
|
||||
|
||||
test "can send a transaction":
|
||||
let signer = provider.getSigner()
|
||||
let transaction = Transaction.example
|
||||
let populated = await signer.populateTransaction(transaction)
|
||||
|
||||
let txResp = await signer.sendTransaction(populated)
|
||||
check txResp.hash.len == 32
|
||||
check UInt256.fromHex("0x" & txResp.hash.toHex) > 0
|
||||
|
||||
test "can wait for a transaction to be confirmed":
|
||||
for confirmations in 1..3:
|
||||
let signer = provider.getSigner()
|
||||
let transaction = Transaction.example
|
||||
let populated = await signer.populateTransaction(transaction)
|
||||
let confirming = signer.sendTransaction(populated).confirm(confirmations)
|
||||
await sleepAsync(100.millis) # wait for tx to be mined
|
||||
await provider.mineBlocks(confirmations)
|
||||
let receipt = await confirming
|
||||
check receipt.blockNumber.isSome
|
||||
|
||||
test "confirmation times out":
|
||||
let hash = TransactionHash.example
|
||||
let tx = TransactionResponse(provider: provider, hash: hash)
|
||||
let confirming = tx.confirm(confirmations = 2, timeout = 5)
|
||||
await sleepAsync(100.millis) # wait for confirm to subscribe to new blocks
|
||||
await provider.mineBlocks(5)
|
||||
expect EthersError:
|
||||
discard await confirming
|
||||
|
||||
test "raises JsonRpcProviderError when something goes wrong":
|
||||
let provider = JsonRpcProvider.new("http://invalid.")
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.listAccounts()
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.send("evm_mine")
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.getBlockNumber()
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.getBlock(BlockTag.latest)
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.subscribe(proc(_: ?!Block) = discard)
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.getSigner().sendTransaction(Transaction.example)
|
||||
|
||||
test "syncing":
|
||||
let isSyncing = await provider.isSyncing()
|
||||
check not isSyncing
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import pkg/asynctest
|
||||
import std/os
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/ethers
|
||||
import pkg/stew/byteutils
|
||||
import ./examples
|
||||
import ../../examples
|
||||
|
||||
suite "JsonRpcSigner":
|
||||
|
||||
var provider: JsonRpcProvider
|
||||
var accounts: seq[Address]
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new()
|
||||
provider = JsonRpcProvider.new("http://" & providerUrl)
|
||||
accounts = await provider.listAccounts()
|
||||
|
||||
teardown:
|
||||
await provider.close()
|
||||
|
||||
test "is connected to the first account of the provider by default":
|
||||
let signer = provider.getSigner()
|
||||
check (await signer.getAddress()) == accounts[0]
|
||||
@ -50,20 +55,27 @@ suite "JsonRpcSigner":
|
||||
let transaction = Transaction.example
|
||||
let populated = await signer.populateTransaction(transaction)
|
||||
check !populated.sender == await signer.getAddress()
|
||||
check !populated.gasPrice == await signer.getGasPrice()
|
||||
check !populated.nonce == await signer.getTransactionCount(BlockTag.pending)
|
||||
check !populated.gasLimit == await signer.estimateGas(transaction)
|
||||
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":
|
||||
let signer = provider.getSigner()
|
||||
var transaction = Transaction.example
|
||||
transaction.sender = some await signer.getAddress()
|
||||
transaction.nonce = some UInt256.example
|
||||
transaction.chainId = some await signer.getChainId()
|
||||
transaction.gasPrice = some UInt256.example
|
||||
transaction.maxPriorityFeePerGas = some UInt256.example
|
||||
transaction.gasLimit = some UInt256.example
|
||||
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
|
||||
|
||||
test "populate fails when sender does not match signer address":
|
||||
218
testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim
Normal file
218
testmodule/providers/jsonrpc/testJsonRpcSubscriptions.nim
Normal file
@ -0,0 +1,218 @@
|
||||
import std/os
|
||||
import std/importutils
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/serde
|
||||
import pkg/json_rpc/rpcclient
|
||||
import pkg/json_rpc/rpcserver
|
||||
import ethers/provider
|
||||
import ethers/providers/jsonrpc/subscriptions
|
||||
|
||||
import ../../examples
|
||||
import ./rpc_mock
|
||||
|
||||
suite "JsonRpcSubscriptions":
|
||||
|
||||
test "can be instantiated with an http client":
|
||||
let client = newRpcHttpClient()
|
||||
let subscriptions = JsonRpcSubscriptions.new(client)
|
||||
check not isNil subscriptions
|
||||
|
||||
test "can be instantiated with a websocket client":
|
||||
let client = newRpcWebSocketClient()
|
||||
let subscriptions = JsonRpcSubscriptions.new(client)
|
||||
check not isNil subscriptions
|
||||
|
||||
template subscriptionTests(subscriptions, client) =
|
||||
|
||||
test "subscribes to new blocks":
|
||||
var latestBlock: Block
|
||||
proc callback(blck: ?!Block) =
|
||||
latestBlock = blck.value
|
||||
let subscription = await subscriptions.subscribeBlocks(callback)
|
||||
discard await client.call("evm_mine", newJArray())
|
||||
check eventually latestBlock.number.isSome
|
||||
check latestBlock.hash.isSome
|
||||
check latestBlock.timestamp > 0.u256
|
||||
await subscriptions.unsubscribe(subscription)
|
||||
|
||||
test "stops listening to new blocks when unsubscribed":
|
||||
var count = 0
|
||||
proc callback(blck: ?!Block) =
|
||||
if blck.isOk:
|
||||
inc count
|
||||
let subscription = await subscriptions.subscribeBlocks(callback)
|
||||
discard await client.call("evm_mine", newJArray())
|
||||
check eventually count > 0
|
||||
await subscriptions.unsubscribe(subscription)
|
||||
count = 0
|
||||
discard await client.call("evm_mine", newJArray())
|
||||
await sleepAsync(100.millis)
|
||||
check count == 0
|
||||
|
||||
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":
|
||||
var count = 0
|
||||
proc callback(blck: ?!Block) =
|
||||
if blck.isOk:
|
||||
inc count
|
||||
discard await subscriptions.subscribeBlocks(callback)
|
||||
discard await client.call("evm_mine", newJArray())
|
||||
check eventually count > 0
|
||||
await subscriptions.close()
|
||||
count = 0
|
||||
discard await client.call("evm_mine", newJArray())
|
||||
await sleepAsync(100.millis)
|
||||
check count == 0
|
||||
|
||||
suite "Web socket subscriptions":
|
||||
|
||||
var subscriptions: JsonRpcSubscriptions
|
||||
var client: RpcWebSocketClient
|
||||
|
||||
setup:
|
||||
client = newRpcWebSocketClient()
|
||||
await client.connect("ws://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545"))
|
||||
subscriptions = JsonRpcSubscriptions.new(client)
|
||||
subscriptions.start()
|
||||
|
||||
teardown:
|
||||
await subscriptions.close()
|
||||
await client.close()
|
||||
|
||||
subscriptionTests(subscriptions, client)
|
||||
|
||||
suite "HTTP polling subscriptions":
|
||||
|
||||
var subscriptions: JsonRpcSubscriptions
|
||||
var client: RpcHttpClient
|
||||
|
||||
setup:
|
||||
client = newRpcHttpClient()
|
||||
await client.connect("http://" & getEnv("ETHERS_TEST_PROVIDER", "localhost:8545"))
|
||||
subscriptions = JsonRpcSubscriptions.new(client,
|
||||
pollingInterval = 100.millis)
|
||||
subscriptions.start()
|
||||
|
||||
teardown:
|
||||
await subscriptions.close()
|
||||
await client.close()
|
||||
|
||||
subscriptionTests(subscriptions, client)
|
||||
|
||||
suite "HTTP polling subscriptions - mock tests":
|
||||
|
||||
var subscriptions: PollingSubscriptions
|
||||
var client: RpcHttpClient
|
||||
var mockServer: MockRpcHttpServer
|
||||
|
||||
privateAccess(PollingSubscriptions)
|
||||
privateAccess(JsonRpcSubscriptions)
|
||||
|
||||
proc startServer() {.async.} =
|
||||
mockServer = MockRpcHttpServer.new()
|
||||
mockServer.start()
|
||||
await client.connect("http://" & $mockServer.localAddress()[0])
|
||||
|
||||
proc stopServer() {.async.} =
|
||||
await mockServer.stop()
|
||||
|
||||
setup:
|
||||
client = newRpcHttpClient()
|
||||
await startServer()
|
||||
|
||||
subscriptions = PollingSubscriptions(
|
||||
JsonRpcSubscriptions.new(
|
||||
client,
|
||||
pollingInterval = 1.millis))
|
||||
subscriptions.start()
|
||||
|
||||
teardown:
|
||||
await subscriptions.close()
|
||||
await client.close()
|
||||
await mockServer.stop()
|
||||
|
||||
test "filter not found error recreates log filter":
|
||||
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
|
||||
let emptyHandler = proc(log: ?!Log) = discard
|
||||
|
||||
check subscriptions.logFilters.len == 0
|
||||
check subscriptions.subscriptionMapping.len == 0
|
||||
|
||||
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
|
||||
|
||||
check subscriptions.logFilters[id] == filter
|
||||
check subscriptions.subscriptionMapping[id] == id
|
||||
check subscriptions.logFilters.len == 1
|
||||
check subscriptions.subscriptionMapping.len == 1
|
||||
|
||||
mockServer.invalidateFilter(id)
|
||||
|
||||
check eventually subscriptions.subscriptionMapping[id] != id
|
||||
|
||||
test "recreated log filter can be still unsubscribed using the original id":
|
||||
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
|
||||
let emptyHandler = proc(log: ?!Log) = discard
|
||||
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
|
||||
mockServer.invalidateFilter(id)
|
||||
check eventually subscriptions.subscriptionMapping[id] != id
|
||||
|
||||
await subscriptions.unsubscribe(id)
|
||||
|
||||
check not subscriptions.logFilters.hasKey id
|
||||
check not subscriptions.subscriptionMapping.hasKey id
|
||||
|
||||
test "filter not found error recreates block filter":
|
||||
let emptyHandler = proc(blck: ?!Block) = discard
|
||||
|
||||
check subscriptions.subscriptionMapping.len == 0
|
||||
let id = await subscriptions.subscribeBlocks(emptyHandler)
|
||||
check subscriptions.subscriptionMapping[id] == id
|
||||
|
||||
mockServer.invalidateFilter(id)
|
||||
|
||||
check eventually subscriptions.subscriptionMapping[id] != id
|
||||
|
||||
test "recreated block filter can be still unsubscribed using the original id":
|
||||
let emptyHandler = proc(blck: ?!Block) = discard
|
||||
let id = await subscriptions.subscribeBlocks(emptyHandler)
|
||||
mockServer.invalidateFilter(id)
|
||||
check eventually subscriptions.subscriptionMapping[id] != id
|
||||
|
||||
await subscriptions.unsubscribe(id)
|
||||
|
||||
check not subscriptions.subscriptionMapping.hasKey id
|
||||
|
||||
test "polling continues with new filter after temporary error":
|
||||
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
|
||||
let emptyHandler = proc(log: ?!Log) = discard
|
||||
|
||||
let id = await subscriptions.subscribeLogs(filter, emptyHandler)
|
||||
|
||||
await stopServer()
|
||||
mockServer.invalidateFilter(id)
|
||||
await sleepAsync(50.milliseconds)
|
||||
await startServer()
|
||||
|
||||
check eventually subscriptions.subscriptionMapping[id] != id
|
||||
|
||||
test "calls callback with failed result on error":
|
||||
let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example])
|
||||
var failedResultReceived = false
|
||||
|
||||
proc handler(log: ?!Log) =
|
||||
if log.isErr:
|
||||
failedResultReceived = true
|
||||
|
||||
let id = await subscriptions.subscribeLogs(filter, handler)
|
||||
|
||||
await sleepAsync(50.milliseconds)
|
||||
mockServer.nextGetChangesReturnsError = true
|
||||
check eventually failedResultReceived
|
||||
56
testmodule/providers/jsonrpc/testWsResubscription.nim
Normal file
56
testmodule/providers/jsonrpc/testWsResubscription.nim
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
||||
8
testmodule/providers/testJsonRpc.nim
Normal file
8
testmodule/providers/testJsonRpc.nim
Normal file
@ -0,0 +1,8 @@
|
||||
import ./jsonrpc/testJsonRpcProvider
|
||||
import ./jsonrpc/testJsonRpcSigner
|
||||
import ./jsonrpc/testJsonRpcSubscriptions
|
||||
import ./jsonrpc/testWsResubscription
|
||||
import ./jsonrpc/testConversions
|
||||
import ./jsonrpc/testErrors
|
||||
|
||||
{.warning[UnusedImport]:off.}
|
||||
@ -1,9 +1,14 @@
|
||||
import ./testJsonRpcProvider
|
||||
import ./testJsonRpcSigner
|
||||
import ./testProviders
|
||||
import ./testContracts
|
||||
import ./testReturns
|
||||
import ./testEnums
|
||||
import ./testEvents
|
||||
import ./testWallet
|
||||
import ./testTesting
|
||||
import ./testErc20
|
||||
import ./testGasEstimation
|
||||
import ./testErrorDecoding
|
||||
import ./testCustomErrors
|
||||
import ./testBlockTag
|
||||
|
||||
{.warning[UnusedImport]:off.}
|
||||
|
||||
@ -3,8 +3,7 @@ author = "Nim Ethers Authors"
|
||||
description = "Tests for Nim Ethers library"
|
||||
license = "MIT"
|
||||
|
||||
requires "asynctest >= 0.3.0 & < 0.4.0"
|
||||
requires "questionable >= 0.10.3 & < 0.11.0"
|
||||
requires "asynctest >= 0.5.4 & < 0.6.0"
|
||||
|
||||
task test, "Run the test suite":
|
||||
exec "nimble install -d -y"
|
||||
|
||||
56
testmodule/testBlockTag.nim
Normal file
56
testmodule/testBlockTag.nim
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
||||
@ -1,183 +1,327 @@
|
||||
import std/json
|
||||
import pkg/asynctest
|
||||
import pkg/serde
|
||||
import std/os
|
||||
import std/options
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/questionable
|
||||
import pkg/stint
|
||||
import pkg/ethers
|
||||
import pkg/ethers/erc20
|
||||
import ./hardhat
|
||||
import ./helpers
|
||||
import ./miner
|
||||
import ./mocks
|
||||
|
||||
type
|
||||
TestToken = ref object of Erc20Token
|
||||
|
||||
Erc20* = ref object of Contract
|
||||
TestToken = ref object of Erc20
|
||||
method mint(token: TestToken, holder: Address, amount: UInt256): Confirmable {.base, contract.}
|
||||
method myBalance(token: TestToken): UInt256 {.base, contract, view.}
|
||||
|
||||
Transfer = object of Event
|
||||
sender {.indexed.}: Address
|
||||
receiver {.indexed.}: Address
|
||||
value: UInt256
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
|
||||
|
||||
method name*(erc20: Erc20): string {.base, contract, view.}
|
||||
method totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.}
|
||||
method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract, view.}
|
||||
method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.}
|
||||
method transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.base, contract.}
|
||||
method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.}
|
||||
suite "Contracts (" & url & ")":
|
||||
|
||||
suite "Contracts":
|
||||
var token: TestToken
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
var accounts: seq[Address]
|
||||
|
||||
var token: TestToken
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
var accounts: seq[Address]
|
||||
setup:
|
||||
provider = JsonRpcProvider.new(url, pollingInterval = 100.millis)
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
accounts = await provider.listAccounts()
|
||||
let deployment = readDeployment()
|
||||
token = TestToken.new(!deployment.address(TestToken), provider)
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new("ws://localhost:8545")
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
accounts = await provider.listAccounts()
|
||||
let deployment = readDeployment()
|
||||
token = TestToken.new(!deployment.address(TestToken), provider)
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
test "can call constant functions":
|
||||
check (await token.name()) == "TestToken"
|
||||
check (await token.totalSupply()) == 0.u256
|
||||
check (await token.balanceOf(accounts[0])) == 0.u256
|
||||
check (await token.allowance(accounts[0], accounts[1])) == 0.u256
|
||||
|
||||
test "can call constant functions":
|
||||
check (await token.name()) == "TestToken"
|
||||
check (await token.totalSupply()) == 0.u256
|
||||
check (await token.balanceOf(accounts[0])) == 0.u256
|
||||
check (await token.allowance(accounts[0], accounts[1])) == 0.u256
|
||||
test "can call non-constant functions":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
discard await token.mint(accounts[1], 100.u256)
|
||||
check (await token.totalSupply()) == 100.u256
|
||||
check (await token.balanceOf(accounts[1])) == 100.u256
|
||||
|
||||
test "can call non-constant functions":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
discard await token.mint(accounts[1], 100.u256)
|
||||
check (await token.totalSupply()) == 100.u256
|
||||
check (await token.balanceOf(accounts[1])) == 100.u256
|
||||
test "can call constant functions with a signer and the account is used for the call":
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
discard await token.connect(signer0).mint(accounts[1], 100.u256)
|
||||
check (await token.connect(signer0).myBalance()) == 0.u256
|
||||
check (await token.connect(signer1).myBalance()) == 100.u256
|
||||
|
||||
test "can call non-constant functions without a signer":
|
||||
discard await token.mint(accounts[1], 100.u256)
|
||||
check (await token.balanceOf(accounts[1])) == 0.u256
|
||||
test "can call non-constant functions without a signer":
|
||||
discard await token.mint(accounts[1], 100.u256)
|
||||
check (await token.balanceOf(accounts[1])) == 0.u256
|
||||
|
||||
test "can call constant functions without a return type":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract, view.}
|
||||
await mint(token, accounts[1], 100.u256)
|
||||
check (await balanceOf(token, accounts[1])) == 0.u256
|
||||
test "can call constant functions without a return type":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract, view.}
|
||||
await token.mint(accounts[1], 100.u256)
|
||||
check (await balanceOf(token, accounts[1])) == 0.u256
|
||||
|
||||
test "can call non-constant functions without a return type":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract.}
|
||||
await token.mint(accounts[1], 100.u256)
|
||||
check (await balanceOf(token, accounts[1])) == 100.u256
|
||||
test "can call non-constant functions without a return type":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract.}
|
||||
await token.mint(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":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
proc mint(token: TestToken,
|
||||
holder: Address,
|
||||
amount: UInt256): Confirmable {.contract.}
|
||||
let confirmable = await token.mint(accounts[1], 100.u256)
|
||||
check confirmable is Confirmable
|
||||
check confirmable.response.isSome
|
||||
|
||||
test "can call non-constant functions with a Confirmable return type":
|
||||
test "fails to compile when function has an implementation":
|
||||
let works = compiles:
|
||||
proc foo(token: TestToken, bar: Address) {.contract.} = discard
|
||||
check not works
|
||||
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
proc mint(token: TestToken,
|
||||
holder: Address,
|
||||
amount: UInt256): Confirmable {.contract.}
|
||||
let txResp = await token.mint(accounts[1], 100.u256)
|
||||
check txResp is Confirmable
|
||||
check txResp.isSome
|
||||
test "fails to compile when function has no parameters":
|
||||
let works = compiles:
|
||||
proc foo() {.contract.}
|
||||
check not works
|
||||
|
||||
test "fails to compile when function has an implementation":
|
||||
let works = compiles:
|
||||
proc foo(token: TestToken, bar: Address) {.contract.} = discard
|
||||
check not works
|
||||
test "fails to compile when non-constant function has a return type":
|
||||
let works = compiles:
|
||||
proc foo(token: TestToken, bar: Address): UInt256 {.contract.}
|
||||
check not works
|
||||
|
||||
test "fails to compile when function has no parameters":
|
||||
let works = compiles:
|
||||
proc foo() {.contract.}
|
||||
check not works
|
||||
test "can connect to different providers and signers":
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
discard await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
check (await token.connect(provider).balanceOf(accounts[0])) == 50.u256
|
||||
check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256
|
||||
check (await token.connect(provider).balanceOf(accounts[2])) == 25.u256
|
||||
|
||||
test "fails to compile when non-constant function has a return type":
|
||||
let works = compiles:
|
||||
proc foo(token: TestToken, bar: Address): UInt256 {.contract.}
|
||||
check not works
|
||||
test "takes custom values for nonce, gasprice and maxPriorityFeePerGas":
|
||||
let overrides = TransactionOverrides(
|
||||
nonce: some 100.u256,
|
||||
maxPriorityFeePerGas: some 200.u256,
|
||||
gasLimit: some 300.u256
|
||||
)
|
||||
let signer = MockSigner.new(provider)
|
||||
discard await token.connect(signer).mint(accounts[0], 42.u256, overrides)
|
||||
check signer.transactions.len == 1
|
||||
check signer.transactions[0].nonce == overrides.nonce
|
||||
check signer.transactions[0].maxPriorityFeePerGas == overrides.maxPriorityFeePerGas
|
||||
check signer.transactions[0].gasLimit == overrides.gasLimit
|
||||
|
||||
test "can connect to different providers and signers":
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
check (await token.connect(provider).balanceOf(accounts[0])) == 50.u256
|
||||
check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256
|
||||
check (await token.connect(provider).balanceOf(accounts[2])) == 25.u256
|
||||
test "can call functions 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()
|
||||
|
||||
test "takes custom values for nonce, gasprice and gaslimit":
|
||||
let overrides = TransactionOverrides(
|
||||
nonce: some 100.u256,
|
||||
gasPrice: some 200.u256,
|
||||
gasLimit: some 300.u256
|
||||
)
|
||||
let signer = MockSigner.new(provider)
|
||||
discard await token.connect(signer).mint(accounts[0], 42.u256, overrides)
|
||||
check signer.transactions.len == 1
|
||||
check signer.transactions[0].nonce == overrides.nonce
|
||||
check signer.transactions[0].gasPrice == overrides.gasPrice
|
||||
check signer.transactions[0].gasLimit == overrides.gasLimit
|
||||
let beforeMint = CallOverrides(blockTag: some BlockTag.init(block1))
|
||||
let afterMint = CallOverrides(blockTag: some BlockTag.init(block2))
|
||||
|
||||
test "receives events when subscribed":
|
||||
var transfers: seq[Transfer]
|
||||
check (await token.balanceOf(accounts[0], beforeMint)) == 0
|
||||
check (await token.balanceOf(accounts[0], afterMint)) == 100
|
||||
|
||||
proc handleTransfer(transfer: Transfer) =
|
||||
transfers.add(transfer)
|
||||
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 signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
let beforeMint = CallOverrides(blockTag: some BlockTag.init(block1))
|
||||
let afterMint = CallOverrides(blockTag: some BlockTag.init(block2))
|
||||
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
await subscription.unsubscribe()
|
||||
expect ProviderError:
|
||||
discard await token.transfer(accounts[1], 50.u256, beforeMint)
|
||||
discard await token.transfer(accounts[1], 50.u256, afterMint)
|
||||
|
||||
check transfers == @[
|
||||
Transfer(receiver: accounts[0], value: 100.u256),
|
||||
Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256),
|
||||
Transfer(sender: accounts[1], receiver: accounts[2], value: 25.u256)
|
||||
]
|
||||
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 "stops receiving events when unsubscribed":
|
||||
var transfers: seq[Transfer]
|
||||
test "receives events when subscribed":
|
||||
var transfers: seq[Transfer]
|
||||
|
||||
proc handleTransfer(transfer: Transfer) =
|
||||
transfers.add(transfer)
|
||||
proc handleTransfer(transferRes: ?!Transfer) =
|
||||
without transfer =? transferRes, error:
|
||||
echo error.msg
|
||||
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
transfers.add(transfer)
|
||||
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
await subscription.unsubscribe()
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
|
||||
await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
discard await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
|
||||
check transfers == @[Transfer(receiver: accounts[0], value: 100.u256)]
|
||||
check eventually transfers == @[
|
||||
Transfer(receiver: accounts[0], value: 100.u256),
|
||||
Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256),
|
||||
Transfer(sender: accounts[1], receiver: accounts[2], value: 25.u256)
|
||||
]
|
||||
|
||||
test "can wait for contract interaction tx to be mined":
|
||||
# must not be awaited so we can get newHeads inside of .wait
|
||||
let futMined = provider.mineBlocks(10)
|
||||
await subscription.unsubscribe()
|
||||
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let receipt = await token.connect(signer0)
|
||||
.mint(accounts[1], 100.u256)
|
||||
.confirm(3) # wait for 3 confirmations
|
||||
let endBlock = await provider.getBlockNumber()
|
||||
test "stops receiving events when unsubscribed":
|
||||
var transfers: seq[Transfer]
|
||||
|
||||
check receipt.blockNumber.isSome # was eventually mined
|
||||
proc handleTransfer(transferRes: ?!Transfer) =
|
||||
if transfer =? transferRes:
|
||||
transfers.add(transfer)
|
||||
|
||||
# >= 3 because more blocks may have been mined by the time the
|
||||
# check in `.wait` was done.
|
||||
# +1 for the block the tx was mined in
|
||||
check (endBlock - !receipt.blockNumber) + 1 >= 3
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
|
||||
await futMined
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
|
||||
check eventually transfers.len == 1
|
||||
await subscription.unsubscribe()
|
||||
|
||||
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
await sleepAsync(100.millis)
|
||||
|
||||
check transfers.len == 1
|
||||
|
||||
test "can wait for contract interaction tx to be mined":
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let confirming = token.connect(signer0)
|
||||
.mint(accounts[1], 100.u256)
|
||||
.confirm(3)
|
||||
await sleepAsync(100.millis) # wait for tx to be mined
|
||||
await provider.mineBlocks(2) # two additional blocks
|
||||
let receipt = await confirming
|
||||
check receipt.blockNumber.isSome
|
||||
|
||||
test "can query last block event log":
|
||||
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
|
||||
let logs = await token.queryFilter(Transfer)
|
||||
|
||||
check eventually logs == @[
|
||||
Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256)
|
||||
]
|
||||
|
||||
test "can query past event logs by specifying from and to blocks":
|
||||
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
discard await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
|
||||
let currentBlock = await provider.getBlockNumber()
|
||||
let logs = await token.queryFilter(Transfer,
|
||||
BlockTag.init(currentBlock - 1),
|
||||
BlockTag.latest)
|
||||
|
||||
check logs == @[
|
||||
Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256),
|
||||
Transfer(sender: accounts[1], receiver: accounts[2], value: 25.u256)
|
||||
]
|
||||
|
||||
test "can query past event logs by specifying a block hash":
|
||||
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
|
||||
let receipt = await token.connect(signer0)
|
||||
.mint(accounts[0], 100.u256)
|
||||
.confirm(1)
|
||||
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
|
||||
let logs = await token.queryFilter(Transfer, !receipt.blockHash)
|
||||
|
||||
check logs == @[
|
||||
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
|
||||
|
||||
161
testmodule/testCustomErrors.nim
Normal file
161
testmodule/testCustomErrors.nim
Normal file
@ -0,0 +1,161 @@
|
||||
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])
|
||||
@ -1,5 +1,7 @@
|
||||
import pkg/asynctest
|
||||
import std/os
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/ethers
|
||||
import pkg/serde
|
||||
import ./hardhat
|
||||
|
||||
type
|
||||
@ -13,15 +15,17 @@ suite "Contract enum parameters and return values":
|
||||
var contract: TestEnums
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new("ws://localhost:8545")
|
||||
provider = JsonRpcProvider.new("http://" & providerUrl)
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
let deployment = readDeployment()
|
||||
contract = TestEnums.new(!deployment.address(TestEnums), provider)
|
||||
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
test "handles enum parameter and return value":
|
||||
proc returnValue(contract: TestEnums,
|
||||
|
||||
125
testmodule/testErc20.nim
Normal file
125
testmodule/testErc20.nim
Normal file
@ -0,0 +1,125 @@
|
||||
import std/os
|
||||
import pkg/serde
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/questionable
|
||||
import pkg/stint
|
||||
import pkg/ethers
|
||||
import pkg/ethers/erc20
|
||||
import ./hardhat
|
||||
|
||||
type
|
||||
TestToken = ref object of Erc20Token
|
||||
|
||||
method mint(token: TestToken, holder: Address, amount: UInt256): Confirmable {.base, contract.}
|
||||
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
for url in ["ws://" & providerUrl, "http://" & providerUrl]:
|
||||
|
||||
suite "ERC20 (" & url & ")":
|
||||
|
||||
var token: Erc20Token
|
||||
var testToken: TestToken
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
var accounts: seq[Address]
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new(url, pollingInterval = 100.millis)
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
accounts = await provider.listAccounts()
|
||||
let deployment = readDeployment()
|
||||
testToken = TestToken.new(!deployment.address(TestToken), provider.getSigner())
|
||||
token = Erc20Token.new(!deployment.address(TestToken), provider.getSigner())
|
||||
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
test "retrieves basic information":
|
||||
check (await token.name()) == "TestToken"
|
||||
check (await token.symbol()) == "TST"
|
||||
check (await token.decimals()) == 12
|
||||
check (await token.totalSupply()) == 0.u256
|
||||
check (await token.balanceOf(accounts[0])) == 0.u256
|
||||
check (await token.allowance(accounts[0], accounts[1])) == 0.u256
|
||||
|
||||
test "transfer tokens":
|
||||
check (await token.balanceOf(accounts[0])) == 0.u256
|
||||
check (await token.allowance(accounts[0], accounts[1])) == 0.u256
|
||||
|
||||
discard await testToken.mint(accounts[0], 100.u256)
|
||||
|
||||
check (await token.totalSupply()) == 100.u256
|
||||
check (await token.balanceOf(accounts[0])) == 100.u256
|
||||
check (await token.balanceOf(accounts[1])) == 0.u256
|
||||
|
||||
discard await token.transfer(accounts[1], 50.u256)
|
||||
|
||||
check (await token.balanceOf(accounts[0])) == 50.u256
|
||||
check (await token.balanceOf(accounts[1])) == 50.u256
|
||||
|
||||
test "approve tokens":
|
||||
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.approve(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 "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":
|
||||
let senderAccount = accounts[0]
|
||||
let receiverAccount = accounts[1]
|
||||
let receiverAccountSigner = provider.getSigner(receiverAccount)
|
||||
|
||||
check (await token.balanceOf(senderAccount)) == 0.u256
|
||||
check (await token.allowance(senderAccount, receiverAccount)) == 0.u256
|
||||
|
||||
discard await testToken.mint(senderAccount, 100.u256)
|
||||
|
||||
check (await token.totalSupply()) == 100.u256
|
||||
check (await token.balanceOf(senderAccount)) == 100.u256
|
||||
check (await token.balanceOf(receiverAccount)) == 0.u256
|
||||
|
||||
discard await token.approve(receiverAccount, 50.u256)
|
||||
|
||||
check (await token.allowance(senderAccount, receiverAccount)) == 50.u256
|
||||
check (await token.balanceOf(senderAccount)) == 100.u256
|
||||
check (await token.balanceOf(receiverAccount)) == 0.u256
|
||||
|
||||
discard await token.connect(receiverAccountSigner).transferFrom(senderAccount, receiverAccount, 50.u256)
|
||||
|
||||
check (await token.balanceOf(senderAccount)) == 50.u256
|
||||
check (await token.balanceOf(receiverAccount)) == 50.u256
|
||||
check (await token.allowance(senderAccount, receiverAccount)) == 0.u256
|
||||
|
||||
56
testmodule/testErrorDecoding.nim
Normal file
56
testmodule/testErrorDecoding.nim
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
||||
@ -1,8 +1,16 @@
|
||||
import pkg/asynctest
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/ethers
|
||||
import pkg/contractabi
|
||||
import ./examples
|
||||
|
||||
## Define outside the scope of the suite to allow for exporting
|
||||
## To use custom distinct types, these procs will generally need
|
||||
## to be defined in the application code anyway
|
||||
type
|
||||
DistinctAlias = distinct array[32, byte]
|
||||
|
||||
proc `==`*(x, y: DistinctAlias): bool {.borrow.}
|
||||
|
||||
suite "Events":
|
||||
|
||||
type
|
||||
@ -25,6 +33,9 @@ suite "Events":
|
||||
d {.indexed.}: seq[byte]
|
||||
e {.indexed.}: (Address, UInt256)
|
||||
f {.indexed.}: array[33, byte]
|
||||
IndexedWithDistinctType = object of Event
|
||||
a {.indexed.}: DistinctAlias
|
||||
b: DistinctAlias
|
||||
|
||||
proc example(_: type SimpleEvent): SimpleEvent =
|
||||
SimpleEvent(
|
||||
@ -47,6 +58,11 @@ suite "Events":
|
||||
e: array[32, byte].example
|
||||
)
|
||||
|
||||
proc example(_: type IndexedWithDistinctType): IndexedWithDistinctType =
|
||||
IndexedWithDistinctType(
|
||||
a: DistinctAlias(array[32, byte].example)
|
||||
)
|
||||
|
||||
func encode[T](_: type Topic, value: T): Topic =
|
||||
let encoded = AbiEncoder.encode(value)
|
||||
result[0..<Topic.len] = encoded[0..<Topic.len]
|
||||
@ -71,6 +87,14 @@ suite "Events":
|
||||
let data = AbiEncoder.encode( (event.a, event.c) )
|
||||
check IndexedEvent.decode(data, topics) == success event
|
||||
|
||||
test "decodes indexed fields with distinct types":
|
||||
let event = IndexedWithDistinctType.example
|
||||
var topics: seq[Topic]
|
||||
topics.add Topic.default
|
||||
topics.add Topic.encode(event.a)
|
||||
let data = AbiEncoder.encode( (event.b,) )
|
||||
check IndexedWithDistinctType.decode(data, topics) == success event
|
||||
|
||||
test "fails when data is incomplete":
|
||||
let event = SimpleEvent.example
|
||||
let invalid = AbiEncoder.encode( (event.a,) )
|
||||
|
||||
77
testmodule/testGasEstimation.nim
Normal file
77
testmodule/testGasEstimation.nim
Normal file
@ -0,0 +1,77 @@
|
||||
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
|
||||
@ -1,258 +0,0 @@
|
||||
import std/json
|
||||
import pkg/asynctest
|
||||
import pkg/chronos
|
||||
import pkg/ethers
|
||||
import pkg/stew/byteutils
|
||||
import ./examples
|
||||
import ./miner
|
||||
|
||||
suite "JsonRpcProvider":
|
||||
|
||||
var provider: JsonRpcProvider
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new("ws://localhost:8545")
|
||||
|
||||
test "can be instantiated with a default URL":
|
||||
discard JsonRpcProvider.new()
|
||||
|
||||
test "can be instantiated with an HTTP URL":
|
||||
discard JsonRpcProvider.new("http://localhost:8545")
|
||||
|
||||
test "can be instantiated with a websocket URL":
|
||||
discard JsonRpcProvider.new("ws://localhost:8545")
|
||||
|
||||
test "lists all accounts":
|
||||
let accounts = await provider.listAccounts()
|
||||
check accounts.len > 0
|
||||
|
||||
test "sends raw messages to the provider":
|
||||
let response = await provider.send("evm_mine")
|
||||
check response == %"0x0"
|
||||
|
||||
test "returns block number":
|
||||
let blocknumber1 = await provider.getBlockNumber()
|
||||
discard await provider.send("evm_mine")
|
||||
let blocknumber2 = await provider.getBlockNumber()
|
||||
check blocknumber2 > blocknumber1
|
||||
|
||||
test "returns block":
|
||||
let block1 = !await provider.getBlock(BlockTag.earliest)
|
||||
let block2 = !await provider.getBlock(BlockTag.latest)
|
||||
check block1.hash != block2.hash
|
||||
check !block1.number < !block2.number
|
||||
check block1.timestamp < block2.timestamp
|
||||
|
||||
test "subscribes to new blocks":
|
||||
let oldBlock = !await provider.getBlock(BlockTag.latest)
|
||||
var newBlock: Block
|
||||
let blockHandler = proc(blck: Block) {.async.} = newBlock = blck
|
||||
let subscription = await provider.subscribe(blockHandler)
|
||||
discard await provider.send("evm_mine")
|
||||
check !newBlock.number > !oldBlock.number
|
||||
check newBlock.timestamp > oldBlock.timestamp
|
||||
check newBlock.hash != oldBlock.hash
|
||||
await subscription.unsubscribe()
|
||||
|
||||
test "can send a transaction":
|
||||
let signer = provider.getSigner()
|
||||
let transaction = Transaction.example
|
||||
let populated = await signer.populateTransaction(transaction)
|
||||
|
||||
let txResp = await signer.sendTransaction(populated)
|
||||
check txResp.hash.len == 32
|
||||
check UInt256.fromHex("0x" & txResp.hash.toHex) > 0
|
||||
|
||||
test "can wait for a transaction to be confirmed":
|
||||
let signer = provider.getSigner()
|
||||
let transaction = Transaction.example
|
||||
let populated = await signer.populateTransaction(transaction)
|
||||
|
||||
# must not be awaited so we can get newHeads inside of .wait
|
||||
let futMined = provider.mineBlocks(5)
|
||||
|
||||
let receipt = await signer.sendTransaction(populated).confirm(3)
|
||||
let endBlock = await provider.getBlockNumber()
|
||||
|
||||
check receipt.blockNumber.isSome # was eventually mined
|
||||
|
||||
# >= 3 because more blocks may have been mined by the time the
|
||||
# check in `.wait` was done.
|
||||
# +1 for the block the tx was mined in
|
||||
check (endBlock - !receipt.blockNumber) + 1 >= 3
|
||||
|
||||
await futMined
|
||||
|
||||
test "waiting for block to be mined times out":
|
||||
|
||||
# must not be awaited so we can get newHeads inside of .wait
|
||||
let futMined = provider.mineBlocks(7)
|
||||
|
||||
let startBlock = await provider.getBlockNumber()
|
||||
let response = TransactionResponse(hash: TransactionHash.example,
|
||||
provider: provider)
|
||||
try:
|
||||
discard await response.confirm(wantedConfirms = 2,
|
||||
timeoutInBlocks = 5)
|
||||
|
||||
await futMined
|
||||
except EthersError as e:
|
||||
check e.msg == "Transaction was not mined in 5 blocks"
|
||||
|
||||
let endBlock = await provider.getBlockNumber()
|
||||
|
||||
# >= 5 because more blocks may have been mined by the time the
|
||||
# check in `.wait` was done.
|
||||
# +1 for including the start block
|
||||
check (endBlock - startBlock) + 1 >= 5 # +1 including start block
|
||||
if not futMined.completed and not futMined.finished: await futMined
|
||||
|
||||
test "Conversion: missing block number in Block isNone":
|
||||
|
||||
var blkJson = %*{
|
||||
"subscription": "0x20",
|
||||
"result":{
|
||||
"number": newJNull(),
|
||||
"hash":"0x2d7d68c8f48b4213d232a1f12cab8c9fac6195166bb70a5fb21397984b9fe1c7",
|
||||
"timestamp":"0x6285c293"
|
||||
}
|
||||
}
|
||||
|
||||
var blk = Block.fromJson(blkJson["result"])
|
||||
check blk.number.isNone
|
||||
|
||||
blkJson["result"]["number"] = newJString("")
|
||||
|
||||
blk = Block.fromJson(blkJson["result"])
|
||||
check blk.number.isSome
|
||||
check blk.number.get.isZero
|
||||
|
||||
test "Conversion: missing block number in TransactionReceipt isNone":
|
||||
|
||||
var txReceiptJson = %*{
|
||||
"sender": newJNull(),
|
||||
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
|
||||
"contractAddress": newJNull(),
|
||||
"transactionIndex": "0x0",
|
||||
"gasUsed": "0x10db1",
|
||||
"logsBloom": "0x00000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000840020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000020000000000000000000000000000000000001000000000000000000000000000000",
|
||||
"blockHash": "0x7b00154e06fe4f27a87208eba220efb4dbc52f7429549a39a17bba2e0d98b960",
|
||||
"transactionHash": "0xa64f07b370cbdcce381ec9bfb6c8004684341edfb6848fd418189969d4b9139c",
|
||||
"logs": [
|
||||
{
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000000064",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blockNumber": newJNull(),
|
||||
"cumulativeGasUsed": "0x10db1",
|
||||
"status": "0000000000000001"
|
||||
}
|
||||
|
||||
var txReceipt = TransactionReceipt.fromJson(txReceiptJson)
|
||||
check txReceipt.blockNumber.isNone
|
||||
|
||||
txReceiptJson["blockNumber"] = newJString("")
|
||||
txReceipt = TransactionReceipt.fromJson(txReceiptJson)
|
||||
check txReceipt.blockNumber.isSome
|
||||
check txReceipt.blockNumber.get.isZero
|
||||
|
||||
test "Conversion: missing block hash in TransactionReceipt isNone":
|
||||
|
||||
var txReceiptJson = %*{
|
||||
"sender": newJNull(),
|
||||
"to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
|
||||
"contractAddress": newJNull(),
|
||||
"transactionIndex": "0x0",
|
||||
"gasUsed": "0x10db1",
|
||||
"logsBloom": "0x00000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000840020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000020000000000000000000000000000000000001000000000000000000000000000000",
|
||||
"blockHash": newJNull(),
|
||||
"transactionHash": "0xa64f07b370cbdcce381ec9bfb6c8004684341edfb6848fd418189969d4b9139c",
|
||||
"logs": [
|
||||
{
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000000000064",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"
|
||||
]
|
||||
}
|
||||
],
|
||||
"blockNumber": newJNull(),
|
||||
"cumulativeGasUsed": "0x10db1",
|
||||
"status": "0000000000000001"
|
||||
}
|
||||
|
||||
var txReceipt = TransactionReceipt.fromJson(txReceiptJson)
|
||||
check txReceipt.blockHash.isNone
|
||||
|
||||
test "confirmations calculated correctly":
|
||||
# when receipt block number is higher than current block number,
|
||||
# should return 0
|
||||
check confirmations(2.u256, 1.u256) == 0.u256
|
||||
|
||||
# Same receipt and current block counts as one confirmation
|
||||
check confirmations(1.u256, 1.u256) == 1.u256
|
||||
|
||||
check confirmations(1.u256, 2.u256) == 2.u256
|
||||
|
||||
test "checks if transation has been mined correctly":
|
||||
|
||||
var receipt: TransactionReceipt
|
||||
var currentBlock = 1.u256
|
||||
var wantedConfirms = 1
|
||||
let blockHash = hexToByteArray[32](
|
||||
"0x7b00154e06fe4f27a87208eba220efb4dbc52f7429549a39a17bba2e0d98b960"
|
||||
).some
|
||||
|
||||
# missing blockHash
|
||||
receipt = TransactionReceipt(
|
||||
blockNumber: 1.u256.some
|
||||
)
|
||||
check not receipt.hasBeenMined(currentBlock, wantedConfirms)
|
||||
|
||||
# missing block number
|
||||
receipt = TransactionReceipt(
|
||||
blockHash: blockHash
|
||||
)
|
||||
check not receipt.hasBeenMined(currentBlock, wantedConfirms)
|
||||
|
||||
# block number is 0
|
||||
receipt = TransactionReceipt(
|
||||
blockNumber: 0.u256.some
|
||||
)
|
||||
check not receipt.hasBeenMined(currentBlock, wantedConfirms)
|
||||
|
||||
# not enough confirms
|
||||
receipt = TransactionReceipt(
|
||||
blockNumber: 1.u256.some
|
||||
)
|
||||
check not receipt.hasBeenMined(currentBlock, wantedConfirms)
|
||||
|
||||
# success
|
||||
receipt = TransactionReceipt(
|
||||
blockNumber: 1.u256.some,
|
||||
blockHash: blockHash
|
||||
)
|
||||
currentBlock = int.high.u256
|
||||
wantedConfirms = int.high
|
||||
check receipt.hasBeenMined(currentBlock, wantedConfirms)
|
||||
|
||||
test "raises JsonRpcProviderError when something goes wrong":
|
||||
let provider = JsonRpcProvider.new("http://invalid.")
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.listAccounts()
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.send("evm_mine")
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.getBlockNumber()
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.getBlock(BlockTag.latest)
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.subscribe(proc(_: Block) {.async.} = discard)
|
||||
expect JsonRpcProviderError:
|
||||
discard await provider.getSigner().sendTransaction(Transaction.example)
|
||||
3
testmodule/testProviders.nim
Normal file
3
testmodule/testProviders.nim
Normal file
@ -0,0 +1,3 @@
|
||||
import ./providers/testJsonRpc
|
||||
|
||||
{.warning[UnusedImport]:off.}
|
||||
@ -1,5 +1,7 @@
|
||||
import pkg/asynctest
|
||||
import std/os
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/ethers
|
||||
import pkg/serde
|
||||
import ./hardhat
|
||||
|
||||
type
|
||||
@ -12,15 +14,17 @@ suite "Contract return values":
|
||||
var contract: TestReturns
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new("ws://localhost:8545")
|
||||
provider = JsonRpcProvider.new("http://" & providerUrl)
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
let deployment = readDeployment()
|
||||
contract = TestReturns.new(!deployment.address(TestReturns), provider)
|
||||
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
test "handles static size structs":
|
||||
proc getStatic(contract: TestReturns): Static {.contract, pure.}
|
||||
@ -51,3 +55,11 @@ suite "Contract return values":
|
||||
let values = await contract.getDynamics()
|
||||
check values.a == ("1", 2.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)
|
||||
|
||||
118
testmodule/testTesting.nim
Normal file
118
testmodule/testTesting.nim
Normal file
@ -0,0 +1,118 @@
|
||||
import std/os
|
||||
import std/strformat
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/chronos
|
||||
import pkg/ethers
|
||||
import pkg/ethers/testing
|
||||
import pkg/serde
|
||||
import ./helpers
|
||||
|
||||
suite "Testing helpers":
|
||||
|
||||
let revertReason = "revert reason"
|
||||
let rpcResponse = "Error: VM Exception while processing transaction: " &
|
||||
fmt"reverted with reason string '{revertReason}'"
|
||||
|
||||
test "checks that call reverts":
|
||||
proc call() {.async.} =
|
||||
raise newException(EstimateGasError, $rpcResponse)
|
||||
|
||||
check await call().reverts()
|
||||
|
||||
test "checks reason for revert":
|
||||
proc call() {.async.} =
|
||||
raise newException(EstimateGasError, $rpcResponse)
|
||||
|
||||
check await call().reverts(revertReason)
|
||||
|
||||
test "correctly indicates there was no revert":
|
||||
proc call() {.async.} = discard
|
||||
|
||||
check not await call().reverts()
|
||||
|
||||
test "reverts only checks ProviderErrors, EstimateGasErrors":
|
||||
proc callProviderError() {.async.} =
|
||||
raise newException(ProviderError, "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()
|
||||
check await callSignerError().reverts()
|
||||
check await callEstimateGasError().reverts()
|
||||
expect EthersError:
|
||||
check await callEthersError().reverts()
|
||||
|
||||
test "reverts with reason only checks ProviderErrors, EstimateGasErrors":
|
||||
proc callProviderError() {.async.} =
|
||||
raise newException(ProviderError, revertReason)
|
||||
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)
|
||||
check await callSignerError().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":
|
||||
proc call() {.async.} = discard
|
||||
|
||||
check not await call().reverts(revertReason)
|
||||
|
||||
test "reverts is false when the revert reason doesn't match":
|
||||
proc call() {.async.} =
|
||||
raise newException(EstimateGasError, "other reason")
|
||||
|
||||
check not await call().reverts(revertReason)
|
||||
|
||||
test "revert handles non-standard revert prefix":
|
||||
let nonStdMsg = fmt"Provider VM Exception: reverted with {revertReason}"
|
||||
proc call() {.async.} =
|
||||
raise newException(EstimateGasError, nonStdMsg)
|
||||
|
||||
check await call().reverts(nonStdMsg)
|
||||
|
||||
test "works with functions that return a value":
|
||||
proc call(): Future[int] {.async.} = return 42
|
||||
check not await call().reverts()
|
||||
check not await call().reverts(revertReason)
|
||||
|
||||
|
||||
suite "Testing helpers - contracts":
|
||||
|
||||
var helpersContract: TestHelpers
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
var accounts: seq[Address]
|
||||
let revertReason = "revert reason"
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new("ws://" & providerUrl)
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
accounts = await provider.listAccounts()
|
||||
helpersContract = TestHelpers.new(provider.getSigner())
|
||||
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
test "revert reason can be retrieved when transaction fails":
|
||||
let txResp = helpersContract.doRevert(
|
||||
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)
|
||||
@ -1,4 +1,6 @@
|
||||
import pkg/asynctest
|
||||
import std/os
|
||||
import pkg/asynctest/chronos/unittest
|
||||
import pkg/serde
|
||||
import pkg/stew/byteutils
|
||||
import ../ethers
|
||||
|
||||
@ -9,33 +11,36 @@ type Erc20* = ref object of Contract
|
||||
proc transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.contract.}
|
||||
|
||||
suite "Wallet":
|
||||
|
||||
#TODO add more tests. I am not sure if I am testing everything currently
|
||||
#TODO take close look at current signing tests. I am not 100% sure they are correct and work
|
||||
#TODO add setup/teardown if required. Currently doing all nonces manually
|
||||
|
||||
var provider: JsonRpcProvider
|
||||
var snapshot: JsonNode
|
||||
let providerUrl = getEnv("ETHERS_TEST_PROVIDER", "localhost:8545")
|
||||
|
||||
setup:
|
||||
provider = JsonRpcProvider.new()
|
||||
provider = JsonRpcProvider.new("http://" & providerUrl)
|
||||
snapshot = await provider.send("evm_snapshot")
|
||||
|
||||
|
||||
teardown:
|
||||
discard await provider.send("evm_revert", @[snapshot])
|
||||
await provider.close()
|
||||
|
||||
test "Can create Wallet with private key":
|
||||
discard Wallet.new(pk1)
|
||||
check isSuccess Wallet.new(pk1)
|
||||
discard Wallet.new(PrivateKey.fromHex(pk1).get)
|
||||
|
||||
test "Private key can start with 0x":
|
||||
discard Wallet.new("0x" & pk1)
|
||||
check isSuccess Wallet.new("0x" & pk1)
|
||||
|
||||
test "Can create Wallet with provider":
|
||||
let provider = JsonRpcProvider.new()
|
||||
discard Wallet.new(pk1, provider)
|
||||
check isSuccess Wallet.new(pk1, provider)
|
||||
discard Wallet.new(PrivateKey.fromHex(pk1).get, provider)
|
||||
|
||||
test "Cannot create wallet with invalid key string":
|
||||
check isFailure Wallet.new("0xInvalidKey")
|
||||
check isFailure Wallet.new("0xInvalidKey", JsonRpcProvider.new())
|
||||
|
||||
test "Can connect Wallet to provider":
|
||||
let wallet = Wallet.new(pk1)
|
||||
let wallet = !Wallet.new(pk1)
|
||||
wallet.connect(provider)
|
||||
|
||||
test "Can create Random Wallet":
|
||||
@ -50,50 +55,40 @@ suite "Wallet":
|
||||
check $wallet1.privateKey != $wallet2.privateKey
|
||||
|
||||
test "Creates the correct public key and Address from private key":
|
||||
let wallet = Wallet.new(pk1)
|
||||
let wallet = !Wallet.new(pk1)
|
||||
check $wallet.publicKey == "5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca"
|
||||
check $wallet.address == "0x328809bc894f92807417d2dad6b7c998c1afdac6"
|
||||
|
||||
test "Can sign manually created transaction":
|
||||
let wallet = Wallet.new(pk1)
|
||||
let tx = Transaction(
|
||||
to: wallet.address,
|
||||
nonce: some 0.u256,
|
||||
chainId: some 31337.u256,
|
||||
gasPrice: some 1_000_000_000.u256,
|
||||
gasLimit: some 21_000.u256,
|
||||
# Example from EIP-155
|
||||
let wallet = !Wallet.new("0x4646464646464646464646464646464646464646464646464646464646464646")
|
||||
let transaction = Transaction(
|
||||
to: !Address.init("0x3535353535353535353535353535353535353535"),
|
||||
nonce: some 9.u256,
|
||||
chainId: some 1.u256,
|
||||
gasPrice: some 20 * 10.u256.pow(9),
|
||||
gasLimit: some 21000.u256,
|
||||
value: 10.u256.pow(18),
|
||||
data: @[]
|
||||
)
|
||||
let signedTx = await wallet.signTransaction(tx)
|
||||
check signedTx.toHex == "f86380843b9aca0082520894328809bc894f92807417d2dad6b7c998c1afdac680801ba04ae9b24cba72103bb30a1e91c016796fc2bf2d46d2b75ca80211fae0337c3c03a05e3c81ce9944a07f18b65142a1847c5b72f993c8e7c28d5d4360ff36a2fed049"
|
||||
|
||||
test "Can sign manually created contract call":
|
||||
let wallet = Wallet.new(pk1)
|
||||
let tx = Transaction(
|
||||
to: wallet.address,
|
||||
data: @[24.byte, 22, 13, 221], # Arbitrary Calldata for totalsupply()
|
||||
nonce: some 0.u256,
|
||||
chainId: some 31337.u256,
|
||||
gasPrice: some 1_000_000_000.u256,
|
||||
gasLimit: some 21_000.u256,
|
||||
)
|
||||
let signedTx = await wallet.signTransaction(tx)
|
||||
check signedTx.toHex == "f86780843b9aca0082520894328809bc894f92807417d2dad6b7c998c1afdac6808418160ddd1ca029261fc74ffbbb5ce3d0a3b6eac9726f05d8a849e0f1535722e057bdd83b9659a044f857852bd8b7bb8c0c0a5a61c2c56fce42edacab73f42301b509edb7600ff1"
|
||||
let signed = await wallet.signTransaction(transaction)
|
||||
check signed.toHex == "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
|
||||
|
||||
test "Can sign manually created tx with EIP1559":
|
||||
let wallet = Wallet.new(pk1)
|
||||
let wallet = !Wallet.new(pk1)
|
||||
let tx = Transaction(
|
||||
to: wallet.address,
|
||||
nonce: some 0.u256,
|
||||
chainId: some 31337.u256,
|
||||
maxFee: some 2_000_000_000.u256,
|
||||
maxPriorityFee: some 1_000_000_000.u256,
|
||||
maxFeePerGas: some 2_000_000_000.u256,
|
||||
maxPriorityFeePerGas: some 1_000_000_000.u256,
|
||||
gasLimit: some 21_000.u256
|
||||
)
|
||||
let signedTx = await wallet.signTransaction(tx)
|
||||
check signedTx.toHex == "02f86c827a6980843b9aca00847735940082520894328809bc894f92807417d2dad6b7c998c1afdac68080c001a0162929fc5b4cb286ed4cd630d172d1dd747dad4ffbeb413b037f21168f4fe366a062b931c1fc55028ae1fdf5342564300cae251791d785a0efd31c088405a651e7"
|
||||
|
||||
test "Can send rawTransaction":
|
||||
let wallet = Wallet.new(pk_with_funds)
|
||||
let wallet = !Wallet.new(pk_with_funds)
|
||||
let tx = Transaction(
|
||||
to: wallet.address,
|
||||
nonce: some 0.u256,
|
||||
@ -103,25 +98,25 @@ suite "Wallet":
|
||||
)
|
||||
let signedTx = await wallet.signTransaction(tx)
|
||||
let txHash = await provider.sendTransaction(signedTx)
|
||||
check txHash.hash == TransactionHash([167.byte, 105, 79, 222, 144, 123, 214, 138, 4, 199, 124, 181, 35, 236, 79, 93, 84, 4, 85, 172, 40, 50, 189, 187, 219, 6, 172, 98, 243, 196, 93, 64])
|
||||
check txHash.hash != TransactionHash.default
|
||||
|
||||
test "Can call state-changing function automatically":
|
||||
#TODO add actual token contract, not random address. Should work regardless
|
||||
let wallet = Wallet.new(pk_with_funds, provider)
|
||||
let wallet = !Wallet.new(pk_with_funds, provider)
|
||||
let overrides = TransactionOverrides(
|
||||
nonce: some 0.u256,
|
||||
gasPrice: some 1_000_000_000.u256,
|
||||
gasLimit: some 22_000.u256)
|
||||
let testToken = Erc20.new(wallet.address, wallet)
|
||||
await testToken.transfer(wallet.address, 24.u256, overrides)
|
||||
|
||||
|
||||
test "Can call state-changing function automatically EIP1559":
|
||||
#TODO add actual token contract, not random address. Should work regardless
|
||||
let wallet = Wallet.new(pk_with_funds, provider)
|
||||
let wallet = !Wallet.new(pk_with_funds, provider)
|
||||
let overrides = TransactionOverrides(
|
||||
nonce: some 0.u256,
|
||||
maxFee: some 1_000_000_000.u256,
|
||||
maxPriorityFee: some 1_000_000_000.u256,
|
||||
maxFeePerGas: some 1_000_000_000.u256,
|
||||
maxPriorityFeePerGas: some 1_000_000_000.u256,
|
||||
gasLimit: some 22_000.u256)
|
||||
let testToken = Erc20.new(wallet.address, wallet)
|
||||
await testToken.transfer(wallet.address, 24.u256, overrides)
|
||||
|
||||
58
testnode/contracts/TestCustomErrors.sol
Normal file
58
testnode/contracts/TestCustomErrors.sol
Normal file
@ -0,0 +1,58 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
22
testnode/contracts/TestGasEstimation.sol
Normal file
22
testnode/contracts/TestGasEstimation.sol
Normal file
@ -0,0 +1,22 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
testnode/contracts/TestHelpers.sol
Normal file
10
testnode/contracts/TestHelpers.sol
Normal file
@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract TestHelpers {
|
||||
|
||||
function doRevert(string calldata reason) public pure {
|
||||
// Revert every tx with given reason
|
||||
require(false, reason);
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,9 @@ contract TestReturns {
|
||||
uint256 b;
|
||||
}
|
||||
|
||||
StaticStruct public staticVariable = StaticStruct(1, 2);
|
||||
DynamicStruct public dynamicVariable = DynamicStruct("3", 4);
|
||||
|
||||
function getStatic() external pure returns (StaticStruct memory) {
|
||||
return StaticStruct(1, 2);
|
||||
}
|
||||
|
||||
@ -6,7 +6,19 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
contract TestToken is ERC20 {
|
||||
constructor() ERC20("TestToken", "TST") {}
|
||||
|
||||
function decimals() public view virtual override returns (uint8) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
function mint(address holder, uint amount) public {
|
||||
_mint(holder, amount);
|
||||
}
|
||||
|
||||
function burn(address holder, uint amount) public {
|
||||
_burn(holder, amount);
|
||||
}
|
||||
|
||||
function myBalance() public view returns (uint256) {
|
||||
return balanceOf(msg.sender);
|
||||
}
|
||||
}
|
||||
|
||||
6
testnode/deploy/testcustomerrors.js
Normal file
6
testnode/deploy/testcustomerrors.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = async ({ deployments, getNamedAccounts }) => {
|
||||
const { deployer } = await getNamedAccounts();
|
||||
await deployments.deploy("TestCustomErrors", { from: deployer });
|
||||
};
|
||||
|
||||
module.exports.tags = ["TestCustomErrors"];
|
||||
6
testnode/deploy/testgasestimation.js
Normal file
6
testnode/deploy/testgasestimation.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = async ({ deployments, getNamedAccounts }) => {
|
||||
const { deployer } = await getNamedAccounts();
|
||||
await deployments.deploy("TestGasEstimation", { from: deployer });
|
||||
};
|
||||
|
||||
module.exports.tags = ["TestGasEstimation"];
|
||||
6
testnode/deploy/testhelpers.js
Normal file
6
testnode/deploy/testhelpers.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = async ({deployments, getNamedAccounts}) => {
|
||||
const { deployer } = await getNamedAccounts()
|
||||
await deployments.deploy('TestHelpers', { from: deployer })
|
||||
}
|
||||
|
||||
module.exports.tags = ["TestHelpers"];
|
||||
@ -2,7 +2,7 @@ require("hardhat-deploy")
|
||||
require("hardhat-deploy-ethers")
|
||||
|
||||
module.exports = {
|
||||
solidity: "0.8.11",
|
||||
solidity: "0.8.24",
|
||||
namedAccounts: {
|
||||
deployer: { default: 0 }
|
||||
}
|
||||
|
||||
7259
testnode/package-lock.json
generated
7259
testnode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,10 @@
|
||||
"name": "hardhat-project",
|
||||
"devDependencies": {
|
||||
"@openzeppelin/contracts": "^4.4.2",
|
||||
"ethers": "^5.5.3",
|
||||
"hardhat": "^2.8.3",
|
||||
"hardhat-deploy": "^0.9.24",
|
||||
"hardhat-deploy-ethers": "^0.3.0-beta.13"
|
||||
"ethers": "^6.11.1",
|
||||
"hardhat": "^2.22.1",
|
||||
"hardhat-deploy": "^0.11.34",
|
||||
"hardhat-deploy-ethers": "^0.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "hardhat node --export 'deployment.json'"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user