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:
parent
0549800af6
commit
2f97a03fe2
|
@ -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]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.} =
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue