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:
Eric Mastro 2022-05-17 14:57:18 +10:00 committed by Eric Mastro
parent 2f97a03fe2
commit a3e888128c
6 changed files with 78 additions and 51 deletions

View File

@ -101,6 +101,13 @@ func isConstant(procedure: NimNode): bool =
return true return true
false 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) = func addContractCall(procedure: var NimNode) =
let contract = procedure[3][1][0] let contract = procedure[3][1][0]
let function = $basename(procedure[0]) let function = $basename(procedure[0])
@ -115,9 +122,15 @@ func addContractCall(procedure: var NimNode) =
quote: quote:
return await call(`contract`, `function`, `parameters`, `returntype`) return await call(`contract`, `function`, `parameters`, `returntype`)
else: else:
quote: # When ?TransactionResponse is specified as the return type of contract
# TODO: need to be able to use wait here # method, it allows for contract transactions to be awaited on
discard await send(`contract`, `function`, `parameters`) # confirmations
if returntype.isTxResponse:
quote:
return await send(`contract`, `function`, `parameters`)
else:
quote:
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]
@ -132,11 +145,18 @@ func addAsyncPragma(procedure: var NimNode) =
func checkReturnType(procedure: NimNode) = func checkReturnType(procedure: NimNode) =
let returntype = procedure[3][0] let returntype = procedure[3][0]
if returntype.kind != nnkEmpty and not procedure.isConstant:
const message = if returntype.kind != nnkEmpty:
"only contract functions with {.view.} or {.pure.} " & # Do not throw exception for methods that have a TransactionResponse
"can have a return type" # return type as that is needed for .wait
error(message, returntype) 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 = macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped =

View File

@ -10,30 +10,22 @@ 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] TransactionHash* = array[32, byte]
BlockHash* = array[32, byte] BlockHash* = array[32, byte]
TransactionStatus* = enum TransactionStatus* = enum
Failure = 0, Failure = 0,
Success = 1, Success = 1,
Invalid = 2 Invalid = 2
TransactionResponse* = object TransactionResponse* = object
provider*: Provider provider*: Provider
hash*: TransactionHash hash*: TransactionHash
TransactionReceipt* = object TransactionReceipt* = object
sender*: ?Address sender*: ?Address
to*: ?Address to*: ?Address
@ -47,38 +39,17 @@ type
blockNumber*: ?UInt256 blockNumber*: ?UInt256
cumulativeGasUsed*: UInt256 cumulativeGasUsed*: UInt256
status*: TransactionStatus 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 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.} = method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"

View File

@ -198,7 +198,7 @@ method sendTransaction*(signer: JsonRpcSigner,
method wait*(tx: TransactionResponse, method wait*(tx: TransactionResponse,
wantedConfirms = DEFAULT_CONFIRMATIONS, 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] Future[TransactionReceipt]
{.async, upraises: [JsonRpcProviderError].} = # raises for clarity {.async, upraises: [JsonRpcProviderError].} = # raises for clarity
@ -239,8 +239,7 @@ method wait*(tx: TransactionResponse,
raiseProviderError("Transaction was not mined in " & raiseProviderError("Transaction was not mined in " &
$(!timeoutInBlocks) & " blocks") $(!timeoutInBlocks) & " blocks")
# TODO: should this be set to the current block time? await sleepAsync(RECEIPT_POLLING_INTERVAL.seconds)
await sleepAsync(1.seconds)
# has been mined, need to check # of confirmations thus far # has been mined, need to check # of confirmations thus far
let confirms = (!receipt).confirmations(startBlock) let confirms = (!receipt).confirmations(startBlock)
@ -254,7 +253,7 @@ method wait*(tx: TransactionResponse,
method wait*(tx: Future[TransactionResponse], method wait*(tx: Future[TransactionResponse],
wantedConfirms = DEFAULT_CONFIRMATIONS, wantedConfirms = DEFAULT_CONFIRMATIONS,
timeoutInBlocks = int.none): timeoutInBlocks = RECEIPT_TIMEOUT_BLKS.some):
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:
@ -262,3 +261,19 @@ method wait*(tx: Future[TransactionResponse],
let txResp = await tx let txResp = await tx
return await txResp.wait(wantedConfirms, timeoutInBlocks) 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)

8
testmodule/miner.nim Normal file
View File

@ -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)

View File

@ -3,6 +3,7 @@ import pkg/asynctest
import pkg/stint import pkg/stint
import pkg/ethers import pkg/ethers
import ./hardhat import ./hardhat
import ./miner
type type
@ -18,8 +19,7 @@ method totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.}
method balanceOf*(erc20: Erc20, account: Address): 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 allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.}
method transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.base, contract.} method transfer*(erc20: Erc20, recipient: Address, amount: UInt256) {.base, contract.}
method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.}
method mint(token: TestToken, holder: Address, amount: UInt256) {.base, contract.}
suite "Contracts": suite "Contracts":
@ -45,12 +45,12 @@ suite "Contracts":
test "can call non-constant functions": test "can call non-constant functions":
token = TestToken.new(token.address, provider.getSigner()) 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.totalSupply()) == 100.u256
check (await token.balanceOf(accounts[1])) == 100.u256 check (await token.balanceOf(accounts[1])) == 100.u256
test "can call non-constant functions without a signer": 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 check (await token.balanceOf(accounts[1])) == 0.u256
test "can call constant functions without a return type": test "can call constant functions without a return type":
@ -77,7 +77,7 @@ suite "Contracts":
test "can connect to different providers and signers": test "can connect to different providers and signers":
let signer0 = provider.getSigner(accounts[0]) let signer0 = provider.getSigner(accounts[0])
let signer1 = provider.getSigner(accounts[1]) 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(signer0).transfer(accounts[1], 50.u256)
await token.connect(signer1).transfer(accounts[2], 25.u256) await token.connect(signer1).transfer(accounts[2], 25.u256)
check (await token.connect(provider).balanceOf(accounts[0])) == 50.u256 check (await token.connect(provider).balanceOf(accounts[0])) == 50.u256
@ -94,7 +94,7 @@ suite "Contracts":
let signer1 = provider.getSigner(accounts[1]) let signer1 = provider.getSigner(accounts[1])
let subscription = await token.subscribe(Transfer, handleTransfer) 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(signer0).transfer(accounts[1], 50.u256)
await token.connect(signer1).transfer(accounts[2], 25.u256) await token.connect(signer1).transfer(accounts[2], 25.u256)
await subscription.unsubscribe() await subscription.unsubscribe()
@ -114,9 +114,22 @@ suite "Contracts":
let signer0 = provider.getSigner(accounts[0]) let signer0 = provider.getSigner(accounts[0])
let subscription = await token.subscribe(Transfer, handleTransfer) 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 subscription.unsubscribe()
await token.connect(signer0).transfer(accounts[1], 50.u256) await token.connect(signer0).transfer(accounts[1], 50.u256)
check transfers == @[Transfer(receiver: accounts[0], value: 100.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

View File

@ -1,7 +1,7 @@
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
import pkg/stew/byteutils import pkg/stew/byteutils
import ./examples import ./examples
import ./miner import ./miner