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
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 =

View File

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

View File

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

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/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

View File

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