nim-ethers/ethers/provider.nim

317 lines
9.9 KiB
Nim

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 raises: [].}
type
Provider* = ref object of RootObj
EstimateGasError* = object of ProviderError
transaction*: Transaction
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"
method isSyncing*(provider: Provider): Future[bool] {.base, async.} =
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))
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:
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]).} =
## Waits for a transaction to be mined and for the specified number of blocks
## to pass since it was mined (confirmations). The number of confirmations
## 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.
assert confirmations > 0
var blockNumber: UInt256
## We need initialized succesfull Result, because the first iteration of the `while` loop
## bellow is triggered "manually" by calling `await updateBlockNumber` and not by block
## subscription. If left uninitialized then the Result is in error state and error is raised.
## This result is not used for block value, but for block subscription errors.
var blockSubscriptionResult: ?!Block = success(Block(number: UInt256.none, timestamp: 0.u256, hash: BlockHash.none))
let blockEvent = newAsyncEvent()
proc updateBlockNumber {.async: (raises: []).} =
try:
let number = await tx.provider.getBlockNumber()
if number > blockNumber:
blockNumber = number
blockEvent.fire()
except ProviderError:
# there's nothing we can do here
discard
proc onBlock(blckResult: ?!Block) =
blockSubscriptionResult = blckResult
if blckResult.isErr:
blockEvent.fire()
return
# ignore block parameter; hardhat may call this with pending blocks
asyncSpawn updateBlockNumber()
await updateBlockNumber()
let subscription = await tx.provider.subscribe(onBlock)
let finish = blockNumber + timeout.u256
var receipt: ?TransactionReceipt
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.} =
## 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