Compare commits

...

164 Commits
0.2.2 ... main

Author SHA1 Message Date
Jacek Sieka
965b8cd752
chore: bump eth (#124)
* recycle common transaction signature helpers
* bump versions
2025-12-13 07:32:36 +01:00
Arnaud
30871c7b1d
chore: add EIP-1559 implementation for gas price (#113)
* Add EIP-1559 implementation for gas price

* Improve logs

* Improve comment

* Rename maxFee and maxPriorityFee to use official EIP-1559 names

* Delete gas price when using EIP-1559

* Allow override maxFeePerGas

* Code style

* Remove useless specific EIP1559 test because Hardhart support it so all transactions are using EIP1559 by default

* Restore test to check legacy transaction

* Update after rebase

* Call eth_maxPriorityFeePerGas and returns a manual defined maxPriorityFeePerGas as a fallback

* Catch JsonRpcProviderError instead of ProviderError

* Improve readability

* Set none value for maxFeePerGas in case of non EIP-1559 transaction

* Assign none to maxPriorityFeePerGas for non EIP-1559 transaction to avoid potential side effect in wallet signing

* Remove upper bound version for stew and update contractabi
2025-05-28 16:14:01 +02:00
Mark Spanbroek
bbced46733
version 2.0.0
Changes:
- supports Nim 2.0.x and 2.2.x
- no longer supports Nim versions 1.6.x
- better handling of async exceptions
- block number can be retrieved from a block tag
- workaround for hardhat websocket subscription timeouts
- supports estimating gas for contract calls
2025-04-15 10:55:57 +02:00
Mark Spanbroek
c85192ae34 Make comments less confusing
(I hope)

Co-Authored-By: Eric <5089238+emizzle@users.noreply.github.com>
2025-04-15 10:45:52 +02:00
Mark Spanbroek
f9d115ae75 Use pending block for gas estimations 2025-04-15 10:45:52 +02:00
Mark Spanbroek
a29e86bfc8 Handle custom errors when estimating gas 2025-04-15 10:45:52 +02:00
Mark Spanbroek
4441050c3d Move contract error handling into its own modules 2025-04-15 10:45:52 +02:00
Mark Spanbroek
e37f454761 Allow for gas estimation of contract calls 2025-04-15 10:45:52 +02:00
Mark Spanbroek
def12bfdc1 Split contract module into several parts 2025-04-15 10:45:52 +02:00
Mark Spanbroek
51aa7bc1b3 Fix asyntest update merge error 2025-04-14 16:03:45 +02:00
Mark Spanbroek
518afa3e4c update asynctest dependency
fixes segfault in Nim 2.2.2
2025-04-14 15:44:04 +02:00
Arnaud
af3d7379c8
chore: add ws resubscription for hardhat workaround (#112)
* Move logFilters to JsonRpcSubscriptions

* Add resubscribe flag

* Add documentation for the resubscribe symbol

* Rename the symbol for better clarity

* Provide better message

* Add nimbledeps to git ignore

* Update wording

* Update wording

* Remove the ws_resubscribe flag from the config

* Handle the concurrency issues when updating the logFilters and add tests

* Update log filters comment

* Add lock when subscribing to blocks

* Remove useless private access

* Fix wording

* Fix try except format

* Restore privateAccess because logEvents moved to JsonRpcSubscriptions

* Use seconds instead of milliseconds

* Remove extra dot in test label

* Restore new lines

* Pass the resubscribe internal in new function and remove unneeded try except

* Remove ws_resubscribe default value making testing easier

* Remove unneeded condition

* Add new line

* Fix nim syntax

* Update symbol description

* Log warning when the resubscription interval is more than 300 seconds

* Catch errors in close method

* Redefine raises for async pragma in close methods

* Provide better error message
2025-04-10 10:48:41 +02:00
Arnaud
7081e6922f
Re-activate styleCheck 2025-03-18 08:42:52 +01:00
Arnaud
5d07b5dbcf
Define raises for async pragma 2025-03-18 08:12:24 +01:00
Eric
b505ef1ab8
Raise SignerError instead of propagating AsyncLockError (#109) 2025-03-13 14:45:31 +11:00
Eric
d2b11a8657
fix(async): propagate async cancellation (#105)
* fix(async): propagate CancelledErrors

* remove CatchableError from contract macro async raises list

* remove mistakenly added ContractError
2025-02-17 20:31:24 +11:00
Arnaud
26342d3e27
Update to nim 2 x (#103)
* Update dependencies for Nim 2.x

* Use refc as memory management and disable styleCheck because of testutils

* Fix ambiguous import

* Change Address init because eth introduced Byte20 type for Address type

* use uint64 instead of init64

* Rename properties after a change in eth to be closer to the spec

* Use Opt type instead of Option

* Add 2.0.12 version to CI

* Increment the version

* Update the Nim version in CI

* Update to Nim 2.0.14

* Use Nim 2.x commit hash for contractabi

* Remove stable on CI because we don't want to test with Nim 2.2.x

* Update Nim minimum version to 2.0.14

* fix version deps

* remove fq typename

* Add debug flag

* Define maximumtaggedversions

* Update readme

---------

Co-authored-by: Eric <5089238+emizzle@users.noreply.github.com>
2025-02-14 14:18:19 +11:00
Marcin Czenko
0f98528758 adds tests for BlockTag 2024-12-10 17:41:42 +01:00
Marcin Czenko
c7c57113ce adds number getter for BlockTags with number 2024-12-10 17:41:42 +01:00
Adam Uhlíř
037bef0256
chore: fix async raises warnings (#100) 2024-12-09 16:22:25 +01:00
Marcin Czenko
04d3548553
version 1.0.0
This is a braking change. Subscription callbacks wrap the arguments in the Result type.
Corrects the preceding commit marked with wrong version number (0.10.2).
2024-12-02 17:15:42 +01:00
Marcin Czenko
2808a05488
version 0.10.2 2024-11-28 16:15:40 +01:00
Marcin Czenko
5c93971f97 fix the test after rebasing 2024-11-28 16:08:51 +01:00
Marcin Czenko
c0cc437aa2 applies review comments 2024-11-28 16:08:51 +01:00
Marcin Czenko
4642545309 makes sure that a key on subscriptionMapping exists before trying to access it 2024-11-28 16:08:51 +01:00
Adam Uhlíř
d88e4614b1
feat: subscriptions get passed result questionable (#91)
Co-authored-by: Eric <5089238+emizzle@users.noreply.github.com>
2024-11-28 14:48:10 +01:00
Eric
04c00e2d91
Updates non-versioned deps to their versioned counterparts (#97)
Also bumps ethers patch version
2024-11-28 13:26:58 +11:00
Mark Spanbroek
1ae2cd4a35
version 0.10.0
This is a breaking change. Calling .confirm(0)
is no longer supported; you need at least 1
confirmation.
2024-11-13 10:14:09 +01:00
Mark Spanbroek
e9d862ceca do not crash when we cannot get block number
Co-Authored-By: Eric <5089238+emizzle@users.noreply.github.com>
2024-11-13 10:09:40 +01:00
Mark Spanbroek
35aebdb46f cleanup 2024-11-13 10:09:40 +01:00
Mark Spanbroek
f15d55f513 do not crash polling when just unsubscribed 2024-11-13 10:09:40 +01:00
Mark Spanbroek
c6a59b5187 resubscribe when error in polling 2024-11-13 10:09:40 +01:00
Mark Spanbroek
5a9895b792 disallow .confirm(0)
reason: it didn't wait for any blocks to be mined,
not even the block that includes the transaction.
2024-11-13 10:09:40 +01:00
Mark Spanbroek
c9275b1f6c cleanup 2024-11-13 10:09:40 +01:00
Mark Spanbroek
40dee9b525 disable chronicles logging in tests 2024-11-13 10:09:40 +01:00
Eric
0ce6abf0fe
fix(nonce): indentation mistake after last merge (#92)
* fix an indentation mistake after last merge

* add assertion to ensure nonce is not populated

* assert populated nonce is populated, not transaction
2024-11-01 16:49:06 +01:00
Adam Uhlíř
80b2ead97c
fix: block filters can be also recreated (#85)
* fix: block filters can be also recreated

* refactor: rename filter to logFilter
2024-10-30 17:26:27 +01:00
Eric
d60cedbb98
chore: bump ethers to forked deps (#89) 2024-10-30 17:12:24 +01:00
Slava
4607817057
ci: add matrix status job (#83) 2024-10-28 15:27:50 +02:00
Eric
6523e70eaf
fix: items(JsonNode) symbol not found (#87)
* chore: export subscriptions

This has a knock-on effect of nim-serde not being imported into subscriptions when JsonRpcProvider.new is called from a consumer that does not export nim-serde.

* import/export serde

* Replace all instances of std/json with pkg/serde
2024-10-28 14:06:20 +11:00
Eric
765379a662
fix: nonce too high (#81)
* fix nonce issues by locking populate and send transaction

Concurrent asynchronous population of transactions cause issues with nonces not being in sync with the transaction count for an account on chain. This was being mitigated by tracking a "last seen" nonce and locking inside of `populateTransaction` so that the nonce could be populated in a concurrent fashion. However, if there was an async cancellation before the transaction was sent, then the nonce would become out of sync. One solution was to decrease the nonce if a cancellation occurred. The other solution, in this commit, is simply to lock the populate and sendTransaction calls together, so that there will not be concurrent nonce discrepancies. This removes the need for "lastSeenNonce" and is overall more simple.

* remove lastSeenNonce

Internal nonce tracking is no longer needed since populate/sendTransaction is now locked. Even if cancelled midway, the nonce will get a refreshed value from the number of transactions from chain.

* chronos v4 exception tracking

* Add tests
2024-10-25 15:08:00 +11:00
Eric
b68bea9909
fix: modify unsubscribe cleanup routine and tests (#84)
* fix: modify unsubscribe cleanup routine

Ignore exceptions (other than CancelledError) if uninstallation of the filter fails. If it's the last step in the subscription 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.

This includes a number of fixes:
- `CancelledError` is now caught inside of `getChanges`. This was causing conditions during `subscriptions.close`, where the `CancelledError` would get consumed by the `except CatchableError`, if there was an ongoing `poll` happening at the time of close.
- After creating a new filter inside of `getChanges`, the new filter is polled for changes before returning.
- `getChanges` also does not swallow `CatchableError` by returning an empty array, and instead re-raises the error if it is not `filter not found`.
- The tests were simplified by accessing the private fields of `PollingSubscriptions`. That way, there wasn't a race condition for the `newFilterId` counter inside of the mock.
- The `MockRpcHttpServer` was simplified by keeping track of the active filters only, and invalidation simply removes the filter. The tests then only needed to rely on the fact that the filter id changed in the mapping.
- Because of the above changes, we no longer needed to sleep inside of the tests, so the sleeps were removed, and the polling interval could be changed to 1ms, which not only makes the tests faster, but would further highlight any race conditions if present.

* docs: rpc custom port documentation

---------

Co-authored-by: Adam Uhlíř <adam@uhlir.dev>
2024-10-25 14:58:45 +11:00
Adam Uhlíř
507ac6a4cc
fix(subscriptions): filter not found recreates polling filter (#78) 2024-10-22 15:57:25 +02:00
Adam Uhlíř
53e596e75a
fix: pinning nim-eth dependency (#77)
Co-authored-by: Eric <5089238+emizzle@users.noreply.github.com>
2024-10-22 10:39:11 +02:00
Mark Spanbroek
e15974eb1f version 0.9.0
This is a breaking change. Contract functions can no
longer be defined to return ?TransactionResponse,
use Confirmable as the return type instead.
2024-05-21 13:27:19 +02:00
Mark Spanbroek
17c6e9a8c5 test for multiple error types on a function
Co-Authored-By: Eric Mastro <eric.mastro@gmail.com>
2024-05-21 13:19:24 +02:00
Mark Spanbroek
2da59a86c3 improve decoding failure messages
Co-Authored-By: Eric Mastro <eric.mastro@gmail.com>
2024-05-21 13:19:24 +02:00
Mark Spanbroek
131316de08 move error conversion from TransactionResponse to Confirmable
Ensures that provider no longer needs to know about error
conversion, it now localized in contract.nim.

Co-Authored-By: Eric Mastro <eric.mastro@gmail.com>
2024-05-21 13:19:24 +02:00
Mark Spanbroek
ab10354910 wrap transaction response into Confirmable
This is a breaking change for the API of nim-ethers.
2024-05-21 13:19:24 +02:00
Mark Spanbroek
cdb230d30f handle custom errors in confirm() calls 2024-05-21 13:19:24 +02:00
Mark Spanbroek
067e0f2eb7 readme: add description of custom errors 2024-05-21 13:19:24 +02:00
Mark Spanbroek
a6f136afdd test custom arguments when sending transaction 2024-05-21 13:19:24 +02:00
Mark Spanbroek
9cb033e865 keep error data intact when replaying transaction 2024-05-21 13:19:24 +02:00
Mark Spanbroek
241ce6e8f3 sendTransaction raises ProviderError instead of SignerError
Allows it to contain error data.
It is not the signing that fails, so it makes sense to
use an error type that indicates that the provider failed.
2024-05-21 13:19:24 +02:00
Mark Spanbroek
1ce9824738 EstimateGasError is a ProviderError instead of a SignerError
Which allows for it to contain error data
2024-05-21 13:19:24 +02:00
Mark Spanbroek
80fcb246f6 do not export encoding and decoding functions 2024-05-21 13:19:24 +02:00
Mark Spanbroek
ce63c375f7 error messages for custom errors 2024-05-21 13:19:24 +02:00
Mark Spanbroek
955ac2d58f abi decoding of custom error fails on trailing bytes 2024-05-21 13:19:24 +02:00
Mark Spanbroek
6d6777e8c3 test custom errors with struct arguments 2024-05-21 13:19:24 +02:00
Mark Spanbroek
9c76803302 support custom errors with arguments 2024-05-21 13:19:24 +02:00
Mark Spanbroek
74f15fca9c support custom errors in contract calls
Currently only errors without arguments
2024-05-21 13:19:24 +02:00
Mark Spanbroek
6b57e56a39 abi decoding of simple custom errors
Only supports errors without arguments for now
2024-05-21 13:19:24 +02:00
Mark Spanbroek
875900b493 jsonrpc: extract error data from JSON RPC error
Inspired by 'spelunk' from ethers.js:
f97b92bbb1/packages/providers/src.ts/json-rpc-provider.ts (L25)
2024-05-21 13:19:24 +02:00
Mark Spanbroek
52d7d3dbed jsonrpc: move error handling to separate module 2024-05-21 13:19:24 +02:00
Eric
027b5c37ad
fix: deserialize BlockTag from empty string (#73)
Allows BlockTag to be deserialized from an empty string
2024-05-21 13:10:06 +10:00
Eric
958d7b45d1
Remove overloaded UInt256.fromJson (#74)
* Remove overloaded UInt256.fromJson

Rely instead on UInt256.fromJson from nim-serde, which deserializes an empty string for ?UInt256 into UInt256.none. Previously, empty strings were deserialized into 0.u256. BlockNumber was using this deserialization, and it appears that deserializing a missing block number from a TransactionReceipt into 0 might actually cause some issues when waiting on block confirmations.

* bump version of serde

* Remove "v" from version in `.nimble`

* Fix nimble serde version again ¯\_(ツ)_/¯
2024-05-21 13:09:42 +10:00
Mark Spanbroek
6393546ad6 fix flaky test 2024-05-13 11:52:14 +02:00
markspanbroek
4ad5b6065e
Update dependencies (#70)
Updates hardhat and solidity

Uses personal_sign instead of eth_sign, because ethers.js also uses personal_sign, and eth_sign is now broken in hardhat (arguments are reversed).

Co-authored-by: Adam Uhlíř <adam@uhlir.dev>
2024-05-13 11:51:43 +02:00
Mark Spanbroek
bcb539148a Remove unnecessary test requirement
`questionable` is already in the main nimble file
2024-03-12 09:27:18 +01:00
Mark Spanbroek
14a7485a88 Handle getter functions for public state variables
Getter functions that are generated by the solidity
compiler do not wrap their return value in a tuple
like other functions do.
2024-03-12 09:27:18 +01:00
Mark Spanbroek
b78463a299 Refactor: handle multiple returns earlier 2024-03-12 09:27:18 +01:00
Ben Bierens
942fe034fc
Fixes isSyncing issue where object is evaluated as false (#68) 2024-03-08 12:55:36 +01:00
Mark Spanbroek
4a57089ed2 Fix warnings 2024-03-03 06:33:52 +01:00
Mark Spanbroek
877ff82ef6 Fix: overrides when simulating transaction 2024-03-03 06:33:52 +01:00
Mark Spanbroek
af5a0f5fb4 Fix: ensure that gas estimations are done using the "pending" block 2024-02-27 09:40:20 +01:00
Mark Spanbroek
d46f5a10d3 version 0.8.0
Replaces version 0.7.2, because it includes
breaking changes
2024-02-27 09:13:36 +01:00
benbierens
7911ac6c57
version 0.7.2 2024-02-26 14:03:48 +01:00
Ben Bierens
e8196b3c82
Adds isSyncing to provider (#62) 2024-02-20 16:25:23 +01:00
Eric
43500c63d7
Upgrade to nim-json-rpc v0.4.2 and chronos v4 (#64)
* Add json de/serialization lib from codex to handle conversions

json-rpc now requires nim-json-serialization to convert types to/from json. Use the nim-json-serialization signatures to call the json serialization lib from nim-codex (should be moved to its own lib)

* Add ethers implementation for setMethodHandler

Was removed in json-rpc

* More json conversion updates

* Fix json_rpc.call returning JsonString instead of JsonNode

* Update exceptions

Use {.async: (raises: [...].} where needed
Annotate provider with {.push raises:[].}
Format signatures

* Start fixing tests (mainly conversion fixes)

* rename sender to `from`, update json error logging, add more conversions

* Refactor exceptions for providers and signers, fix more tests

- signer procs raise SignerError, provider procs raise ProviderError
- WalletError now inherits from SignerError
- move wallet module under signers
- create jsonrpo moudle under signers
- bump nim-json-rpc for null-handling fixes
- All jsonrpc provider tests passing, still need to fix others

* remove raises from async annotation for dynamic dispatch

- removes async: raises from getAddress and signTransaction because derived JsonRpcSigner methods were not being used when dynamically dispatched. Once `raises` was removed from the async annotation, the dynamic dispatch worked again. This is only the case for getAddress and signTransaction.
- add gcsafe annotation to wallet.provider so that it matches the base method

* Catch EstimateGasError before ProviderError

EstimateGasError is now a ProviderError (it is a SignerError, and SignerError is a ProviderError), so EstimateGasErrors were not being caught

* clean up - all tests passing

* support nim 2.0

* lock in chronos version

* Add serde options to the json util, along with tests

next step is to:
1. change back any ethers var names that were changed for serialization purposes, eg `from` and `type`
2. move the json util to its own lib

* bump json-rpc to 0.4.0 and fix test

* fix: specify raises for getAddress and sendTransaction

Fixes issue where getAddress and sendTransaction could not be found for MockSigner in tests. The problem was that the async: raises update had not been applied to the MockSigner.

* handle exceptions during jsonrpc init

There are too many exceptions to catch individually, including chronos raising CatchableError exceptions in await expansion. There are also many other errors captured inside of the new proc with CatchableError. Instead of making it more complicated and harder to read, I think sticking with excepting CatchableError inside of convertError is a sensible solution

* cleanup

* deserialize key defaults to serialize key

* Add more tests for OptIn/OptOut/Strict modes, fix logic

* use nim-serde instead of json util

Allows aliasing of de/serialized fields, so revert changes of sender to `from` and transactionType to `type`

* Move hash* shim to its own module

* address PR feedback

- add comments to hashes shim
- remove .catch from callback condition
- derive SignerError from EthersError instead of ProviderError. This allows Providers and Signers to be separate, as Ledger does it, to isolate functionality. Some signer functions now raise both ProviderError and SignerError
- Update reverts to check for SignerError
- Update ERC-20 method comment

* rename subscriptions.init > subscriptions.start
2024-02-19 16:50:46 +11:00
Mark Spanbroek
fd16d71ea5 version 0.7.1 2023-12-12 09:28:52 +01:00
Mark Spanbroek
c25de86656 remove upraises
we no longer support nim 1.2.x,
so upraises is no longer necessary
2023-12-12 09:28:06 +01:00
Mark Spanbroek
04b91d9f65 Test with Nim 1.6.16 2023-12-12 09:17:52 +01:00
Mark Spanbroek
abe8585f53 Do not decrease nonce when it wasn't increased 2023-12-12 09:08:01 +01:00
Eric
16b28f4535 wrap try/finally around populateTransaction logic to ensure the lock is always released in the case of an error 2023-12-12 09:08:01 +01:00
Eric
2428b756d6
On transaction failure, fetch revert reason with replayed transaction (#57)
When transaction fails (receipt.status is Failed), fetch revert reason by replaying transaction.
2023-10-25 11:36:00 +11:00
Eric
7eac8410af
prevent stuck transactions by async locking nonce sequencing (+ estimate gas) (#55)
- async lock during nonce sequencing + gas estimation
- simplified cancelTransaction (still exported) such that the new transaction is populated using populateTransaction, so that all gas and fees are reset
- moved reverting contract function into its own testing helpers module, and refactored any tests to use it
- updated the test helper reverts to check EstimateGasErrors
- combine ensureNonceSequence into populateTransaction
2023-10-25 10:42:25 +11:00
Adam Uhlíř
620b402a7d
feat: (de/in)crease allowance (#56) 2023-10-16 10:23:58 +02:00
Eric
f0303473f6
Increment nonce count when populating transaction (#54)
Increment nonce count when populating transaction

Co-authored-by: markspanbroek <mark@spanbroek.net>
2023-09-15 09:54:08 +10:00
Mark Spanbroek
8fff63102a version 0.7.0 2023-09-13 13:54:41 +02:00
Mark Spanbroek
15ed76ebed Use Result to return error when wallet creation fails
Co-authored-by: Eric Mastro <eric.mastro@gmail.com>
2023-09-13 10:11:18 +02:00
Mark Spanbroek
43041e7948 Small fix in Readme 2023-09-13 10:11:18 +02:00
Mark Spanbroek
81ec482fca Wallet: handle invalid key when instantiating new wallet 2023-09-13 10:11:18 +02:00
Mark Spanbroek
2ec0313dd3 version 0.6.0
updated contractabi brings in breaking change in nimcrypto
2023-08-29 12:25:39 +02:00
Mark Spanbroek
9327294044 update contractabi to 0.6.0 2023-08-29 12:25:39 +02:00
Mark Spanbroek
2b6f7b7a0d Fixes for Nim 2.0.0 2023-08-29 12:25:39 +02:00
Mark Spanbroek
99c225caa1 Update latest nim 1.6.x in CI 2023-08-29 12:25:39 +02:00
Eric
9f4f762e21
version 0.5.0
Breaking change:
`Filter` has been changed to `EventFilter` to be inline with ethers.js. `Filter` is used for creating subscriptions in `nim-ethers`. All previously-created instances of `Filter` in your consuming application code should be changed to `EventFilter`.
2023-07-24 15:54:18 +10:00
Eric
12d7a35203
Query past contract events (#51)
Based on ethers.js's queryFilter, allows querying of past contract events, by querying the logs for a contract's event topic.

* queryFilter to query past logs
* Allow querying of past block log events
* Can query by block number or block hash
2023-07-20 15:51:28 +10:00
Mark Spanbroek
c49311fca2 version 0.4.0 2023-07-05 15:09:31 +02:00
Mark Spanbroek
5f820fc971 Cleanup 2023-07-05 15:08:35 +02:00
Mark Spanbroek
2b181aa0f7 Allow wallet to be instantiated with a PrivateKey 2023-07-05 15:08:35 +02:00
Mark Spanbroek
5ed3f15706 Return transaction response for ERC20 functions
Allows callers to wait for confirmation of the
transaction
2023-07-05 15:08:22 +02:00
Mark Spanbroek
d7b7f67afb Formatting 2023-07-05 15:08:22 +02:00
Mark Spanbroek
842bf4d0a2 Refactor wallet signing 2023-07-05 15:07:52 +02:00
Mark Spanbroek
f1a1221d14 Move WalletError into its own module 2023-07-05 15:07:52 +02:00
Mark Spanbroek
c89701016a Fix EIP-155 signatures 2023-07-05 15:07:52 +02:00
Mark Spanbroek
5127991117 Add "value" to Transaction object 2023-07-05 15:07:52 +02:00
Mark Spanbroek
e086b71b42 version 0.3.0 2023-07-04 12:58:48 +02:00
Mark Spanbroek
310b06dfe8 Fix warnings 2023-07-04 12:58:48 +02:00
Mark Spanbroek
cd32dffc73 Move JSON conversion tests into their own module 2023-07-04 12:58:48 +02:00
Mark Spanbroek
09810e73ff Move confirm() override into contract module
And simplify its test
2023-07-04 12:58:48 +02:00
Mark Spanbroek
4e4a55b13e Cleanup 2023-07-04 12:58:48 +02:00
Mark Spanbroek
cb95cbc15a Make BlockHandler callback synchronous (breaking change)
Refactored the confirm() implementation to work
with a synchronous callback
2023-07-04 12:58:48 +02:00
Mark Spanbroek
0674548ecc Update contractabi to 0.5.0 2023-07-03 13:09:09 +02:00
Mark Spanbroek
82f6449374 Move JsonRpcSubscription type to jsonrpc module
Allows it to insert convertError to ensure that
any errors are re-raised as JsonRpcProviderError
2023-07-03 13:09:09 +02:00
Mark Spanbroek
738c6a87e2 Stop polling when provider is closed 2023-07-03 13:09:09 +02:00
Mark Spanbroek
a27c2de41c Close provider by unsubscribing and closing client 2023-07-03 13:09:09 +02:00
Mark Spanbroek
f8cac08cde Test that subscription stops after call to unsubscribe() 2023-07-03 13:09:09 +02:00
Mark Spanbroek
ceedf03c82 Subscriptions now also supported with http url 2023-07-03 13:09:09 +02:00
Mark Spanbroek
738d028fe3 Remove websockets url where not needed for tests 2023-07-03 13:09:09 +02:00
Mark Spanbroek
7e346914c0 Test contracts with polling 2023-07-03 13:09:09 +02:00
Mark Spanbroek
2481bda6e4 Subscribe to logs with polling 2023-07-03 13:09:09 +02:00
Mark Spanbroek
0aea16047c Ignore errors when retrieving block by hash 2023-07-03 13:09:09 +02:00
Mark Spanbroek
76bd3090d1 Fix intermittently failing test
eth_getFilterChanges returns the current block for
new subscriptions, which made the test fail.
2023-07-03 13:09:09 +02:00
Mark Spanbroek
1b151d589d Add polling interval to constructor of provider 2023-07-03 13:09:09 +02:00
Mark Spanbroek
88d60b14b0 Test JSON-RPC Provider with polling 2023-07-03 13:09:09 +02:00
Mark Spanbroek
0322ae1451 Ignore errors while polling 2023-07-03 13:09:09 +02:00
Mark Spanbroek
50cfd9d9dd untilCancelled template 2023-07-03 13:09:09 +02:00
Mark Spanbroek
3a76fa74f1 Make polling interval configurable 2023-07-03 13:09:09 +02:00
Mark Spanbroek
beac903a3f Remove duplication in tests 2023-07-03 13:09:09 +02:00
Mark Spanbroek
6a034870f8 Polling block subscriptions for non-websocket connections 2023-07-03 13:09:09 +02:00
Mark Spanbroek
127c9c9b0d Formatting 2023-07-03 13:09:09 +02:00
Mark Spanbroek
16fa0cfcf8 Use new subscription handling in JSON RPC provider 2023-07-03 13:09:09 +02:00
Mark Spanbroek
a7dc0ac9eb Move subscription handling to its own module 2023-07-03 13:09:09 +02:00
Mark Spanbroek
67c2d631d7 Update asynctest to 0.4.0 2023-07-03 13:09:09 +02:00
Mark Spanbroek
f0ac7065ed Move tests for JSON RPC provider into their own folder 2023-07-03 13:09:09 +02:00
Mark Spanbroek
0b951ce146 Set correct content-type for JSON-RPC 2023-07-03 11:29:31 +02:00
Eric Mastro
34b7a82565 fix: pending blocks may not contains block hash
Pending blocks may not contain a block hash and therefore Block.hash should be optional.
2023-07-03 11:29:09 +02:00
Adam Uhlíř
0321e6d7bd
fix: dont export json conversions of jsonrpc (#44) 2023-06-19 14:13:44 +02:00
Adam Uhlíř
18e225607c
fix: eth_call use signers address (#43) 2023-06-13 16:24:59 +02:00
Mark Spanbroek
5a4f786757 version 0.2.5 2023-04-19 10:06:04 +02:00
Mark Spanbroek
1ca90d0b3c Allow contract calls to override the block tag 2023-04-19 10:03:50 +02:00
Adam Uhlíř
3c12a65769
feat: erc20 module (#38)
Co-authored-by: Eric Mastro <github@egonat.me>
2023-03-29 13:41:44 +02:00
Ben Bierens
577e02b8a2
enables stylecheck (#36)
* enables stylecheck

* applies style check

* Applying style check

* uses alias to fix ambiguity
2023-03-09 10:58:54 +01:00
Mark Spanbroek
e462649aec version 0.2.4 2022-11-10 10:22:24 +01:00
Mark Spanbroek
e8592bb922 Remove unnecessary error check 2022-09-21 10:29:31 +02:00
Mark Spanbroek
7d2acd65e8 Fix imports 2022-09-21 10:29:31 +02:00
Mark Spanbroek
a62ea4fb8f Ensure that reverts works with functions with a return type 2022-09-21 10:29:31 +02:00
Mark Spanbroek
c5a40e5f9d Remove dependency on json-rpc provider for reverts 2022-09-21 10:29:31 +02:00
Mark Spanbroek
f545169331 Remove JSON wrapper from error in JSON RPC provider 2022-09-21 10:29:31 +02:00
Mark Spanbroek
cac6026b34 Change reverts API
- Enables postfix syntax: `call().reverts(reason)`
- Removes doesNotRevert etc; uses `check not` instead
- Removes waitFor(); return Future instead
2022-09-21 10:29:31 +02:00
Mark Spanbroek
d001ee8e01 Use solidityType() to check indexed event parameter 2022-09-21 10:27:45 +10:00
Eric Mastro
fc3cc9c577 version 0.2.3, bump contractabi 2022-09-21 10:27:45 +10:00
Eric Mastro
bbabee3727 update contractabi dependency to support distinct 2022-09-21 10:27:45 +10:00
Eric Mastro
8a484299e6 Remove en/decoding advice from readme 2022-09-21 10:27:45 +10:00
Eric Mastro
4d7e40eb0e remove en/decoding for distinct types
The changes to `nim-contract-abi` in https://github.com/status-im/nim-contract-abi/pull/5 have allowed for distinct type en/decoding procs to not need to be defined.
2022-09-21 10:27:45 +10:00
Eric Mastro
01d277f801 version 0.2.2 2022-09-21 10:27:45 +10:00
Eric Mastro
31ffc8992f Update compile time check to use when 2022-09-21 10:27:45 +10:00
Eric Mastro
ae2d33aacd Support 1.2.16 distinctBase compilation error 2022-09-21 10:27:45 +10:00
Eric Mastro
0adf56c65b Support distinct types for Event fields
Add support for indexed (and non-indexed) Event fields types that are distinct `ValueType` or `SmallByteArray`. For example,
```nim
type
  DistinctAlias = distinct array[32, byte]
  MyEvent = object of Event
    a {.indexed.}: DistinctAlias
    b: DistinctAlias # also allowed for non-indexed fields

## The below funcs generally need to be included for ABI
## encoding/decoding purposes when implementing distinct types.

func toArray(value: DistinctAlias): array[32, byte] =
  array[32, byte](value)

func encode*(encoder: var AbiEncoder, value: DistinctAlias) =
  encoder.write(value.toArray)

func decode*(decoder: var AbiDecoder,
             T: type DistinctAlias): ?!T =
  let d = ?decoder.read(type array[32, byte])
  success DistinctAlias(d)
```
2022-09-21 10:27:45 +10:00
Eric Mastro
e1a1a3805b remove extra spaces 2022-09-20 13:15:15 +10:00
Eric Mastro
5fe41a76ab PR comments
1. rename helpers to testing and expose externally via `import pkg/ethers/testing`
2. Change detection of revert from `EthersError` to `JsonRpcProviderError`
3, Remove catch of `CatchableError` from revert detection as this would swallow errors. Update tests accordingly.
2022-09-20 13:15:15 +10:00
Eric Mastro
f8ba91a297 Catch ValueError from nim-json-rpc 2022-09-20 13:15:15 +10:00
Eric Mastro
e0ac15b3ba add revert helpers for testing
Add the following helpers to help detect transaction reverts:
1. `reverts`
2. `revertsWith`
3. `doesNotRevert`
4. `doesNotRevertWith`
2022-09-20 13:15:15 +10:00
82 changed files with 9790 additions and 2886 deletions

View File

@ -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
View File

@ -3,3 +3,7 @@
!*.*
nimble.develop
nimble.paths
.idea
.nimble
.envrc
nimbledeps

112
Readme.md
View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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
View 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)
)

View 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
)

View 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

View 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

View 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

View 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

View 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)

View 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

View 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)

View 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
View 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()

View 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

View 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)

View 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
View 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
View 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)

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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)

View 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)

View File

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

View File

@ -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

View 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

View File

@ -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)

View File

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

89
ethers/signers/wallet.nim Normal file
View 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)

View 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)

View 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
View 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

View File

@ -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 &= ")"

View File

@ -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

View File

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

View File

@ -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"

View File

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

14
testmodule/helpers.nim Normal file
View 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)

View File

@ -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)

View 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()

View 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\""

View 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]

View 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

View File

@ -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":

View 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

View 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

View 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.}

View File

@ -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.}

View File

@ -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"

View 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

View File

@ -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

View 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])

View File

@ -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
View 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

View 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

View File

@ -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,) )

View 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

View File

@ -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)

View File

@ -0,0 +1,3 @@
import ./providers/testJsonRpc
{.warning[UnusedImport]:off.}

View File

@ -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
View 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)

View File

@ -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)

View 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);
}
}

View 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;
}
}
}

View 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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

@ -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 }
}

File diff suppressed because it is too large Load Diff

View File

@ -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'"