nim-ethers/ethers/provider.nim
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

307 lines
9.3 KiB
Nim

import pkg/chronicles
import pkg/serde
import pkg/stew/byteutils
import ./basics
import ./transaction
import ./blocktag
export basics
export transaction
export blocktag
{.push raises: [].}
type
Provider* = ref object of RootObj
ProviderError* = object of EthersError
Subscription* = ref object of RootObj
EventFilter* {.serialize.} = ref object of RootObj
address*: Address
topics*: seq[Topic]
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]
TransactionStatus* = enum
Failure = 0,
Success = 1,
Invalid = 2
TransactionResponse* = object
provider*: Provider
hash* {.serialize.}: TransactionHash
TransactionReceipt* {.serialize.} = object
sender* {.serialize("from"), deserialize("from").}: ?Address
to*: ?Address
contractAddress*: ?Address
transactionIndex*: UInt256
gasUsed*: UInt256
logsBloom*: seq[byte]
blockHash*: ?BlockHash
transactionHash*: TransactionHash
logs*: seq[Log]
blockNumber*: ?UInt256
cumulativeGasUsed*: UInt256
effectiveGasPrice*: ?UInt256
status*: TransactionStatus
transactionType* {.serialize("type"), deserialize("type").}: TransactionType
LogHandler* = proc(log: Log) {.gcsafe, raises:[].}
BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].}
Topic* = array[32, byte]
Block* {.serialize.} = object
number*: ?UInt256
timestamp*: UInt256
hash*: ?BlockHash
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
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]).} =
doAssert false, "not implemented"
method getBlock*(
provider: Provider,
tag: BlockTag): Future[?Block] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method call*(
provider: Provider,
tx: Transaction,
blockTag = BlockTag.latest): Future[seq[byte]] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method getGasPrice*(
provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method getTransactionCount*(
provider: Provider,
address: Address,
blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method getTransaction*(
provider: Provider,
txHash: TransactionHash): Future[?PastTransaction] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method getTransactionReceipt*(
provider: Provider,
txHash: TransactionHash): Future[?TransactionReceipt] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method sendTransaction*(
provider: Provider,
rawTransaction: seq[byte]): Future[TransactionResponse] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method getLogs*(
provider: Provider,
filter: EventFilter): Future[seq[Log]] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method estimateGas*(
provider: Provider,
transaction: Transaction,
blockTag = BlockTag.latest): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method getChainId*(
provider: Provider): Future[UInt256] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method subscribe*(
provider: Provider,
filter: EventFilter,
callback: LogHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method subscribe*(
provider: Provider,
callback: BlockHandler): Future[Subscription] {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
method unsubscribe*(
subscription: Subscription) {.base, async: (raises:[ProviderError]).} =
doAssert false, "not implemented"
proc replay*(
provider: Provider,
tx: Transaction,
blockNumber: UInt256) {.async: (raises:[ProviderError]).} =
# 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))
method getRevertReason*(
provider: Provider,
hash: TransactionHash,
blockNumber: UInt256): Future[?string] {.base, async: (raises: [ProviderError]).} =
without pastTx =? await provider.getTransaction(hash):
return none string
try:
await provider.replay(pastTx.toTransaction, blockNumber)
return none string
except ProviderError as e:
# should contain the revert reason
return some e.msg
method getRevertReason*(
provider: Provider,
receipt: TransactionReceipt): Future[?string] {.base, async: (raises: [ProviderError]).} =
if receipt.status != TransactionStatus.Failure:
return none string
without blockNumber =? receipt.blockNumber:
return none string
return await provider.getRevertReason(receipt.transactionHash, blockNumber - 1)
proc ensureSuccess(
provider: Provider,
receipt: TransactionReceipt) {.async: (raises: [ProviderError]).} =
## 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:
logScope:
transactionHash = receipt.transactionHash.to0xHex
trace "transaction failed, replaying transaction to get revert reason"
if revertReason =? await provider.getRevertReason(receipt):
trace "transaction revert reason obtained", revertReason
raiseProviderError(revertReason)
else:
trace "transaction replay completed, no revert reason obtained"
raiseProviderError("Transaction reverted with unknown reason")
proc confirm*(
tx: TransactionResponse,
confirmations = EthersDefaultConfirmations,
timeout = EthersReceiptTimeoutBlks): Future[TransactionReceipt]
{.async: (raises: [CancelledError, ProviderError, EthersError]).} =
## Waits for a transaction to be mined and for the specified number of blocks
## to pass since it was mined (confirmations).
## 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 blockNumber: UInt256
let blockEvent = newAsyncEvent()
proc onBlockNumber(number: UInt256) =
blockNumber = number
blockEvent.fire()
proc onBlock(blck: Block) =
if number =? blck.number:
onBlockNumber(number)
onBlockNumber(await tx.provider.getBlockNumber())
let subscription = await tx.provider.subscribe(onBlock)
let finish = blockNumber + timeout.u256
var receipt: ?TransactionReceipt
while true:
await blockEvent.wait()
blockEvent.clear()
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.} =
## 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(confirmations, timeout)
method close*(provider: Provider) {.base, async: (raises:[ProviderError]).} =
discard