First commit

This commit is contained in:
coffeepots 2018-03-02 11:46:59 +00:00
parent 00852b774b
commit bc375ad817
15 changed files with 687 additions and 0 deletions

View File

@ -0,0 +1,110 @@
import asyncnet, asyncdispatch, tables, json, oids, ethcalls, macros
type
RpcClient* = ref object
socket: AsyncSocket
awaiting: Table[string, Future[Response]]
address: string
port: Port
Response* = tuple[error: bool, result: JsonNode]
proc newRpcClient*(): RpcClient =
## Creates a new ``RpcClient`` instance.
RpcClient(
socket: newAsyncSocket(),
awaiting: initTable[string, Future[Response]]()
)
proc call*(self: RpcClient, name: string, params: JsonNode): Future[Response] {.async.} =
## Remotely calls the specified RPC method.
let id = $genOid() # TODO: finalise approach
let msg = %{"jsonrpc": %"2.0", "method": %name, "params": params, "id": %id}
await self.socket.send($msg & "\c\l")
# This Future is completed by processMessage.
var idFut = newFuture[Response]()
self.awaiting[id] = idFut # add to awaiting responses
result = await idFut
proc isNull(node: JsonNode): bool = node.kind == JNull
proc processMessage(self: RpcClient, line: string) =
let node = parseJson(line)
assert node.hasKey("jsonrpc")
assert node["jsonrpc"].str == "2.0"
assert node.hasKey("id")
assert self.awaiting.hasKey(node["id"].str)
if node["error"].kind == JNull:
self.awaiting[node["id"].str].complete((false, node["result"]))
self.awaiting.del(node["id"].str)
else:
if not node["id"].isNull:
# If the node id is null, we cannot complete the future.
self.awaiting[node["id"].str].complete((true, node["error"]))
# TODO: Safe to delete here?
self.awaiting.del(node["id"].str)
proc connect*(self: RpcClient, address: string, port: Port): Future[void]
proc processData(self: RpcClient) {.async.} =
while true:
# read until no data
let line = await self.socket.recvLine()
if line == "":
# transmission ends
self.socket.close() # TODO: Do we need to drop/reacquire sockets?
self.socket = newAsyncSocket()
break
processMessage(self, line)
# async loop reconnection and waiting
await connect(self, self.address, self.port)
proc connect*(self: RpcClient, address: string, port: Port) {.async.} =
await self.socket.connect(address, port)
self.address = address
self.port = port
asyncCheck processData(self)
proc makeTemplate(name: string, params: NimNode, body: NimNode, starred: bool = false): NimNode =
# set up template AST
result = newNimNode(nnkTemplateDef)
if starred:
var nPostFix = newNimNode(nnkPostFix)
nPostFix.add ident("*"), ident(name)
result.add nPostFix
else:
result.add ident(name)
result.add newEmptyNode(), newEmptyNode(), params, newEmptyNode(), newEmptyNode(), body
proc appendFormalParam(formalParams: NimNode, identName, typeName: string) =
# set up formal params AST
formalParams.expectKind(nnkFormalParams)
if formalParams.len == 0: formalParams.add newEmptyNode()
var identDef = newIdentDefs(ident(identName), ident(typeName))
formalParams.add identDef
macro generateCalls: untyped =
## Generate templates for client calls so that:
## client.call("web3_clientVersion", params)
## can be written as:
## client.web3_clientVersion(params)
result = newStmtList()
for callName in ETHEREUM_RPC_CALLS:
var
# TODO: Use macros.newProc
params = newNimNode(nnkFormalParams)
call = newNimNode(nnkCall)
body = newStmtList().add call
templ = makeTemplate(callName, params, body, true)
call.add newDotExpr(ident("client"), ident("call")), newStrLitNode(callName), ident("params")
params.add newNimNode(nnkBracketExpr).add(ident("Future"), ident("Response"))
params.appendFormalParam("client", "RpcClient")
params.appendFormalParam("params", "JsonNode")
result.add templ
# generate all client ethereum rpc calls
generateCalls()

66
src/client/ethcalls.nim Normal file
View File

@ -0,0 +1,66 @@
const
ETHEREUM_RPC_CALLS* = [
"web3_clientVersion",
"web3_sha3",
"net_version",
"net_peerCount",
"net_listening",
"eth_protocolVersion",
"eth_syncing",
"eth_coinbase",
"eth_mining",
"eth_hashrate",
"eth_gasPrice",
"eth_accounts",
"eth_blockNumber",
"eth_getBalance",
"eth_getStorageAt",
"eth_getTransactionCount",
"eth_getBlockTransactionCountByHash",
"eth_getBlockTransactionCountByNumber",
"eth_getUncleCountByBlockHash",
"eth_getUncleCountByBlockNumber",
"eth_getCode",
"eth_sign",
"eth_sendTransaction",
"eth_sendRawTransaction",
"eth_call",
"eth_estimateGas",
"eth_getBlockByHash",
"eth_getBlockByNumber",
"eth_getTransactionByHash",
"eth_getTransactionByBlockHashAndIndex",
"eth_getTransactionByBlockNumberAndIndex",
"eth_getTransactionReceipt",
"eth_getUncleByBlockHashAndIndex",
"eth_getUncleByBlockNumberAndIndex",
"eth_getCompilers",
"eth_compileLLL",
"eth_compileSolidity",
"eth_compileSerpent",
"eth_newFilter",
"eth_newBlockFilter",
"eth_newPendingTransactionFilter",
"eth_uninstallFilter",
"eth_getFilterChanges",
"eth_getFilterLogs",
"eth_getLogs",
"eth_getWork",
"eth_submitWork",
"eth_submitHashrate",
"db_putString",
"db_getString",
"db_putHex",
"db_getHex",
"shh_post",
"shh_version",
"shh_newIdentity",
"shh_hasIdentity",
"shh_newGroup",
"shh_addToGroup",
"shh_newFilter",
"shh_uninstallFilter",
"shh_getFilterChanges",
"shh_getMessages"
]

3
src/rpcclient.nim Normal file
View File

@ -0,0 +1,3 @@
import client / clientdispatch
export clientdispatch

2
src/rpcserver.nim Normal file
View File

@ -0,0 +1,2 @@
import server / [servertypes, rpcconsts, serverdispatch]
export servertypes, rpcconsts, serverdispatch

15
src/server/asyncutils.nim Normal file
View File

@ -0,0 +1,15 @@
import json, asyncdispatch, asyncnet, jsonutils, private / debugutils
proc wrapReply*(id: JsonNode, value: JsonNode, error: JsonNode): JsonNode =
return %{"jsonrpc": %"2.0", "result": value, "error": error, "id": id}
proc sendError*(client: AsyncSocket, code: int, msg: string, id: JsonNode, data: JsonNode = newJNull()) {.async.} =
## Send error message to client
let error = %{"code": %(code), "message": %msg, "data": data}
ifDebug: echo "Send error json: ", wrapReply(newJNull(), error, id).pretty & "\c\l"
result = client.send($wrapReply(id, newJNull(), error) & "\c\l")
proc sendJsonError*(state: RpcJsonError, client: AsyncSocket, id: JsonNode, data = newJNull()) {.async.} =
## Send client response for invalid json state
let errMsgs = jsonErrorMessages[state]
await client.sendError(errMsgs[0], errMsgs[1], id, data)

38
src/server/jsonutils.nim Normal file
View File

@ -0,0 +1,38 @@
import rpcconsts, options, json
type
RpcJsonError* = enum rjeInvalidJson, rjeVersionError, rjeNoMethod, rjeNoId
RpcJsonErrorContainer* = tuple[err: RpcJsonError, msg: string]
const
jsonErrorMessages*: array[RpcJsonError, (int, string)] =
[
(JSON_PARSE_ERROR, "Invalid JSON"),
(INVALID_REQUEST, "JSON 2.0 required"),
(INVALID_REQUEST, "No method requested"),
(INVALID_REQUEST, "No id specified")
]
template jsonValid*(jsonString: string, node: var JsonNode): (bool, string) =
var
valid = true
msg = ""
try: node = parseJson(line)
except:
valid = false
msg = getCurrentExceptionMsg()
(valid, msg)
proc checkJsonErrors*(line: string, node: var JsonNode): Option[RpcJsonErrorContainer] =
## Tries parsing line into node, if successful checks required fields
## Returns: error state or none
let res = jsonValid(line, node)
if not res[0]:
return some((rjeInvalidJson, res[1]))
if not node.hasKey("jsonrpc"):
return some((rjeVersionError, ""))
if not node.hasKey("method"):
return some((rjeNoMethod, ""))
if not node.hasKey("id"):
return some((rjeNoId, ""))
return none(RpcJsonErrorContainer)

View File

@ -0,0 +1,2 @@
template ifDebug*(actions: untyped): untyped =
when not defined(release): actions else: discard

View File

@ -0,0 +1,43 @@
from asyncdispatch import Port
proc `$`*(port: Port): string = $int(port)
iterator bytes*[T: SomeUnsignedInt](value: T): byte {.inline.} =
## Traverse the bytes of a value in little endian
let argSize = sizeOf(T)
for bIdx in 0 ..< argSize:
let
shift = bIdx.uint * 8
mask = 0xff'u64 shl shift
yield byte((value and mask) shr shift)
iterator bytePairs*[T: SomeUnsignedInt](value: T): tuple[key: int, val: byte] {.inline.} =
let argSize = sizeOf(T)
for bIdx in 0 ..< argSize:
let
shift = bIdx.uint * 8
mask = 0xff'u64 shl shift
yield (bIdx, byte((value and mask) shr shift))
template stripLeadingZeros(value: string): string =
var cidx = 0
# ignore the last character so we retain '0' on zero value
while cidx < value.len - 1 and value[cidx] == '0':
cidx.inc
value[cidx .. ^1]
proc encodeQuantity*(value: SomeUnsignedInt): string =
var hValue = value.toHex.stripLeadingZeros
result = "0x" & hValue
proc encodeData*[T: SomeUnsignedInt](values: seq[T]): string =
## Translates seq of values to hex string
let argSize = sizeOf(T)
result = newString((values.len * argSize) * 2 + 2) # reserve 2 bytes for "0x"
result[0..1] = "0x"
var cPos = 0
for idx, value in values:
for bValue in values[idx].bytes:
result[cPos .. cPos + 1] = bValue.int.toHex(2)
cPos = cPos + 2

7
src/server/rpcconsts.nim Normal file
View File

@ -0,0 +1,7 @@
const
JSON_PARSE_ERROR* = -32700
INVALID_REQUEST* = -32600
METHOD_NOT_FOUND* = -32601
INVALID_PARAMS* = -32602
INTERNAL_ERROR* = -32603
SERVER_ERROR* = -32000

278
src/server/rpcprocs.nim Normal file
View File

@ -0,0 +1,278 @@
import servertypes, json, asyncdispatch, macros
macro rpc*(prc: untyped): untyped =
result = prc
let params = prc.findChild(it.kind == nnkFormalParams)
assert params != nil
for param in params.children:
if param.kind == nnkIdentDefs:
if param[1] == ident("JsonNode"):
return
var identDefs = newNimNode(nnkIdentDefs)
identDefs.add ident("params"), ident("JsonNode"), newEmptyNode()
# proc result
# check there isn't already a result type
assert params.len == 1 and params[0].kind == nnkEmpty
params[0] = ident("JsonNode")
params.add identDefs
# finally, register with server's table.
# this requires a server variable to be passed somehow
#var body = prc.findChild(it.kind == nnkStmtList)
#body.add(quote do:
# server.register "web3_clientVersion", web3_clientVersion
#)
proc web3_clientVersion {.rpc.} =
return %("Nimbus-RPC-Test")
proc web3_sha3 {.rpc.} =
discard
proc net_version {.rpc.} =
discard
proc net_peerCount {.rpc.} =
discard
proc net_listening {.rpc.} =
discard
proc eth_protocolVersion {.rpc.} =
discard
proc eth_syncing {.rpc.} =
discard
proc eth_coinbase {.rpc.} =
discard
proc eth_mining {.rpc.} =
discard
proc eth_hashrate {.rpc.} =
discard
proc eth_gasPrice {.rpc.} =
discard
proc eth_accounts {.rpc.} =
discard
proc eth_blockNumber {.rpc.} =
discard
proc eth_getBalance {.rpc.} =
discard
proc eth_getStorageAt {.rpc.} =
discard
proc eth_getTransactionCount {.rpc.} =
discard
proc eth_getBlockTransactionCountByHash {.rpc.} =
discard
proc eth_getBlockTransactionCountByNumber {.rpc.} =
discard
proc eth_getUncleCountByBlockHash {.rpc.} =
discard
proc eth_getUncleCountByBlockNumber {.rpc.} =
discard
proc eth_getCode {.rpc.} =
discard
proc eth_sign {.rpc.} =
discard
proc eth_sendTransaction {.rpc.} =
discard
proc eth_sendRawTransaction {.rpc.} =
discard
proc eth_call {.rpc.} =
discard
proc eth_estimateGas {.rpc.} =
discard
proc eth_getBlockByHash {.rpc.} =
discard
proc eth_getBlockByNumber {.rpc.} =
discard
proc eth_getTransactionByHash {.rpc.} =
discard
proc eth_getTransactionByBlockHashAndIndex {.rpc.} =
discard
proc eth_getTransactionByBlockNumberAndIndex {.rpc.} =
discard
proc eth_getTransactionReceipt {.rpc.} =
discard
proc eth_getUncleByBlockHashAndIndex {.rpc.} =
discard
proc eth_getUncleByBlockNumberAndIndex {.rpc.} =
discard
proc eth_getCompilers {.rpc.} =
discard
proc eth_compileLLL {.rpc.} =
discard
proc eth_compileSolidity {.rpc.} =
discard
proc eth_compileSerpent {.rpc.} =
discard
proc eth_newFilter {.rpc.} =
discard
proc eth_newBlockFilter {.rpc.} =
discard
proc eth_newPendingTransactionFilter {.rpc.} =
discard
proc eth_uninstallFilter {.rpc.} =
discard
proc eth_getFilterChanges {.rpc.} =
discard
proc eth_getFilterLogs {.rpc.} =
discard
proc eth_getLogs {.rpc.} =
discard
proc eth_getWork {.rpc.} =
discard
proc eth_submitWork {.rpc.} =
discard
proc eth_submitHashrate {.rpc.} =
discard
proc db_putString {.rpc.} =
discard
proc db_getString {.rpc.} =
discard
proc db_putHex {.rpc.} =
discard
proc db_getHex {.rpc.} =
discard
proc shh_post {.rpc.} =
discard
proc shh_version {.rpc.} =
discard
proc shh_newIdentity {.rpc.} =
discard
proc shh_hasIdentity {.rpc.} =
discard
proc shh_newGroup {.rpc.} =
discard
proc shh_addToGroup {.rpc.} =
discard
proc shh_newFilter {.rpc.} =
discard
proc shh_uninstallFilter {.rpc.} =
discard
proc shh_getFilterChanges {.rpc.} =
discard
proc shh_getMessages {.rpc.} =
discard
proc registerEthereumRpcs*(server: RpcServer) =
## Register all ethereum rpc calls to the server
# TODO: Automate this
server.register "web3_clientVersion", web3_clientVersion
server.register "web3_sha3", web3_sha3
server.register "net_version", net_version
server.register "net_peerCount", net_peerCount
server.register "net_listening", net_listening
server.register "eth_protocolVersion", eth_protocolVersion
server.register "eth_syncing", eth_syncing
server.register "eth_coinbase", eth_coinbase
server.register "eth_mining", eth_mining
server.register "eth_hashrate", eth_hashrate
server.register "eth_gasPrice", eth_gasPrice
server.register "eth_accounts", eth_accounts
server.register "eth_blockNumber", eth_blockNumber
server.register "eth_getBalance", eth_getBalance
server.register "eth_getStorageAt", eth_getStorageAt
server.register "eth_getTransactionCount", eth_getTransactionCount
server.register "eth_getBlockTransactionCountByHash", eth_getBlockTransactionCountByHash
server.register "eth_getBlockTransactionCountByNumber", eth_getBlockTransactionCountByNumber
server.register "eth_getUncleCountByBlockHash", eth_getUncleCountByBlockHash
server.register "eth_getUncleCountByBlockNumber", eth_getUncleCountByBlockNumber
server.register "eth_getCode", eth_getCode
server.register "eth_sign", eth_sign
server.register "eth_sendTransaction", eth_sendTransaction
server.register "eth_sendRawTransaction", eth_sendRawTransaction
server.register "eth_call", eth_call
server.register "eth_estimateGas", eth_estimateGas
server.register "eth_getBlockByHash", eth_getBlockByHash
server.register "eth_getBlockByNumber", eth_getBlockByNumber
server.register "eth_getTransactionByHash", eth_getTransactionByHash
server.register "eth_getTransactionByBlockHashAndIndex", eth_getTransactionByBlockHashAndIndex
server.register "eth_getTransactionByBlockNumberAndIndex", eth_getTransactionByBlockNumberAndIndex
server.register "eth_getTransactionReceipt", eth_getTransactionReceipt
server.register "eth_getUncleByBlockHashAndIndex", eth_getUncleByBlockHashAndIndex
server.register "eth_getUncleByBlockNumberAndIndex", eth_getUncleByBlockNumberAndIndex
server.register "eth_getCompilers", eth_getCompilers
server.register "eth_compileLLL", eth_compileLLL
server.register "eth_compileSolidity", eth_compileSolidity
server.register "eth_compileSerpent", eth_compileSerpent
server.register "eth_newFilter", eth_newFilter
server.register "eth_newBlockFilter", eth_newBlockFilter
server.register "eth_newPendingTransactionFilter", eth_newPendingTransactionFilter
server.register "eth_uninstallFilter", eth_uninstallFilter
server.register "eth_getFilterChanges", eth_getFilterChanges
server.register "eth_getFilterLogs", eth_getFilterLogs
server.register "eth_getLogs", eth_getLogs
server.register "eth_getWork", eth_getWork
server.register "eth_submitWork", eth_submitWork
server.register "eth_submitHashrate", eth_submitHashrate
server.register "db_putString", db_putString
server.register "db_getString", db_getString
server.register "db_putHex", db_putHex
server.register "db_getHex", db_getHex
server.register "shh_post", shh_post
server.register "shh_version", shh_version
server.register "shh_newIdentity", shh_newIdentity
server.register "shh_hasIdentity", shh_hasIdentity
server.register "shh_newGroup", shh_newGroup
server.register "shh_addToGroup", shh_addToGroup
server.register "shh_newFilter", shh_newFilter
server.register "shh_uninstallFilter", shh_uninstallFilter
server.register "shh_getFilterChanges", shh_getFilterChanges
server.register "shh_getMessages", shh_getMessages

View File

@ -0,0 +1,58 @@
import asyncdispatch, asyncnet, json, tables, strutils,
servertypes, rpcconsts, private / [transportutils, debugutils], jsonutils, asyncutils, rpcprocs,
options
proc processMessage(server: RpcServer, client: AsyncSocket, line: string) {.async.} =
var
node: JsonNode
jsonErrorState = checkJsonErrors(line, node) # set up node and/or flag errors
if jsonErrorState.isSome:
let errState = jsonErrorState.get
var id: JsonNode
if errState.err == rjeInvalidJson: id = newJNull() # id cannot be retrieved
else: id = node["id"]
await errState.err.sendJsonError(client, id, %errState.msg)
else:
let
methodName = node["method"].str
id = node["id"]
if not server.procs.hasKey(methodName):
await client.sendError(METHOD_NOT_FOUND, "Method not found", id, %(methodName & " is not a registered method."))
else:
# TODO: Performance or other effects from NOT calling rpc with await?
let callRes = server.procs[methodName](node["params"])
await client.send($wrapReply(id, callRes, newJNull()) & "\c\l")
proc processClient(server: RpcServer, client: AsyncSocket) {.async.} =
while true:
let line = await client.recvLine()
if line == "":
# Disconnected.
client.close()
echo server.port, " request with no data"
break
ifDebug: echo "Process client: ", server.port, ":" & line
let fut = processMessage(server, client, line)
await fut
if fut.failed:
if fut.readError of RpcProcError:
# This error signifies that the proc wants us to respond with a custom
# error object.
let err = fut.readError.RpcProcError
await client.sendError(err.code, err.msg, err.data)
else:
await client.sendError(-32000, "Error", %getCurrentExceptionMsg())
proc serve*(server: RpcServer) {.async.} =
server.registerEthereumRpcs
server.socket.bindAddr(server.port, server.address)
server.socket.listen()
while true:
let client = await server.socket.accept()
asyncCheck server.processClient(client)

View File

@ -0,0 +1,25 @@
import asyncdispatch, asyncnet, json, tables
type
RpcProc* = proc (params: JsonNode): JsonNode
RpcServer* = ref object
socket*: AsyncSocket
port*: Port
address*: string
procs*: TableRef[string, RpcProc]
RpcProcError* = ref object of Exception
code*: int
data*: JsonNode
proc newRpcServer*(address: string, port: Port = Port(8545)): RpcServer =
RpcServer(
socket: newAsyncSocket(),
port: port,
address: address,
procs: newTable[string, RpcProc]()
)
proc register*(server: RpcServer, name: string, rpc: RpcProc) =
server.procs[name] = rpc

13
tests/testclient.nim Normal file
View File

@ -0,0 +1,13 @@
import ../src/rpcclient, asyncdispatch, json
when isMainModule:
proc main {.async.} =
var client = newRpcClient()
await client.connect("localhost", Port(8545))
var
response: Response
for i in 0..1000:
response = waitFor client.web3_clientVersion(newJNull())
waitFor main()
echo "Finished."

11
tests/testserver.nim Normal file
View File

@ -0,0 +1,11 @@
import ../src/rpcserver, asyncdispatch
when isMainModule:
echo "Initialising server..."
# create on localhost, default port
var srv = newRpcServer("")
echo "Server started."
asyncCheck srv.serve()
runForever()
echo "Server stopped."

16
tests/testutils.nim Normal file
View File

@ -0,0 +1,16 @@
import strutils, ../src/server/private/transportutils, unittest
suite "Encoding":
test "Encode quantity":
check 0.encodeQuantity == "0x0"
check 0x1000.encodeQuantity == "0x1000"
test "Encode data":
var i = 0
for b in bytes(0x07_06_05_04_03_02_01_00'u64):
check b == i.byte
i.inc
test "Encode data pairs":
for i, b in bytePairs(0x07_06_05_04_03_02_01_00'u64):
check b == i.byte