feat: Can wait for transaction confirmations

Allows specified number of transaction confirmations to be awaited before a transaction is considered finalised. Polls for a transaction receipt then listens for new blocks and calculates the number of blocks between the receipt and the new block.

Notes
1. Only works with websockets currently
2. Replaced transaction not supported yet
This commit is contained in:
Eric Mastro 2022-05-17 12:34:22 +10:00 committed by Eric Mastro
parent 0549800af6
commit 2f97a03fe2
7 changed files with 197 additions and 9 deletions

View File

@ -72,13 +72,17 @@ proc call(contract: Contract,
let response = await contract.provider.call(transaction, blockTag) let response = await contract.provider.call(transaction, blockTag)
return decodeResponse(ReturnType, response) return decodeResponse(ReturnType, response)
proc send(contract: Contract, function: string, parameters: tuple) {.async.} = proc send(contract: Contract, function: string, parameters: tuple):
Future[?TransactionResponse] {.async.} =
if signer =? contract.signer: if signer =? contract.signer:
let transaction = createTransaction(contract, function, parameters) let transaction = createTransaction(contract, function, parameters)
let populated = await signer.populateTransaction(transaction) let populated = await signer.populateTransaction(transaction)
await signer.sendTransaction(populated) let txResp = await signer.sendTransaction(populated)
return txResp.some
else: else:
await call(contract, function, parameters) await call(contract, function, parameters)
return TransactionResponse.none
func getParameterTuple(procedure: NimNode): NimNode = func getParameterTuple(procedure: NimNode): NimNode =
let parameters = procedure[3] let parameters = procedure[3]
@ -112,7 +116,8 @@ func addContractCall(procedure: var NimNode) =
return await call(`contract`, `function`, `parameters`, `returntype`) return await call(`contract`, `function`, `parameters`, `returntype`)
else: else:
quote: quote:
await send(`contract`, `function`, `parameters`) # TODO: need to be able to use wait here
discard await send(`contract`, `function`, `parameters`)
func addFuture(procedure: var NimNode) = func addFuture(procedure: var NimNode) =
let returntype = procedure[3][0] let returntype = procedure[3][0]

View File

@ -10,21 +10,76 @@ push: {.upraises: [].}
type type
Provider* = ref object of RootObj Provider* = ref object of RootObj
Subscription* = ref object of RootObj Subscription* = ref object of RootObj
Filter* = object Filter* = object
address*: Address address*: Address
topics*: seq[Topic] topics*: seq[Topic]
Log* = object Log* = object
data*: seq[byte] data*: seq[byte]
topics*: seq[Topic] topics*: seq[Topic]
TransactionHash* = array[32, byte]
BlockHash* = array[32, byte]
TransactionStatus* = enum
Failure = 0,
Success = 1,
Invalid = 2
TransactionResponse* = object
provider*: Provider
hash*: TransactionHash
TransactionReceipt* = object
sender*: ?Address
to*: ?Address
contractAddress*: ?Address
transactionIndex*: UInt256
gasUsed*: UInt256
logsBloom*: seq[byte]
blockHash*: BlockHash
transactionHash*: TransactionHash
logs*: seq[Log]
blockNumber*: ?UInt256
cumulativeGasUsed*: UInt256
status*: TransactionStatus
ProviderEventKind* = enum
LogEvent,
NewHeadEvent
ProviderEvent* = object
case kind*: ProviderEventKind
of LogEvent:
log*: Log
of NewHeadEvent:
newHead*: NewHead
ProviderEventHandler* = proc(event: ProviderEvent) {.gcsafe, upraises:[].}
ProviderEventCallback* = (ProviderEventHandler, ProviderEventKind)
LogHandler* = proc(log: Log) {.gcsafe, upraises:[].} LogHandler* = proc(log: Log) {.gcsafe, upraises:[].}
BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
NewHead* = object
number*: UInt256 # block number
transactions*: seq[TransactionHash]
# NewHeadHandler* = EventHandler[NewHead]
Topic* = array[32, byte] Topic* = array[32, byte]
Block* = object Block* = object
number*: UInt256 number*: UInt256
timestamp*: UInt256 timestamp*: UInt256
hash*: array[32, byte] hash*: array[32, byte]
const DEFAULT_CONFIRMATIONS* {.intdefine.} = 12
method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} = method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"

View File

@ -114,6 +114,11 @@ method getTransactionCount*(provider: JsonRpcProvider,
Future[UInt256] {.async.} = Future[UInt256] {.async.} =
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionCount(address, blockTag) return await client.eth_getTransactionCount(address, blockTag)
method getTransactionReceipt*(provider: JsonRpcProvider,
txHash: TransactionHash):
Future[?TransactionReceipt] {.async.} =
let client = await provider.client
return await client.eth_getTransactionReceipt(txHash)
method estimateGas*(provider: JsonRpcProvider, method estimateGas*(provider: JsonRpcProvider,
transaction: Transaction): Future[UInt256] {.async.} = transaction: Transaction): Future[UInt256] {.async.} =
@ -184,6 +189,76 @@ method signMessage*(signer: JsonRpcSigner,
return await client.eth_sign(address, message) return await client.eth_sign(address, message)
method sendTransaction*(signer: JsonRpcSigner, method sendTransaction*(signer: JsonRpcSigner,
transaction: Transaction) {.async.} = transaction: Transaction): Future[TransactionResponse] {.async.} =
let client = await signer.provider.client let
discard await client.eth_sendTransaction(transaction) client = await signer.provider.client
hash = await client.eth_sendTransaction(transaction)
return TransactionResponse(hash: hash, provider: signer.provider)
method wait*(tx: TransactionResponse,
wantedConfirms = DEFAULT_CONFIRMATIONS,
timeoutInBlocks = int.none): # will error if tx not mined in x blocks
Future[TransactionReceipt]
{.async, upraises: [JsonRpcProviderError].} = # raises for clarity
var
receipt: ?TransactionReceipt
subscription: JsonRpcSubscription
let
provider = JsonRpcProvider(tx.provider)
retFut = newFuture[TransactionReceipt]("wait")
proc confirmations(receipt: TransactionReceipt, atBlkNum: UInt256): UInt256 =
var confirms = (atBlkNum - !receipt.blockNumber) + 1
if confirms <= 0: confirms = 1.u256
return confirms
proc newBlock(blk: Block) =
# has been mined, need to check # of confirmations thus far
let confirms = (!receipt).confirmations(blk.number)
if confirms >= wantedConfirms.u256:
# fire and forget
discard subscription.unsubscribe()
retFut.complete(!receipt)
let startBlock = await provider.getBlockNumber()
# loop until the tx is mined, or times out (in blocks) if timeout specified
while receipt.isNone:
receipt = await provider.getTransactionReceipt(tx.hash)
if receipt.isSome and (!receipt).blockNumber.isSome:
break
if timeoutInBlocks.isSome:
let currBlock = await provider.getBlockNumber()
let blocksPassed = (currBlock - startBlock) + 1
if blocksPassed >= (!timeoutInBlocks).u256:
raiseProviderError("Transaction was not mined in " &
$(!timeoutInBlocks) & " blocks")
# TODO: should this be set to the current block time?
await sleepAsync(1.seconds)
# has been mined, need to check # of confirmations thus far
let confirms = (!receipt).confirmations(startBlock)
if confirms >= wantedConfirms.u256:
return !receipt
else:
let sub = await provider.subscribe(newBlock)
subscription = JsonRpcSubscription(sub)
return (await retFut)
method wait*(tx: Future[TransactionResponse],
wantedConfirms = DEFAULT_CONFIRMATIONS,
timeoutInBlocks = int.none):
Future[TransactionReceipt] {.async.} =
## Convenience method that allows wait to be chained to a sendTransaction
## call, eg:
## `await signer.sendTransaction(populated).wait(3)`
let txResp = await tx
return await txResp.wait(wantedConfirms, timeoutInBlocks)

View File

@ -1,4 +1,5 @@
import std/json import std/json
import std/strutils
import pkg/json_rpc/jsonmarshal import pkg/json_rpc/jsonmarshal
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../basics import ../../basics
@ -74,3 +75,12 @@ func fromJson*(json: JsonNode, name: string, result: var Log) =
fromJson(json["data"], "data", data) fromJson(json["data"], "data", data)
fromJson(json["topics"], "topics", topics) fromJson(json["topics"], "topics", topics)
result = Log(data: data, topics: topics) result = Log(data: data, topics: topics)
# TransactionStatus
func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) =
let val = fromHex[int](json.getStr)
result = TransactionStatus(val)
func `%`*(status: TransactionStatus): JsonNode =
%(status.int.toHex)

View File

@ -7,7 +7,8 @@ proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256 proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256
proc eth_estimateGas(transaction: Transaction): UInt256 proc eth_estimateGas(transaction: Transaction): UInt256
proc eth_chainId(): UInt256 proc eth_chainId(): UInt256
proc eth_sendTransaction(transaction: Transaction): array[32, byte] proc eth_sendTransaction(transaction: Transaction): TransactionHash
proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt
proc eth_sign(account: Address, message: seq[byte]): seq[byte] proc eth_sign(account: Address, message: seq[byte]): seq[byte]
proc eth_subscribe(name: string, filter = Filter.none): JsonNode proc eth_subscribe(name: string, filter = Filter.none): JsonNode
proc eth_unsubscribe(id: JsonNode): bool proc eth_unsubscribe(id: JsonNode): bool

View File

@ -20,7 +20,7 @@ method signMessage*(signer: Signer,
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*(signer: Signer, method sendTransaction*(signer: Signer,
transaction: Transaction) {.base, async.} = transaction: Transaction): Future[TransactionResponse] {.base, async.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getGasPrice*(signer: Signer): Future[UInt256] {.base.} = method getGasPrice*(signer: Signer): Future[UInt256] {.base.} =

View File

@ -1,7 +1,10 @@
import std/json import std/json
import pkg/asynctest import pkg/asynctest
import pkg/chronos import pkg/chronos
import pkg/ethers/providers/jsonrpc import pkg/ethers #/providers/jsonrpc
import pkg/stew/byteutils
import ./examples
import ./miner
suite "JsonRpcProvider": suite "JsonRpcProvider":
@ -50,3 +53,42 @@ suite "JsonRpcProvider":
check newBlock.timestamp > oldBlock.timestamp check newBlock.timestamp > oldBlock.timestamp
check newBlock.hash != oldBlock.hash check newBlock.hash != oldBlock.hash
await subscription.unsubscribe() 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 and UInt256.fromHex(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 be spawned so we can get newHeads inside of .wait
asyncSpawn provider.mineBlocks(3)
let receipt = await signer.sendTransaction(populated).wait(3)
let endBlock = await provider.getBlockNumber()
check receipt.blockNumber.isSome # was eventually mined
check (endBlock - !receipt.blockNumber) + 1 == 3 # +1 for the block the tx was mined in
test "waiting for block to be mined times out":
# must be spawned so we can get newHeads inside of .wait
asyncSpawn provider.mineBlocks(10)
let startBlock = await provider.getBlockNumber()
let response = TransactionResponse(hash: TransactionHash.example,
provider: provider)
try:
discard await response.wait(wantedConfirms = 2,
timeoutInBlocks = 5.some)
except JsonRpcProviderError as e:
check e.msg == "Transaction was not mined in 5 blocks"
let endBlock = await provider.getBlockNumber()
check (endBlock - startBlock) + 1 == 5 # +1 including start block