feat: Allow contract transactions to be waited on
Allow waiting for a specified number of confirmations for contract transactions. This change only requires an optional TransactionResponse return type to be added to the contract function. This allows the transaction hash to be passed to `.wait`. For example, previously the `mint` method looked like this without a return value: ``` method mint(token: TestToken, holder: Address, amount: UInt256) {.base, contract.} ``` it still works without a return value, but if we want to wait for a 3 confirmations, we can now define it like this: ``` method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.} ``` and use like this: ``` let receipt = await token.connect(signer0) .mint(accounts[1], 100.u256) .wait(3) # wait for 3 confirmations ```
This commit is contained in:
parent
2f97a03fe2
commit
a3e888128c
|
@ -101,6 +101,13 @@ func isConstant(procedure: NimNode): bool =
|
|||
return true
|
||||
false
|
||||
|
||||
func isTxResponse(returntype: NimNode): bool =
|
||||
return returntype.kind == nnkPrefix and
|
||||
returntype[0].kind == nnkIdent and
|
||||
returntype[0].strVal == "?" and
|
||||
returntype[1].kind == nnkIdent and
|
||||
returntype[1].strVal == $TransactionResponse
|
||||
|
||||
func addContractCall(procedure: var NimNode) =
|
||||
let contract = procedure[3][1][0]
|
||||
let function = $basename(procedure[0])
|
||||
|
@ -115,9 +122,15 @@ func addContractCall(procedure: var NimNode) =
|
|||
quote:
|
||||
return await call(`contract`, `function`, `parameters`, `returntype`)
|
||||
else:
|
||||
quote:
|
||||
# TODO: need to be able to use wait here
|
||||
discard await send(`contract`, `function`, `parameters`)
|
||||
# When ?TransactionResponse is specified as the return type of contract
|
||||
# method, it allows for contract transactions to be awaited on
|
||||
# confirmations
|
||||
if returntype.isTxResponse:
|
||||
quote:
|
||||
return await send(`contract`, `function`, `parameters`)
|
||||
else:
|
||||
quote:
|
||||
discard await send(`contract`, `function`, `parameters`)
|
||||
|
||||
func addFuture(procedure: var NimNode) =
|
||||
let returntype = procedure[3][0]
|
||||
|
@ -132,11 +145,18 @@ func addAsyncPragma(procedure: var NimNode) =
|
|||
|
||||
func checkReturnType(procedure: NimNode) =
|
||||
let returntype = procedure[3][0]
|
||||
if returntype.kind != nnkEmpty and not procedure.isConstant:
|
||||
const message =
|
||||
"only contract functions with {.view.} or {.pure.} " &
|
||||
"can have a return type"
|
||||
error(message, returntype)
|
||||
|
||||
if returntype.kind != nnkEmpty:
|
||||
# Do not throw exception for methods that have a TransactionResponse
|
||||
# return type as that is needed for .wait
|
||||
if returntype.isTxResponse:
|
||||
return
|
||||
|
||||
if not procedure.isConstant:
|
||||
const message =
|
||||
"only contract functions with {.view.} or {.pure.} " &
|
||||
"can have a return type"
|
||||
error(message, returntype)
|
||||
|
||||
macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped =
|
||||
|
||||
|
|
|
@ -10,30 +10,22 @@ push: {.upraises: [].}
|
|||
|
||||
type
|
||||
Provider* = ref object of RootObj
|
||||
|
||||
Subscription* = ref object of RootObj
|
||||
|
||||
Filter* = object
|
||||
address*: Address
|
||||
topics*: seq[Topic]
|
||||
|
||||
Log* = object
|
||||
data*: seq[byte]
|
||||
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
|
||||
|
@ -47,38 +39,17 @@ type
|
|||
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:[].}
|
||||
BlockHandler* = proc(blck: Block) {.gcsafe, upraises:[].}
|
||||
|
||||
NewHead* = object
|
||||
number*: UInt256 # block number
|
||||
transactions*: seq[TransactionHash]
|
||||
# NewHeadHandler* = EventHandler[NewHead]
|
||||
|
||||
Topic* = array[32, byte]
|
||||
|
||||
Block* = object
|
||||
number*: UInt256
|
||||
timestamp*: UInt256
|
||||
hash*: array[32, byte]
|
||||
|
||||
const DEFAULT_CONFIRMATIONS* {.intdefine.} = 12
|
||||
const RECEIPT_TIMEOUT_BLKS* {.intdefine.} = 50 # in blocks
|
||||
const RECEIPT_POLLING_INTERVAL* {.intdefine.} = 1 # in seconds
|
||||
|
||||
method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} =
|
||||
doAssert false, "not implemented"
|
||||
|
|
|
@ -198,7 +198,7 @@ method sendTransaction*(signer: JsonRpcSigner,
|
|||
|
||||
method wait*(tx: TransactionResponse,
|
||||
wantedConfirms = DEFAULT_CONFIRMATIONS,
|
||||
timeoutInBlocks = int.none): # will error if tx not mined in x blocks
|
||||
timeoutInBlocks = RECEIPT_TIMEOUT_BLKS.some): # will error if tx not mined in x blocks
|
||||
Future[TransactionReceipt]
|
||||
{.async, upraises: [JsonRpcProviderError].} = # raises for clarity
|
||||
|
||||
|
@ -239,8 +239,7 @@ method wait*(tx: TransactionResponse,
|
|||
raiseProviderError("Transaction was not mined in " &
|
||||
$(!timeoutInBlocks) & " blocks")
|
||||
|
||||
# TODO: should this be set to the current block time?
|
||||
await sleepAsync(1.seconds)
|
||||
await sleepAsync(RECEIPT_POLLING_INTERVAL.seconds)
|
||||
|
||||
# has been mined, need to check # of confirmations thus far
|
||||
let confirms = (!receipt).confirmations(startBlock)
|
||||
|
@ -254,7 +253,7 @@ method wait*(tx: TransactionResponse,
|
|||
|
||||
method wait*(tx: Future[TransactionResponse],
|
||||
wantedConfirms = DEFAULT_CONFIRMATIONS,
|
||||
timeoutInBlocks = int.none):
|
||||
timeoutInBlocks = RECEIPT_TIMEOUT_BLKS.some):
|
||||
Future[TransactionReceipt] {.async.} =
|
||||
## Convenience method that allows wait to be chained to a sendTransaction
|
||||
## call, eg:
|
||||
|
@ -262,3 +261,19 @@ method wait*(tx: Future[TransactionResponse],
|
|||
|
||||
let txResp = await tx
|
||||
return await txResp.wait(wantedConfirms, timeoutInBlocks)
|
||||
|
||||
method wait*(tx: Future[?TransactionResponse],
|
||||
wantedConfirms = DEFAULT_CONFIRMATIONS,
|
||||
timeoutInBlocks = RECEIPT_TIMEOUT_BLKS.some):
|
||||
Future[TransactionReceipt] {.async.} =
|
||||
## Convenience method that allows wait to be chained to a contract
|
||||
## transaction, eg:
|
||||
## `await token.connect(signer0)
|
||||
## .mint(accounts[1], 100.u256)
|
||||
## .wait(3)`
|
||||
|
||||
let txResp = await tx
|
||||
if txResp.isNone:
|
||||
raiseProviderError("Transaction hash required. Possibly was a call instead of a send?")
|
||||
|
||||
return await (!txResp).wait(wantedConfirms, timeoutInBlocks)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import chronos
|
||||
import pkg/ethers/providers/jsonrpc
|
||||
|
||||
|
||||
proc mineBlocks*(provider: JsonRpcProvider, blks: int) {.async.} =
|
||||
for i in 1..blks:
|
||||
discard await provider.send("evm_mine")
|
||||
await sleepAsync(1.seconds)
|
|
@ -3,6 +3,7 @@ import pkg/asynctest
|
|||
import pkg/stint
|
||||
import pkg/ethers
|
||||
import ./hardhat
|
||||
import ./miner
|
||||
|
||||
type
|
||||
|
||||
|
@ -18,8 +19,7 @@ method totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.}
|
|||
method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract, view.}
|
||||
method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.}
|
||||
method transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.base, contract.}
|
||||
|
||||
method mint(token: TestToken, holder: Address, amount: UInt256) {.base, contract.}
|
||||
method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.}
|
||||
|
||||
suite "Contracts":
|
||||
|
||||
|
@ -45,12 +45,12 @@ suite "Contracts":
|
|||
|
||||
test "can call non-constant functions":
|
||||
token = TestToken.new(token.address, provider.getSigner())
|
||||
await token.mint(accounts[1], 100.u256)
|
||||
discard await token.mint(accounts[1], 100.u256)
|
||||
check (await token.totalSupply()) == 100.u256
|
||||
check (await token.balanceOf(accounts[1])) == 100.u256
|
||||
|
||||
test "can call non-constant functions without a signer":
|
||||
await token.mint(accounts[1], 100.u256)
|
||||
discard await token.mint(accounts[1], 100.u256)
|
||||
check (await token.balanceOf(accounts[1])) == 0.u256
|
||||
|
||||
test "can call constant functions without a return type":
|
||||
|
@ -77,7 +77,7 @@ suite "Contracts":
|
|||
test "can connect to different providers and signers":
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let signer1 = provider.getSigner(accounts[1])
|
||||
await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
check (await token.connect(provider).balanceOf(accounts[0])) == 50.u256
|
||||
|
@ -94,7 +94,7 @@ suite "Contracts":
|
|||
let signer1 = provider.getSigner(accounts[1])
|
||||
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
await token.connect(signer1).transfer(accounts[2], 25.u256)
|
||||
await subscription.unsubscribe()
|
||||
|
@ -114,9 +114,22 @@ suite "Contracts":
|
|||
let signer0 = provider.getSigner(accounts[0])
|
||||
|
||||
let subscription = await token.subscribe(Transfer, handleTransfer)
|
||||
await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
discard await token.connect(signer0).mint(accounts[0], 100.u256)
|
||||
await subscription.unsubscribe()
|
||||
|
||||
await token.connect(signer0).transfer(accounts[1], 50.u256)
|
||||
|
||||
check transfers == @[Transfer(receiver: accounts[0], value: 100.u256)]
|
||||
|
||||
test "can wait for contract interaction tx to be mined":
|
||||
# must be spawned so we can get newHeads inside of .wait
|
||||
asyncSpawn provider.mineBlocks(3)
|
||||
|
||||
let signer0 = provider.getSigner(accounts[0])
|
||||
let receipt = await token.connect(signer0)
|
||||
.mint(accounts[1], 100.u256)
|
||||
.wait(3) # wait for 3 confirmations
|
||||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import std/json
|
||||
import pkg/asynctest
|
||||
import pkg/chronos
|
||||
import pkg/ethers #/providers/jsonrpc
|
||||
import pkg/ethers
|
||||
import pkg/stew/byteutils
|
||||
import ./examples
|
||||
import ./miner
|
||||
|
|
Loading…
Reference in New Issue