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-16 12:40:30 +00:00
|
|
|
SubscriptionHandler = proc(id, arguments: JsonNode) {.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):
|
|
|
|
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-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-16 12:40:30 +00:00
|
|
|
proc handler(id, arguments: JsonNode) =
|
|
|
|
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.} =
|
|
|
|
proc handler(id, arguments: JsonNode) =
|
|
|
|
if blck =? Block.fromJson(arguments["result"]).catch:
|
|
|
|
callback(blck)
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|