diff --git a/ethers/contract.nim b/ethers/contract.nim index 799a994..7ae7d06 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -72,13 +72,17 @@ proc call(contract: Contract, let response = await contract.provider.call(transaction, blockTag) return decodeResponse(ReturnType, response) -proc send(contract: Contract, function: string, parameters: tuple) {.async.} = +proc send(contract: Contract, function: string, parameters: tuple): + Future[?TransactionResponse] {.async.} = + if signer =? contract.signer: let transaction = createTransaction(contract, function, parameters) let populated = await signer.populateTransaction(transaction) - await signer.sendTransaction(populated) + let txResp = await signer.sendTransaction(populated) + return txResp.some else: await call(contract, function, parameters) + return TransactionResponse.none func getParameterTuple(procedure: NimNode): NimNode = let parameters = procedure[3] @@ -112,7 +116,8 @@ func addContractCall(procedure: var NimNode) = return await call(`contract`, `function`, `parameters`, `returntype`) else: quote: - await send(`contract`, `function`, `parameters`) + # TODO: need to be able to use wait here + discard await send(`contract`, `function`, `parameters`) func addFuture(procedure: var NimNode) = let returntype = procedure[3][0] diff --git a/ethers/provider.nim b/ethers/provider.nim index 42d4c06..bfb6f9d 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -10,21 +10,76 @@ 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 + contractAddress*: ?Address + transactionIndex*: UInt256 + gasUsed*: UInt256 + logsBloom*: seq[byte] + blockHash*: BlockHash + transactionHash*: TransactionHash + logs*: seq[Log] + 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 + method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} = doAssert false, "not implemented" diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 256fcc6..3940896 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -114,6 +114,11 @@ method getTransactionCount*(provider: JsonRpcProvider, Future[UInt256] {.async.} = let client = await provider.client return await client.eth_getTransactionCount(address, blockTag) +method getTransactionReceipt*(provider: JsonRpcProvider, + txHash: TransactionHash): + Future[?TransactionReceipt] {.async.} = + let client = await provider.client + return await client.eth_getTransactionReceipt(txHash) method estimateGas*(provider: JsonRpcProvider, transaction: Transaction): Future[UInt256] {.async.} = @@ -184,6 +189,76 @@ method signMessage*(signer: JsonRpcSigner, return await client.eth_sign(address, message) method sendTransaction*(signer: JsonRpcSigner, - transaction: Transaction) {.async.} = - let client = await signer.provider.client - discard await client.eth_sendTransaction(transaction) + transaction: Transaction): Future[TransactionResponse] {.async.} = + let + client = await signer.provider.client + hash = await client.eth_sendTransaction(transaction) + + return TransactionResponse(hash: hash, provider: signer.provider) + +method wait*(tx: TransactionResponse, + wantedConfirms = DEFAULT_CONFIRMATIONS, + timeoutInBlocks = int.none): # will error if tx not mined in x blocks + Future[TransactionReceipt] + {.async, upraises: [JsonRpcProviderError].} = # raises for clarity + + var + receipt: ?TransactionReceipt + subscription: JsonRpcSubscription + + let + provider = JsonRpcProvider(tx.provider) + retFut = newFuture[TransactionReceipt]("wait") + + proc confirmations(receipt: TransactionReceipt, atBlkNum: UInt256): UInt256 = + + var confirms = (atBlkNum - !receipt.blockNumber) + 1 + if confirms <= 0: confirms = 1.u256 + return confirms + + proc newBlock(blk: Block) = + # has been mined, need to check # of confirmations thus far + let confirms = (!receipt).confirmations(blk.number) + if confirms >= wantedConfirms.u256: + # fire and forget + discard subscription.unsubscribe() + retFut.complete(!receipt) + + let startBlock = await provider.getBlockNumber() + + # loop until the tx is mined, or times out (in blocks) if timeout specified + while receipt.isNone: + receipt = await provider.getTransactionReceipt(tx.hash) + if receipt.isSome and (!receipt).blockNumber.isSome: + break + + if timeoutInBlocks.isSome: + let currBlock = await provider.getBlockNumber() + let blocksPassed = (currBlock - startBlock) + 1 + if blocksPassed >= (!timeoutInBlocks).u256: + raiseProviderError("Transaction was not mined in " & + $(!timeoutInBlocks) & " blocks") + + # TODO: should this be set to the current block time? + await sleepAsync(1.seconds) + + # has been mined, need to check # of confirmations thus far + let confirms = (!receipt).confirmations(startBlock) + if confirms >= wantedConfirms.u256: + return !receipt + + else: + let sub = await provider.subscribe(newBlock) + subscription = JsonRpcSubscription(sub) + return (await retFut) + +method wait*(tx: Future[TransactionResponse], + wantedConfirms = DEFAULT_CONFIRMATIONS, + timeoutInBlocks = int.none): + Future[TransactionReceipt] {.async.} = + ## Convenience method that allows wait to be chained to a sendTransaction + ## call, eg: + ## `await signer.sendTransaction(populated).wait(3)` + + let txResp = await tx + return await txResp.wait(wantedConfirms, timeoutInBlocks) diff --git a/ethers/providers/jsonrpc/conversions.nim b/ethers/providers/jsonrpc/conversions.nim index 6e90886..1d93bf3 100644 --- a/ethers/providers/jsonrpc/conversions.nim +++ b/ethers/providers/jsonrpc/conversions.nim @@ -1,4 +1,5 @@ import std/json +import std/strutils import pkg/json_rpc/jsonmarshal import pkg/stew/byteutils import ../../basics @@ -74,3 +75,12 @@ func fromJson*(json: JsonNode, name: string, result: var Log) = fromJson(json["data"], "data", data) fromJson(json["topics"], "topics", topics) result = Log(data: data, topics: topics) + +# TransactionStatus + +func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) = + let val = fromHex[int](json.getStr) + result = TransactionStatus(val) + +func `%`*(status: TransactionStatus): JsonNode = + %(status.int.toHex) diff --git a/ethers/providers/jsonrpc/signatures.nim b/ethers/providers/jsonrpc/signatures.nim index b946209..1d9a88f 100644 --- a/ethers/providers/jsonrpc/signatures.nim +++ b/ethers/providers/jsonrpc/signatures.nim @@ -7,7 +7,8 @@ proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256 proc eth_estimateGas(transaction: Transaction): UInt256 proc eth_chainId(): UInt256 -proc eth_sendTransaction(transaction: Transaction): array[32, byte] +proc eth_sendTransaction(transaction: Transaction): TransactionHash +proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt proc eth_sign(account: Address, message: seq[byte]): seq[byte] proc eth_subscribe(name: string, filter = Filter.none): JsonNode proc eth_unsubscribe(id: JsonNode): bool diff --git a/ethers/signer.nim b/ethers/signer.nim index 0b3aa3b..bf6ea7b 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -20,7 +20,7 @@ method signMessage*(signer: Signer, doAssert false, "not implemented" method sendTransaction*(signer: Signer, - transaction: Transaction) {.base, async.} = + transaction: Transaction): Future[TransactionResponse] {.base, async.} = doAssert false, "not implemented" method getGasPrice*(signer: Signer): Future[UInt256] {.base.} = diff --git a/testmodule/testJsonRpcProvider.nim b/testmodule/testJsonRpcProvider.nim index fd44f9a..9391952 100644 --- a/testmodule/testJsonRpcProvider.nim +++ b/testmodule/testJsonRpcProvider.nim @@ -1,7 +1,10 @@ import std/json import pkg/asynctest import pkg/chronos -import pkg/ethers/providers/jsonrpc +import pkg/ethers #/providers/jsonrpc +import pkg/stew/byteutils +import ./examples +import ./miner suite "JsonRpcProvider": @@ -50,3 +53,42 @@ suite "JsonRpcProvider": check newBlock.timestamp > oldBlock.timestamp check newBlock.hash != oldBlock.hash await subscription.unsubscribe() + + test "can send a transaction": + let signer = provider.getSigner() + let transaction = Transaction.example + let populated = await signer.populateTransaction(transaction) + + let txResp = await signer.sendTransaction(populated) + check txResp.hash.len == 32 and UInt256.fromHex(txResp.hash.toHex) > 0 + + test "can wait for a transaction to be confirmed": + let signer = provider.getSigner() + let transaction = Transaction.example + let populated = await signer.populateTransaction(transaction) + + # must be spawned so we can get newHeads inside of .wait + asyncSpawn provider.mineBlocks(3) + + let receipt = await signer.sendTransaction(populated).wait(3) + 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 + + test "waiting for block to be mined times out": + + # must be spawned so we can get newHeads inside of .wait + asyncSpawn provider.mineBlocks(10) + + let startBlock = await provider.getBlockNumber() + let response = TransactionResponse(hash: TransactionHash.example, + provider: provider) + try: + discard await response.wait(wantedConfirms = 2, + timeoutInBlocks = 5.some) + except JsonRpcProviderError as e: + check e.msg == "Transaction was not mined in 5 blocks" + + let endBlock = await provider.getBlockNumber() + check (endBlock - startBlock) + 1 == 5 # +1 including start block