diff --git a/ethers/contract.nim b/ethers/contract.nim index 7ae7d06..d2e68cf 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -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 = diff --git a/ethers/provider.nim b/ethers/provider.nim index bfb6f9d..340ca44 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -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" diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 3940896..37db082 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -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) diff --git a/testmodule/miner.nim b/testmodule/miner.nim new file mode 100644 index 0000000..456c0e1 --- /dev/null +++ b/testmodule/miner.nim @@ -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) diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim index da2715c..508e11b 100644 --- a/testmodule/testContracts.nim +++ b/testmodule/testContracts.nim @@ -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 diff --git a/testmodule/testJsonRpcProvider.nim b/testmodule/testJsonRpcProvider.nim index 9391952..9a2a683 100644 --- a/testmodule/testJsonRpcProvider.nim +++ b/testmodule/testJsonRpcProvider.nim @@ -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