2022-02-02 15:56:37 +00:00
|
|
|
import std/json
|
|
|
|
import std/tables
|
2022-01-18 11:10:20 +00:00
|
|
|
import std/uri
|
|
|
|
import pkg/json_rpc/rpcclient
|
|
|
|
import ../basics
|
|
|
|
import ../provider
|
2022-01-20 13:39:37 +00:00
|
|
|
import ../signer
|
2022-02-02 16:00:12 +00:00
|
|
|
import ./jsonrpc/rpccalls
|
|
|
|
import ./jsonrpc/conversions
|
2022-01-18 11:10:20 +00:00
|
|
|
|
|
|
|
export basics
|
|
|
|
export provider
|
2022-03-17 09:18:21 +00:00
|
|
|
export conversions
|
2022-01-18 11:10:20 +00:00
|
|
|
|
2022-01-18 13:51:53 +00:00
|
|
|
push: {.upraises: [].}
|
|
|
|
|
2022-01-20 13:39:37 +00:00
|
|
|
type
|
|
|
|
JsonRpcProvider* = ref object of Provider
|
|
|
|
client: Future[RpcClient]
|
2022-05-16 12:40:30 +00:00
|
|
|
subscriptions: Table[JsonNode, SubscriptionHandler]
|
2022-02-02 15:56:37 +00:00
|
|
|
JsonRpcSubscription = ref object of Subscription
|
|
|
|
provider: JsonRpcProvider
|
|
|
|
id: JsonNode
|
2022-01-20 13:39:37 +00:00
|
|
|
JsonRpcSigner* = ref object of Signer
|
|
|
|
provider: JsonRpcProvider
|
|
|
|
address: ?Address
|
2022-01-25 09:25:09 +00:00
|
|
|
JsonRpcProviderError* = object of EthersError
|
2022-05-18 13:14:39 +00:00
|
|
|
SubscriptionHandler = proc(id, arguments: JsonNode): Future[void] {.gcsafe, upraises:[].}
|
2022-01-20 13:39:37 +00:00
|
|
|
|
|
|
|
template raiseProviderError(message: string) =
|
|
|
|
raise newException(JsonRpcProviderError, message)
|
|
|
|
|
|
|
|
# Provider
|
2022-01-18 11:10:20 +00:00
|
|
|
|
|
|
|
const defaultUrl = "http://localhost:8545"
|
|
|
|
|
|
|
|
proc connect(_: type RpcClient, url: string): Future[RpcClient] {.async.} =
|
|
|
|
case parseUri(url).scheme
|
|
|
|
of "ws", "wss":
|
|
|
|
let client = newRpcWebSocketClient()
|
|
|
|
await client.connect(url)
|
|
|
|
return client
|
|
|
|
else:
|
|
|
|
let client = newRpcHttpClient()
|
|
|
|
await client.connect(url)
|
|
|
|
return client
|
|
|
|
|
2022-04-19 15:48:27 +00:00
|
|
|
proc connect(provider: JsonRpcProvider, url: string) =
|
|
|
|
|
2022-05-16 12:40:30 +00:00
|
|
|
proc getSubscriptionHandler(id: JsonNode): ?SubscriptionHandler =
|
2022-04-19 15:48:27 +00:00
|
|
|
try:
|
2022-05-16 12:40:30 +00:00
|
|
|
if provider.subscriptions.hasKey(id):
|
|
|
|
provider.subscriptions[id].some
|
2022-04-19 15:48:27 +00:00
|
|
|
else:
|
2022-05-16 12:40:30 +00:00
|
|
|
SubscriptionHandler.none
|
2022-04-19 15:48:27 +00:00
|
|
|
except Exception:
|
2022-05-16 12:40:30 +00:00
|
|
|
SubscriptionHandler.none
|
2022-04-19 15:48:27 +00:00
|
|
|
|
|
|
|
proc handleSubscription(arguments: JsonNode) {.upraises: [].} =
|
|
|
|
if id =? arguments["subscription"].catch and
|
2022-05-16 12:40:30 +00:00
|
|
|
handler =? getSubscriptionHandler(id):
|
2022-05-18 13:14:39 +00:00
|
|
|
# fire and forget
|
|
|
|
discard handler(id, arguments)
|
2022-04-19 15:48:27 +00:00
|
|
|
|
|
|
|
proc subscribe: Future[RpcClient] {.async.} =
|
|
|
|
let client = await RpcClient.connect(url)
|
|
|
|
client.setMethodHandler("eth_subscription", handleSubscription)
|
|
|
|
return client
|
|
|
|
|
|
|
|
provider.client = subscribe()
|
2022-02-02 15:56:37 +00:00
|
|
|
|
2022-01-18 11:10:20 +00:00
|
|
|
proc new*(_: type JsonRpcProvider, url=defaultUrl): JsonRpcProvider =
|
2022-04-19 15:48:27 +00:00
|
|
|
let provider = JsonRpcProvider()
|
|
|
|
provider.connect(url)
|
2022-02-02 15:56:37 +00:00
|
|
|
provider
|
2022-01-18 11:10:20 +00:00
|
|
|
|
2022-01-18 13:24:46 +00:00
|
|
|
proc send*(provider: JsonRpcProvider,
|
|
|
|
call: string,
|
2022-01-25 14:05:54 +00:00
|
|
|
arguments: seq[JsonNode] = @[]): Future[JsonNode] {.async.} =
|
2022-01-18 13:24:46 +00:00
|
|
|
let client = await provider.client
|
2022-01-25 14:05:54 +00:00
|
|
|
return await client.call(call, %arguments)
|
2022-01-18 13:24:46 +00:00
|
|
|
|
2022-01-18 11:10:20 +00:00
|
|
|
proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] {.async.} =
|
|
|
|
let client = await provider.client
|
2022-01-18 11:42:58 +00:00
|
|
|
return await client.eth_accounts()
|
2022-01-18 13:26:41 +00:00
|
|
|
|
2022-01-20 13:39:37 +00:00
|
|
|
proc getSigner*(provider: JsonRpcProvider): JsonRpcSigner =
|
|
|
|
JsonRpcSigner(provider: provider)
|
|
|
|
|
|
|
|
proc getSigner*(provider: JsonRpcProvider, address: Address): JsonRpcSigner =
|
|
|
|
JsonRpcSigner(provider: provider, address: some address)
|
|
|
|
|
2022-01-18 13:26:41 +00:00
|
|
|
method getBlockNumber*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
return await client.eth_blockNumber()
|
2022-01-20 11:56:18 +00:00
|
|
|
|
2022-03-16 13:02:44 +00:00
|
|
|
method getBlock*(provider: JsonRpcProvider,
|
|
|
|
tag: BlockTag): Future[?Block] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
return await client.eth_getBlockByNumber(tag, false)
|
|
|
|
|
2022-01-20 11:56:18 +00:00
|
|
|
method call*(provider: JsonRpcProvider,
|
2022-04-10 20:21:59 +00:00
|
|
|
tx: Transaction,
|
|
|
|
blockTag = BlockTag.latest): Future[seq[byte]] {.async.} =
|
2022-01-20 11:56:18 +00:00
|
|
|
let client = await provider.client
|
2022-04-10 20:21:59 +00:00
|
|
|
return await client.eth_call(tx, blockTag)
|
2022-01-20 13:39:37 +00:00
|
|
|
|
2022-01-24 11:12:52 +00:00
|
|
|
method getGasPrice*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
return await client.eth_gasprice()
|
|
|
|
|
2022-01-24 11:14:31 +00:00
|
|
|
method getTransactionCount*(provider: JsonRpcProvider,
|
|
|
|
address: Address,
|
|
|
|
blockTag = BlockTag.latest):
|
|
|
|
Future[UInt256] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
return await client.eth_getTransactionCount(address, blockTag)
|
2022-05-18 13:14:39 +00:00
|
|
|
|
2022-05-17 02:34:22 +00:00
|
|
|
method getTransactionReceipt*(provider: JsonRpcProvider,
|
|
|
|
txHash: TransactionHash):
|
|
|
|
Future[?TransactionReceipt] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
return await client.eth_getTransactionReceipt(txHash)
|
2022-01-24 11:14:31 +00:00
|
|
|
|
2022-01-24 13:40:47 +00:00
|
|
|
method estimateGas*(provider: JsonRpcProvider,
|
|
|
|
transaction: Transaction): Future[UInt256] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
return await client.eth_estimateGas(transaction)
|
|
|
|
|
2022-01-24 16:29:25 +00:00
|
|
|
method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
try:
|
|
|
|
return await client.eth_chainId()
|
|
|
|
except CatchableError:
|
|
|
|
return parse(await client.net_version(), UInt256)
|
|
|
|
|
2022-05-16 12:51:39 +00:00
|
|
|
proc subscribe(provider: JsonRpcProvider,
|
|
|
|
name: string,
|
|
|
|
filter: ?Filter,
|
|
|
|
handler: SubscriptionHandler): Future[Subscription] {.async.} =
|
|
|
|
let client = await provider.client
|
|
|
|
doAssert client of RpcWebSocketClient, "subscriptions require websockets"
|
|
|
|
|
|
|
|
let id = await client.eth_subscribe(name, filter)
|
|
|
|
provider.subscriptions[id] = handler
|
|
|
|
|
|
|
|
return JsonRpcSubscription(id: id, provider: provider)
|
|
|
|
|
2022-02-02 15:56:37 +00:00
|
|
|
method subscribe*(provider: JsonRpcProvider,
|
|
|
|
filter: Filter,
|
|
|
|
callback: LogHandler):
|
|
|
|
Future[Subscription] {.async.} =
|
2022-05-18 13:14:39 +00:00
|
|
|
proc handler(id, arguments: JsonNode) {.async.} =
|
2022-05-16 12:40:30 +00:00
|
|
|
if log =? Log.fromJson(arguments["result"]).catch:
|
|
|
|
callback(log)
|
2022-05-16 12:51:39 +00:00
|
|
|
return await provider.subscribe("logs", filter.some, handler)
|
2022-05-16 12:40:30 +00:00
|
|
|
|
2022-05-16 12:51:39 +00:00
|
|
|
method subscribe*(provider: JsonRpcProvider,
|
|
|
|
callback: BlockHandler):
|
|
|
|
Future[Subscription] {.async.} =
|
2022-05-18 13:14:39 +00:00
|
|
|
proc handler(id, arguments: JsonNode) {.async.} =
|
2022-05-16 12:51:39 +00:00
|
|
|
if blck =? Block.fromJson(arguments["result"]).catch:
|
2022-05-18 13:14:39 +00:00
|
|
|
await callback(blck)
|
2022-05-16 12:51:39 +00:00
|
|
|
return await provider.subscribe("newHeads", Filter.none, handler)
|
2022-02-02 15:56:37 +00:00
|
|
|
|
|
|
|
method unsubscribe*(subscription: JsonRpcSubscription) {.async.} =
|
|
|
|
let provider = subscription.provider
|
2022-05-17 17:10:58 +00:00
|
|
|
provider.subscriptions.del(subscription.id)
|
2022-02-02 15:56:37 +00:00
|
|
|
let client = await provider.client
|
|
|
|
discard await client.eth_unsubscribe(subscription.id)
|
|
|
|
|
2022-01-20 13:39:37 +00:00
|
|
|
# Signer
|
|
|
|
|
2022-01-24 11:12:52 +00:00
|
|
|
method provider*(signer: JsonRpcSigner): Provider =
|
|
|
|
signer.provider
|
|
|
|
|
2022-01-20 13:39:37 +00:00
|
|
|
method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
|
|
|
|
if address =? signer.address:
|
|
|
|
return address
|
|
|
|
|
|
|
|
let accounts = await signer.provider.listAccounts()
|
|
|
|
if accounts.len > 0:
|
|
|
|
return accounts[0]
|
|
|
|
|
|
|
|
raiseProviderError "no address found"
|
2022-01-25 16:17:43 +00:00
|
|
|
|
2022-01-26 10:21:28 +00:00
|
|
|
method signMessage*(signer: JsonRpcSigner,
|
|
|
|
message: seq[byte]): Future[seq[byte]] {.async.} =
|
|
|
|
let client = await signer.provider.client
|
|
|
|
let address = await signer.getAddress()
|
|
|
|
return await client.eth_sign(address, message)
|
|
|
|
|
2022-01-25 16:17:43 +00:00
|
|
|
method sendTransaction*(signer: JsonRpcSigner,
|
2022-05-17 02:34:22 +00:00
|
|
|
transaction: Transaction): Future[TransactionResponse] {.async.} =
|
|
|
|
let
|
|
|
|
client = await signer.provider.client
|
|
|
|
hash = await client.eth_sendTransaction(transaction)
|
|
|
|
|
|
|
|
return TransactionResponse(hash: hash, provider: signer.provider)
|
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
|
|
|
|
# Removed from `confirm` closure and exported so it can be tested.
|
|
|
|
# Likely there is a better way
|
|
|
|
func confirmations*(receiptBlk, atBlk: UInt256): UInt256 =
|
|
|
|
## Calculates the number of confirmations between two blocks
|
|
|
|
if atBlk < receiptBlk:
|
|
|
|
return 0.u256
|
|
|
|
else:
|
|
|
|
return (atBlk - receiptBlk) + 1 # add 1 for current block
|
|
|
|
|
|
|
|
# Removed from `confirm` closure and exported so it can be tested.
|
|
|
|
# Likely there is a better way
|
|
|
|
func hasBeenMined*(receipt: ?TransactionReceipt,
|
|
|
|
atBlock: UInt256,
|
|
|
|
wantedConfirms: int): bool =
|
|
|
|
## Returns true if the transaction receipt has been returned from the node
|
|
|
|
## with a valid block number and block hash and the specified number of
|
|
|
|
## blocks have passed since the tx was mined (confirmations)
|
|
|
|
|
|
|
|
if receipt.isSome and
|
|
|
|
receipt.get.blockNumber.isSome and
|
|
|
|
receipt.get.blockNumber.get > 0 and
|
|
|
|
# from ethers.js: "geth-etc" returns receipts before they are ready
|
|
|
|
receipt.get.blockHash.isSome:
|
|
|
|
|
|
|
|
let receiptBlock = receipt.get.blockNumber.get
|
|
|
|
return receiptBlock.confirmations(atBlock) >= wantedConfirms.u256
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
method confirm*(tx: TransactionResponse,
|
|
|
|
wantedConfirms: Positive = EthersDefaultConfirmations,
|
|
|
|
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
2022-05-17 02:34:22 +00:00
|
|
|
Future[TransactionReceipt]
|
|
|
|
{.async, upraises: [JsonRpcProviderError].} = # raises for clarity
|
2022-05-18 13:14:39 +00:00
|
|
|
## Waits for a transaction to be mined and for the specified number of blocks
|
|
|
|
## to pass since it was mined (confirmations).
|
|
|
|
## A timeout, in blocks, can be specified that will raise a
|
|
|
|
## JsonRpcProviderError if too many blocks have passed without the tx
|
|
|
|
## having been mined.
|
|
|
|
## Note: this method requires that the JsonRpcProvider client connects
|
|
|
|
## using RpcWebSocketClient, otherwise it will raise a Defect.
|
|
|
|
|
|
|
|
var subscription: JsonRpcSubscription
|
2022-05-17 02:34:22 +00:00
|
|
|
let
|
|
|
|
provider = JsonRpcProvider(tx.provider)
|
|
|
|
retFut = newFuture[TransactionReceipt]("wait")
|
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
# used to check for block timeouts
|
2022-05-17 02:34:22 +00:00
|
|
|
let startBlock = await provider.getBlockNumber()
|
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
proc newBlock(blk: Block) {.async.} =
|
|
|
|
## subscription callback, called every time a new block event is sent from
|
|
|
|
## the node
|
2022-05-17 02:34:22 +00:00
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
# if ethereum node doesn't include blockNumber in the event
|
|
|
|
without blkNum =? blk.number:
|
|
|
|
return
|
2022-05-17 02:34:22 +00:00
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
let receipt = await provider.getTransactionReceipt(tx.hash)
|
|
|
|
if receipt.hasBeenMined(blkNum, wantedConfirms):
|
|
|
|
# fire and forget
|
|
|
|
discard subscription.unsubscribe()
|
|
|
|
retFut.complete(receipt.get)
|
|
|
|
|
|
|
|
elif timeoutInBlocks > 0:
|
|
|
|
let blocksPassed = (blkNum - startBlock) + 1
|
|
|
|
if blocksPassed >= timeoutInBlocks.u256:
|
|
|
|
discard subscription.unsubscribe()
|
|
|
|
retFut.fail(
|
|
|
|
newException(JsonRpcProviderError, "Transaction was not mined in " &
|
|
|
|
$timeoutInBlocks & " blocks"))
|
|
|
|
|
|
|
|
# If our tx is already mined, return the receipt. Otherwise, check each
|
|
|
|
# new block to see if the tx has been mined
|
|
|
|
let receipt = await provider.getTransactionReceipt(tx.hash)
|
|
|
|
if receipt.hasBeenMined(startBlock, wantedConfirms):
|
|
|
|
return receipt.get
|
2022-05-17 02:34:22 +00:00
|
|
|
else:
|
|
|
|
let sub = await provider.subscribe(newBlock)
|
|
|
|
subscription = JsonRpcSubscription(sub)
|
|
|
|
return (await retFut)
|
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
method confirm*(tx: Future[TransactionResponse],
|
|
|
|
wantedConfirms: Positive = EthersDefaultConfirmations,
|
|
|
|
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
2022-05-17 02:34:22 +00:00
|
|
|
Future[TransactionReceipt] {.async.} =
|
|
|
|
## Convenience method that allows wait to be chained to a sendTransaction
|
|
|
|
## call, eg:
|
2022-05-18 13:14:39 +00:00
|
|
|
## `await signer.sendTransaction(populated).confirm(3)`
|
2022-05-17 02:34:22 +00:00
|
|
|
|
|
|
|
let txResp = await tx
|
2022-05-18 13:14:39 +00:00
|
|
|
return await txResp.confirm(wantedConfirms, timeoutInBlocks)
|
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
```
2022-05-17 04:57:18 +00:00
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
method confirm*(tx: Future[?TransactionResponse],
|
|
|
|
wantedConfirms: Positive = EthersDefaultConfirmations,
|
|
|
|
timeoutInBlocks: Natural = EthersReceiptTimeoutBlks):
|
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
```
2022-05-17 04:57:18 +00:00
|
|
|
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)
|
2022-05-18 13:14:39 +00:00
|
|
|
## .confirm(3)`
|
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
```
2022-05-17 04:57:18 +00:00
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
without txResp =? (await tx):
|
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
```
2022-05-17 04:57:18 +00:00
|
|
|
raiseProviderError("Transaction hash required. Possibly was a call instead of a send?")
|
|
|
|
|
2022-05-18 13:14:39 +00:00
|
|
|
return await txResp.confirm(wantedConfirms, timeoutInBlocks)
|