mirror of
https://github.com/codex-storage/nim-ethers.git
synced 2025-01-10 19:36:28 +00:00
Make BlockHandler callback synchronous (breaking change)
Refactored the confirm() implementation to work with a synchronous callback
This commit is contained in:
parent
0674548ecc
commit
cb95cbc15a
@ -41,7 +41,7 @@ type
|
|||||||
cumulativeGasUsed*: UInt256
|
cumulativeGasUsed*: UInt256
|
||||||
status*: TransactionStatus
|
status*: TransactionStatus
|
||||||
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
|
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
|
||||||
BlockHandler* = proc(blck: Block): Future[void] {.gcsafe, upraises:[].}
|
BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
|
||||||
Topic* = array[32, byte]
|
Topic* = array[32, byte]
|
||||||
Block* = object
|
Block* = object
|
||||||
number*: ?UInt256
|
number*: ?UInt256
|
||||||
@ -102,98 +102,65 @@ method subscribe*(provider: Provider,
|
|||||||
method unsubscribe*(subscription: Subscription) {.base, async.} =
|
method unsubscribe*(subscription: Subscription) {.base, async.} =
|
||||||
doAssert false, "not implemented"
|
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
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
if number =? receipt.blockNumber and
|
|
||||||
number > 0 and
|
|
||||||
# from ethers.js: "geth-etc" returns receipts before they are ready
|
|
||||||
receipt.blockHash.isSome:
|
|
||||||
|
|
||||||
return number.confirmations(atBlock) >= wantedConfirms.u256
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
proc confirm*(tx: TransactionResponse,
|
proc confirm*(tx: TransactionResponse,
|
||||||
wantedConfirms: Positive = EthersDefaultConfirmations,
|
confirmations = EthersDefaultConfirmations,
|
||||||
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
timeout = EthersReceiptTimeoutBlks):
|
||||||
Future[TransactionReceipt]
|
Future[TransactionReceipt]
|
||||||
{.async, upraises: [EthersError].} = # raises for clarity
|
{.async, upraises: [EthersError].} =
|
||||||
## Waits for a transaction to be mined and for the specified number of blocks
|
## Waits for a transaction to be mined and for the specified number of blocks
|
||||||
## to pass since it was mined (confirmations).
|
## to pass since it was mined (confirmations).
|
||||||
## A timeout, in blocks, can be specified that will raise an error if too many
|
## A timeout, in blocks, can be specified that will raise an error if too many
|
||||||
## blocks have passed without the tx having been mined.
|
## blocks have passed without the tx having been mined.
|
||||||
|
|
||||||
var subscription: Subscription
|
var blockNumber: UInt256
|
||||||
let
|
let blockEvent = newAsyncEvent()
|
||||||
provider = tx.provider
|
|
||||||
retFut = newFuture[TransactionReceipt]("wait")
|
|
||||||
|
|
||||||
# used to check for block timeouts
|
proc onBlockNumber(number: UInt256) =
|
||||||
let startBlock = await provider.getBlockNumber()
|
blockNumber = number
|
||||||
|
blockEvent.fire()
|
||||||
|
|
||||||
proc newBlock(blk: Block) {.async.} =
|
proc onBlock(blck: Block) =
|
||||||
## subscription callback, called every time a new block event is sent from
|
if number =? blck.number:
|
||||||
## the node
|
onBlockNumber(number)
|
||||||
|
|
||||||
# if ethereum node doesn't include blockNumber in the event
|
onBlockNumber(await tx.provider.getBlockNumber())
|
||||||
without blkNum =? blk.number:
|
let subscription = await tx.provider.subscribe(onBlock)
|
||||||
return
|
|
||||||
|
|
||||||
if receipt =? (await provider.getTransactionReceipt(tx.hash)) and
|
let finish = blockNumber + timeout.u256
|
||||||
receipt.hasBeenMined(blkNum, wantedConfirms):
|
var receipt: ?TransactionReceipt
|
||||||
# fire and forget
|
|
||||||
discard subscription.unsubscribe()
|
|
||||||
if not retFut.finished:
|
|
||||||
retFut.complete(receipt)
|
|
||||||
|
|
||||||
elif timeoutInBlocks > 0:
|
while true:
|
||||||
let blocksPassed = (blkNum - startBlock) + 1
|
await blockEvent.wait()
|
||||||
if blocksPassed >= timeoutInBlocks.u256:
|
blockEvent.clear()
|
||||||
discard subscription.unsubscribe()
|
|
||||||
if not retFut.finished:
|
|
||||||
let message =
|
|
||||||
"Transaction was not mined in " & $timeoutInBlocks & " blocks"
|
|
||||||
retFut.fail(newException(EthersError, message))
|
|
||||||
|
|
||||||
# If our tx is already mined, return the receipt. Otherwise, check each
|
if blockNumber >= finish:
|
||||||
# new block to see if the tx has been mined
|
await subscription.unsubscribe()
|
||||||
if receipt =? (await provider.getTransactionReceipt(tx.hash)) and
|
raise newException(EthersError, "tx not mined before timeout")
|
||||||
receipt.hasBeenMined(startBlock, wantedConfirms):
|
|
||||||
|
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()
|
||||||
return receipt
|
return receipt
|
||||||
else:
|
|
||||||
subscription = await provider.subscribe(newBlock)
|
|
||||||
return (await retFut)
|
|
||||||
|
|
||||||
proc confirm*(tx: Future[TransactionResponse],
|
proc confirm*(tx: Future[TransactionResponse],
|
||||||
wantedConfirms: Positive = EthersDefaultConfirmations,
|
confirmations: int = EthersDefaultConfirmations,
|
||||||
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
timeout: int = EthersReceiptTimeoutBlks):
|
||||||
Future[TransactionReceipt] {.async.} =
|
Future[TransactionReceipt] {.async.} =
|
||||||
## Convenience method that allows wait to be chained to a sendTransaction
|
## Convenience method that allows wait to be chained to a sendTransaction
|
||||||
## call, eg:
|
## call, eg:
|
||||||
## `await signer.sendTransaction(populated).confirm(3)`
|
## `await signer.sendTransaction(populated).confirm(3)`
|
||||||
|
|
||||||
let txResp = await tx
|
let txResp = await tx
|
||||||
return await txResp.confirm(wantedConfirms, timeoutInBlocks)
|
return await txResp.confirm(confirmations, timeout)
|
||||||
|
|
||||||
proc confirm*(tx: Future[?TransactionResponse],
|
proc confirm*(tx: Future[?TransactionResponse],
|
||||||
wantedConfirms: Positive = EthersDefaultConfirmations,
|
confirmations: int = EthersDefaultConfirmations,
|
||||||
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
timeout: int = EthersReceiptTimeoutBlks):
|
||||||
Future[TransactionReceipt] {.async.} =
|
Future[TransactionReceipt] {.async.} =
|
||||||
## Convenience method that allows wait to be chained to a contract
|
## Convenience method that allows wait to be chained to a contract
|
||||||
## transaction, eg:
|
## transaction, eg:
|
||||||
@ -207,7 +174,7 @@ proc confirm*(tx: Future[?TransactionResponse],
|
|||||||
"Transaction hash required. Possibly was a call instead of a send?"
|
"Transaction hash required. Possibly was a call instead of a send?"
|
||||||
)
|
)
|
||||||
|
|
||||||
return await txResp.confirm(wantedConfirms, timeoutInBlocks)
|
return await txResp.confirm(confirmations, timeout)
|
||||||
|
|
||||||
method close*(provider: Provider) {.async, base.} =
|
method close*(provider: Provider) {.async, base.} =
|
||||||
discard
|
discard
|
||||||
|
@ -68,7 +68,7 @@ method subscribeBlocks(subscriptions: WebSocketSubscriptions,
|
|||||||
{.async.} =
|
{.async.} =
|
||||||
proc callback(id, arguments: JsonNode) =
|
proc callback(id, arguments: JsonNode) =
|
||||||
if blck =? Block.fromJson(arguments["result"]).catch:
|
if blck =? Block.fromJson(arguments["result"]).catch:
|
||||||
asyncSpawn onBlock(blck)
|
onBlock(blck)
|
||||||
let id = await subscriptions.client.eth_subscribe("newHeads")
|
let id = await subscriptions.client.eth_subscribe("newHeads")
|
||||||
subscriptions.callbacks[id] = callback
|
subscriptions.callbacks[id] = callback
|
||||||
return id
|
return id
|
||||||
@ -135,7 +135,7 @@ method subscribeBlocks(subscriptions: PollingSubscriptions,
|
|||||||
proc getBlock(hash: BlockHash) {.async.} =
|
proc getBlock(hash: BlockHash) {.async.} =
|
||||||
try:
|
try:
|
||||||
if blck =? (await subscriptions.client.eth_getBlockByHash(hash, false)):
|
if blck =? (await subscriptions.client.eth_getBlockByHash(hash, false)):
|
||||||
await onBlock(blck)
|
onBlock(blck)
|
||||||
except CatchableError:
|
except CatchableError:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
|
|||||||
let oldBlock = !await provider.getBlock(BlockTag.latest)
|
let oldBlock = !await provider.getBlock(BlockTag.latest)
|
||||||
discard await provider.send("evm_mine")
|
discard await provider.send("evm_mine")
|
||||||
var newBlock: Block
|
var newBlock: Block
|
||||||
let blockHandler = proc(blck: Block) {.async.} = newBlock = blck
|
let blockHandler = proc(blck: Block) = newBlock = blck
|
||||||
let subscription = await provider.subscribe(blockHandler)
|
let subscription = await provider.subscribe(blockHandler)
|
||||||
discard await provider.send("evm_mine")
|
discard await provider.send("evm_mine")
|
||||||
check eventually newBlock.number.isSome
|
check eventually newBlock.number.isSome
|
||||||
@ -67,48 +67,23 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
|
|||||||
check UInt256.fromHex("0x" & txResp.hash.toHex) > 0
|
check UInt256.fromHex("0x" & txResp.hash.toHex) > 0
|
||||||
|
|
||||||
test "can wait for a transaction to be confirmed":
|
test "can wait for a transaction to be confirmed":
|
||||||
|
for confirmations in 0..3:
|
||||||
let signer = provider.getSigner()
|
let signer = provider.getSigner()
|
||||||
let transaction = Transaction.example
|
let transaction = Transaction.example
|
||||||
let populated = await signer.populateTransaction(transaction)
|
let populated = await signer.populateTransaction(transaction)
|
||||||
|
let confirming = signer.sendTransaction(populated).confirm(confirmations)
|
||||||
|
await sleepAsync(100.millis) # wait for tx to be mined
|
||||||
|
await provider.mineBlocks(confirmations - 1)
|
||||||
|
let receipt = await confirming
|
||||||
|
check receipt.blockNumber.isSome
|
||||||
|
|
||||||
# must not be awaited so we can get newHeads inside of .wait
|
test "confirmation times out":
|
||||||
let futMined = provider.mineBlocks(5)
|
let hash = TransactionHash.example
|
||||||
|
let tx = TransactionResponse(provider: provider, hash: hash)
|
||||||
let receipt = await signer.sendTransaction(populated).confirm(3)
|
let confirming = tx.confirm(confirmations = 2, timeout = 5)
|
||||||
let endBlock = await provider.getBlockNumber()
|
await provider.mineBlocks(5)
|
||||||
|
expect EthersError:
|
||||||
check receipt.blockNumber.isSome # was eventually mined
|
discard await confirming
|
||||||
|
|
||||||
# >= 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":
|
test "Conversion: missing block number in Block isNone":
|
||||||
|
|
||||||
@ -207,58 +182,6 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
|
|||||||
var txReceipt = TransactionReceipt.fromJson(txReceiptJson)
|
var txReceipt = TransactionReceipt.fromJson(txReceiptJson)
|
||||||
check txReceipt.blockHash.isNone
|
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":
|
test "raises JsonRpcProviderError when something goes wrong":
|
||||||
let provider = JsonRpcProvider.new("http://invalid.")
|
let provider = JsonRpcProvider.new("http://invalid.")
|
||||||
expect JsonRpcProviderError:
|
expect JsonRpcProviderError:
|
||||||
@ -270,6 +193,6 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
|
|||||||
expect JsonRpcProviderError:
|
expect JsonRpcProviderError:
|
||||||
discard await provider.getBlock(BlockTag.latest)
|
discard await provider.getBlock(BlockTag.latest)
|
||||||
expect JsonRpcProviderError:
|
expect JsonRpcProviderError:
|
||||||
discard await provider.subscribe(proc(_: Block) {.async.} = discard)
|
discard await provider.subscribe(proc(_: Block) = discard)
|
||||||
expect JsonRpcProviderError:
|
expect JsonRpcProviderError:
|
||||||
discard await provider.getSigner().sendTransaction(Transaction.example)
|
discard await provider.getSigner().sendTransaction(Transaction.example)
|
||||||
|
@ -20,7 +20,7 @@ template subscriptionTests(subscriptions, client) =
|
|||||||
|
|
||||||
test "subscribes to new blocks":
|
test "subscribes to new blocks":
|
||||||
var latestBlock: Block
|
var latestBlock: Block
|
||||||
proc callback(blck: Block) {.async.} =
|
proc callback(blck: Block) =
|
||||||
latestBlock = blck
|
latestBlock = blck
|
||||||
let subscription = await subscriptions.subscribeBlocks(callback)
|
let subscription = await subscriptions.subscribeBlocks(callback)
|
||||||
discard await client.call("evm_mine", newJArray())
|
discard await client.call("evm_mine", newJArray())
|
||||||
@ -31,7 +31,7 @@ template subscriptionTests(subscriptions, client) =
|
|||||||
|
|
||||||
test "stops listening to new blocks when unsubscribed":
|
test "stops listening to new blocks when unsubscribed":
|
||||||
var count = 0
|
var count = 0
|
||||||
proc callback(blck: Block) {.async.} =
|
proc callback(blck: Block) =
|
||||||
inc count
|
inc count
|
||||||
let subscription = await subscriptions.subscribeBlocks(callback)
|
let subscription = await subscriptions.subscribeBlocks(callback)
|
||||||
discard await client.call("evm_mine", newJArray())
|
discard await client.call("evm_mine", newJArray())
|
||||||
@ -44,7 +44,7 @@ template subscriptionTests(subscriptions, client) =
|
|||||||
|
|
||||||
test "stops listening to new blocks when provider is closed":
|
test "stops listening to new blocks when provider is closed":
|
||||||
var count = 0
|
var count = 0
|
||||||
proc callback(blck: Block) {.async.} =
|
proc callback(blck: Block) =
|
||||||
inc count
|
inc count
|
||||||
let subscription = await subscriptions.subscribeBlocks(callback)
|
let subscription = await subscriptions.subscribeBlocks(callback)
|
||||||
discard await client.call("evm_mine", newJArray())
|
discard await client.call("evm_mine", newJArray())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user