mirror of
https://github.com/status-im/nim-ethers.git
synced 2025-02-20 02:58:09 +00:00
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
|
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 =
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
8
testmodule/miner.nim
Normal 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)
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user