diff --git a/nimbus/graphql/ethapi.nim b/nimbus/graphql/ethapi.nim new file mode 100644 index 000000000..2781af8fa --- /dev/null +++ b/nimbus/graphql/ethapi.nim @@ -0,0 +1,1035 @@ +# nim-graphql +# Copyright (c) 2021 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import + std/[strutils, times], + stew/[results, byteutils], stint, + eth/[common, rlp], chronos, + graphql, graphql/graphql as context, + graphql/common/types, graphql/httpserver, + ../db/[db_chain, state_db], ../errors, ../utils, + ../transaction, ../rpc/rpc_utils, ../vm_state, ../config, + ../vm_computation, ../vm_state_transactions + +from eth/p2p import EthereumNode +export httpserver + +type + EthTypes = enum + ethAccount = "Account" + ethLog = "Log" + ethTransaction = "Transaction" + ethBlock = "Block" + ethCallResult = "CallResult" + ethSyncState = "SyncState" + ethPending = "Pending" + ethQuery = "Query" + ethMutation = "Mutation" + + HeaderNode = ref object + node: NodeObj + header: BlockHeader + + AccountNode = ref object + node: NodeObj + address: EthAddress + account: Account + db: ReadOnlyStateDB + + TxNode = ref object + node: NodeObj + tx: Transaction + index: int + blockNumber: BlockNumber + receipt: Receipt + gasUsed: GasInt + + LogNode = ref object + node: NodeObj + log: Log + index: int + tx: TxNode + + GraphqlContextRef = ref GraphqlContextObj + GraphqlContextObj = object of Graphql + ids: array[EthTypes, Name] + chainDB: BaseChainDB + ethNode: EthereumNode + +proc toHash(n: Node): Hash256 = + result.data = hexToByteArray[32](n.stringVal) + +proc toBlockNumber(n: Node): BlockNumber = + result = parse(n.intVal, UInt256, radix = 10) + +proc headerNode(ctx: GraphqlContextRef, header: BlockHeader): Node = + let n = HeaderNode( + node: NodeObj( + kind: nkMap, + typeName: ctx.ids[ethBlock], + pos: Pos() + ), + header: header + ) + cast[Node](n) + +proc headerNode(n: Node): HeaderNode = + cast[HeaderNode](n) + +proc accountNode(ctx: GraphqlContextRef, acc: Account, address: EthAddress, db: ReadOnlyStateDB): Node = + let n = AccountNode( + node: NodeObj( + kind: nkMap, + typeName: ctx.ids[ethAccount], + pos: Pos() + ), + account: acc, + address: address, + db: db + ) + cast[Node](n) + +proc accountNode(n: Node): AccountNode = + cast[AccountNode](n) + +proc txNode(ctx: GraphqlContextRef, tx: Transaction, index: int, blockNumber: BlockNumber): Node = + let n = TxNode( + node: NodeObj( + kind: nkMap, + typeName: ctx.ids[ethTransaction], + pos: Pos() + ), + tx: tx, + index: index, + blockNumber: blockNumber + ) + cast[Node](n) + +proc txNode(n: Node): TxNode = + cast[TxNode](n) + +proc logNode(ctx: GraphqlContextRef, log: Log, index: int, tx: TxNode): Node = + let n = LogNode( + node: NodeObj( + kind: nkMap, + typeName: ctx.ids[ethLog], + pos: Pos() + ), + log: log, + index: index, + tx: tx + ) + cast[Node](n) + +proc logNode(n: Node): LogNode = + cast[LogNode](n) + +proc getAccountDb(chainDB: BaseChainDB, header: BlockHeader): ReadOnlyStateDB = + ## Retrieves the account db from canonical head + ## we don't use accounst_cache here because it's only read operations + let ac = newAccountStateDB(chainDB.db, header.stateRoot, chainDB.pruneTrie) + ReadOnlyStateDB(ac) + +proc getBlockByNumber(ctx: GraphqlContextRef, number: Node): RespResult = + try: + ok(headerNode(ctx, getBlockHeader(ctx.chainDB, toBlockNumber(number)))) + except EVMError as e: + err("can't get block no '$1': $2" % [number.intVal, e.msg]) + +proc getBlockByNumber(ctx: GraphqlContextRef, number: BlockNumber): RespResult = + try: + ok(headerNode(ctx, getBlockHeader(ctx.chainDB, number))) + except EVMError as e: + err("can't get block no '$1': $2" % [$number, e.msg]) + +proc getBlockByHash(ctx: GraphqlContextRef, hash: Node): RespResult = + try: + ok(headerNode(ctx, getBlockHeader(ctx.chainDB, toHash(hash)))) + except EVMError as e: + err("can't get block with hash '$1': $2" % [hash.stringVal, e.msg]) + +proc getBlockByHash(ctx: GraphqlContextRef, hash: Hash256): RespResult = + try: + ok(headerNode(ctx, getBlockHeader(ctx.chainDB, hash))) + except EVMError as e: + err("can't get block with hash '0x$1': $2" % [hash.data.toHex, e.msg]) + +proc getLatestBlock(ctx: GraphqlContextRef): RespResult = + try: + ok(headerNode(ctx, getCanonicalHead(ctx.chainDB))) + except EVMError as e: + err("can't get latest block: " & e.msg) + +proc getTxCount(ctx: GraphqlContextRef, txRoot: Hash256): RespResult = + try: + ok(resp(getTransactionCount(ctx.chainDB, txRoot))) + except EVMError as e: + err("can't get txcount: " & e.msg) + except Exception as em: + err("can't get txcount: " & em.msg) + +proc longNode(val: uint64 | int64): RespResult = + ok(Node(kind: nkInt, intVal: $val, pos: Pos())) + +proc longNode(val: UInt256): RespResult = + ok(Node(kind: nkInt, intVal: val.toString, pos: Pos())) + +proc bigIntNode(val: UInt256): RespResult = + let hex = val.toHex + ok(Node(kind: nkString, stringVal: if hex.len mod 2 != 0: "0x0" & hex else: "0x" & hex, pos: Pos())) + +proc bigIntNode(val: uint64 | int64): RespResult = + let hex = val.toHex + ok(Node(kind: nkString, stringVal: if hex.len mod 2 != 0: "0x0" & hex else: "0x" & hex, pos: Pos())) + +proc byte32Node(val: UInt256): RespResult = + ok(Node(kind: nkString, stringVal: "0x" & val.dumpHex, pos: Pos())) + +proc resp(hash: Hash256): RespResult = + ok(resp("0x" & hash.data.toHex)) + +proc resp(data: openArray[byte]): RespResult = + ok(resp("0x" & data.toHex)) + +proc getTotalDifficulty(ctx: GraphqlContextRef, blockHash: Hash256): RespResult = + try: + bigIntNode(getScore(ctx.chainDB, blockHash)) + except EVMError as e: + err("can't get total difficulty: " & e.msg) + +proc getOmmerCount(ctx: GraphqlContextRef, ommersHash: Hash256): RespResult = + try: + ok(resp(getUnclesCount(ctx.chainDB, ommersHash))) + except EVMError as e: + err("can't get ommers count: " & e.msg) + except Exception as em: + err("can't get ommers count: " & em.msg) + +proc getOmmers(ctx: GraphqlContextRef, ommersHash: Hash256): RespResult = + try: + let uncles = getUncles(ctx.chainDB, ommersHash) + if uncles.len == 0: + return ok(respNull()) + var list = respList() + for n in uncles: + list.add headerNode(ctx, n) + ok(list) + except EVMError as e: + err("can't get ommers: " & e.msg) + +proc getOmmerAt(ctx: GraphqlContextRef, ommersHash: Hash256, index: int): RespResult = + try: + let uncles = getUncles(ctx.chainDB, ommersHash) + if uncles.len == 0: + return ok(respNull()) + if index < 0 or index >= uncles.len: + return ok(respNull()) + ok(headerNode(ctx, uncles[index])) + except EVMError as e: + err("can't get ommer: " & e.msg) + +proc getTxs(ctx: GraphqlContextRef, header: BlockHeader): RespResult = + try: + let txCount = getTransactionCount(ctx.chainDB, header.txRoot) + if txCount == 0: + return ok(respNull()) + var list = respList() + var index = 0 + for n in getBlockTransactionData(ctx.chainDB, header.txRoot): + let tx = rlp.decode(n, Transaction) + list.add txNode(ctx, tx, index, header.blockNumber) + inc index + + index = 0 + var prevUsed = 0.GasInt + for r in getReceipts(ctx.chainDB, header.receiptRoot): + let tx = txNode(list.sons[index]) + tx.receipt = r + tx.gasUsed = r.cumulativeGasUsed - prevUsed + prevUsed = r.cumulativeGasUsed + inc index + + ok(list) + except EVMError as e: + err("can't get transactions: " & e.msg) + except Exception as em: + err("can't get transactions: " & em.msg) + +proc getTxAt(ctx: GraphqlContextRef, header: BlockHeader, index: int): RespResult = + try: + var tx: Transaction + if getTransaction(ctx.chainDB, header.txRoot, index, tx): + let txn = txNode(ctx, tx, index, header.blockNumber) + + var i = 0 + var prevUsed = 0.GasInt + for r in getReceipts(ctx.chainDB, header.receiptRoot): + if i == index: + let tx = txNode(txn) + tx.receipt = r + tx.gasUsed = r.cumulativeGasUsed - prevUsed + prevUsed = r.cumulativeGasUsed + inc i + + ok(txn) + else: + ok(respNull()) + except EVMError as e: + err("can't get transaction by index '$1': $2" % [$index, e.msg]) + except Exception as em: + err("can't get transaction by index '$1': $2" % [$index, em.msg]) + +proc getTxByHash(ctx: GraphqlContextRef, hash: Hash256): RespResult = + try: + let (blockNumber, index) = getTransactionKey(ctx.chainDB, hash) + let header = getBlockHeader(ctx.chainDB, blockNumber) + getTxAt(ctx, header, index) + except EVMError as e: + err("can't get transaction by hash '$1': $2" % [hash.data.toHex, e.msg]) + except Exception as em: + err("can't get transaction by hash '$1': $2" % [hash.data.toHex, em.msg]) + +proc accountNode(ctx: GraphqlContextRef, header: BlockHeader, address: EthAddress): RespResult = + let db = getAccountDb(ctx.chainDB, header) + if not db.accountExists(address): + return ok(respNull()) + let acc = db.getAccount(address) + ok(accountNode(ctx, acc, address, db)) + +proc parseU64(node: Node): uint64 = + for c in node.intVal: + result = result * 10 + uint64(c.int - '0'.int) + +{.pragma: apiPragma, cdecl, gcsafe, raises: [Defect, CatchableError], locks:0.} +{.push hint[XDeclaredButNotUsed]: off.} + +proc validateHex(x: Node, minLen = 0): NodeResult = + if x.stringVal.len < 2: + return err("hex is too short") + if x.stringVal.len != 2 + minLen * 2 and minLen != 0: + return err("expect hex with len '$1', got '$2'" % [$(2 * minLen + 2), $x.stringVal.len]) + if x.stringVal.len mod 2 != 0: + return err("hex must have even number of nibbles") + if x.stringVal[0] != '0' or x.stringVal[1] notin {'x', 'X'}: + return err("hex should be prefixed by '0x'") + for i in 2.. 66: + # 256 bits = 32 bytes = 64 hex nibbles + # 64 hex nibbles + '0x' prefix = 66 bytes + return err("Big Int should not exceed 66 bytes") + validateHex(node) + else: + # convert it into hex nkString node + let val = parse(node.stringVal, UInt256, radix = 10) + node.stringVal = "0x" & val.toHex + validateHex(node) + else: + return err("expect hex/dec string or int, but got '$1'" % [$node.kind]) + +proc scalarLong(ctx: GraphqlRef, typeNode, node: Node): NodeResult {.cdecl, gcsafe, nosideEffect.} = + ## Long is a 64 bit unsigned integer. + const maxU64 = uint64.high.u256 + if node.kind == nkInt: + let val = parse(node.intVal, UInt256, radix = 10) + if val > maxU64: + return err("long value overflow") + ok(node) + else: + err("expect int, but got '$1'" % [$node.kind]) + +proc accountAddress(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let acc = accountNode(parent) + resp(acc.address) + +proc accountBalance(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let acc = accountNode(parent) + bigIntNode(acc.account.balance) + +proc accountTxCount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let acc = accountNode(parent) + longNode(acc.account.nonce) + +proc accountCode(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let acc = accountNode(parent) + let code = acc.db.getCode(acc.address) + resp(code) + +proc accountStorage(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let acc = accountNode(parent) + let slot = parse(params[0].val.stringVal, UInt256, radix = 16) + let (val, _) = acc.db.getStorage(acc.address, slot) + byte32Node(val) + +const accountProcs = { + "address": accountAddress, + "balance": accountBalance, + "transactionCount": accountTxCount, + "code": accountCode, + "storage": accountStorage +} + +proc logIndex(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let log = logNode(parent) + ok(resp(log.index)) + +proc logAccount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + # TODO: with block param + let ctx = GraphqlContextRef(ud) + let log = logNode(parent) + + let hres = ctx.getBlockByNumber(log.tx.blockNumber) + if hres.isErr: + return hres + let h = headerNode(hres.get()) + ctx.accountNode(h.header, log.log.address) + +proc logTopics(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let log = logNode(parent) + var list = respList() + for n in log.log.topics: + list.add resp("0x" & n.toHex) + ok(list) + +proc logData(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let log = logNode(parent) + resp(log.log.data) + +proc logTransaction(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let log = logNode(parent) + ok(cast[Node](log.tx)) + +const logProcs = { + "account": logAccount, + "index": logIndex, + "topics": logTopics, + "data": logData, + "transaction": logTransaction +} + +proc txHash(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let + tx = txNode(parent) + encodedTx = rlp.encode(tx.tx) + txHash = keccakHash(encodedTx) + resp(txHash) + +proc txNonce(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + longNode(tx.tx.accountNonce) + +proc txIndex(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + ok(resp(tx.index)) + +proc txFrom(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + # TODO: with block param + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + var sender: EthAddress + if not getSender(tx.tx, sender): + return ok(respNull()) + let hres = ctx.getBlockByNumber(tx.blockNumber) + if hres.isErr: + return hres + let h = headerNode(hres.get()) + ctx.accountNode(h.header, sender) + +proc txTo(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + # TODO: with block param + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + if tx.tx.isContractCreation: + return ok(respNull()) + let hres = ctx.getBlockByNumber(tx.blockNumber) + if hres.isErr: + return hres + let h = headerNode(hres.get()) + ctx.accountNode(h.header, tx.tx.to) + +proc txValue(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + bigIntNode(tx.tx.value) + +proc txGasPrice(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + bigIntNode(tx.tx.gasPrice) + +proc txGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + longNode(tx.tx.gasLimit) + +proc txInputData(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + resp(tx.tx.payload) + +proc txBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + ctx.getBlockByNumber(tx.blockNumber) + +proc txStatus(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + if tx.receipt.hasStatus: + longNode(tx.receipt.status().uint64) + else: + ok(respNull()) + +proc txGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + longNode(tx.gasUsed) + +proc txCumulativeGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + longNode(tx.receipt.cumulativeGasUsed) + +proc txCreatedContract(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + var sender: EthAddress + if not getSender(tx.tx, sender): + return ok(respNull()) + + let hres = getBlockByNumber(ctx, tx.blockNumber) + if hres.isErr: + return hres + let h = headerNode(hres.get()) + let db = getAccountDb(ctx.chainDB, h.header) + let creationNonce = db.getNonce(sender) + let contractAddress = generateAddress(sender, creationNonce) + ctx.accountNode(h.header, contractAddress) + +proc txLogs(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let tx = txNode(parent) + var list = respList() + for i, n in tx.receipt.logs: + list.add logNode(ctx, n, i, tx) + ok(list) + +const txProcs = { + "from": txFrom, + "hash": txHash, + "nonce": txNonce, + "index": txIndex, + "to": txTo, + "value": txValue, + "gasPrice": txGasPrice, + "gas": txGas, + "inputData": txInputData, + "block": txBlock, + "status": txStatus, + "gasUsed": txGasUsed, + "cumulativeGasUsed": txCumulativeGasUsed, + "createdContract": txCreatedContract, + "logs": txLogs +} + +proc blockNumberImpl(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + longNode(h.header.blockNumber) + +proc blockHashImpl(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let hash = blockHash(h.header) + resp(hash) + +proc blockParent(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + getBlockByHash(ctx, h.header.parentHash) + +proc blockNonce(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + ok(resp("0x" & h.header.nonce.toHex)) + +proc blockTransactionsRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.txRoot) + +proc blockTransactionCount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + ctx.getTxCount(h.header.txRoot) + +proc blockStateRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.stateRoot) + +proc blockReceiptsRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.receiptRoot) + +proc blockMiner(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + ctx.accountNode(h.header, h.header.coinbase) + +proc blockExtraData(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.extraData) + +proc blockGasLimit(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + longNode(h.header.gasLimit) + +proc blockGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + longNode(h.header.gasUsed) + +proc blockTimestamp(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + bigIntNode(h.header.timestamp.toUnix.uint64) + +proc blockLogsBloom(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.bloom) + +proc blockMixHash(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.mixDigest) + +proc blockDifficulty(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + bigIntNode(h.header.difficulty) + +proc blockTotalDifficulty(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let hash = blockHash(h.header) + getTotalDifficulty(ctx, hash) + +proc blockOmmerCount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + getOmmerCount(ctx, h.header.ommersHash) + +proc blockOmmers(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + getOmmers(ctx, h.header.ommersHash) + +proc blockOmmerAt(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let index = parseU64(params[0].val) + getOmmerAt(ctx, h.header.ommersHash, index.int) + +proc blockOmmerHash(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + resp(h.header.ommersHash) + +proc blockTransactions(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + getTxs(ctx, h.header) + +proc blockTransactionAt(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let index = parseU64(params[0].val) + getTxAt(ctx, h.header, index.int) + +proc blockLogs(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc blockAccount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let address = hexToByteArray[20](params[0].val.stringVal) + ctx.accountNode(h.header, address) + +proc toCallData(n: Node): (CallData, bool) = + # phew, probably need to use macro here :) + var cd: CallData + var gasLimit = false + if n[0][1].kind != nkEmpty: + cd.source = hextoByteArray[20](n[0][1].stringVal) + + if n[1][1].kind != nkEmpty: + cd.to = hextoByteArray[20](n[1][1].stringVal) + else: + cd.contractCreation = true + + if n[2][1].kind != nkEmpty: + cd.gas = parseU64(n[2][1]).GasInt + gasLimit = true + + if n[3][1].kind != nkEmpty: + let gasPrice = parse(n[3][1].stringVal, UInt256, radix = 16) + cd.gasPrice = gasPrice.truncate(GasInt) + + if n[4][1].kind != nkEmpty: + cd.value = parse(n[4][1].stringVal, UInt256, radix = 16) + + if n[5][1].kind != nkEmpty: + cd.data = hexToSeqByte(n[5][1].stringVal) + + (cd, gasLimit) + +proc makeCall(ctx: GraphqlContextRef, callData: CallData, header: BlockHeader, chainDB: BaseChainDB): RespResult = + # TODO: handle revert + var + # we use current header stateRoot, unlike block validation + # which use previous block stateRoot + vmState = newBaseVMState(header.stateRoot, header, chainDB) + fork = toFork(chainDB.config, header.blockNumber) + comp = setupComputation(vmState, callData, fork) + + let gas = comp.gasMeter.gasRemaining + comp.execComputation() + var map = respMap(ctx.ids[ethCallResult]) + map["data"] = resp("0x" & comp.output.toHex) + map["gasUsed"] = longNode(gas - comp.gasMeter.gasRemaining).get() + map["status"] = longNode(if comp.isError: 0 else: 1).get() + ok(map) + +proc blockCall(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let param = params[0].val + try: + let (callData, gasLimit) = toCallData(param) + ctx.makeCall(callData, h.header, ctx.chainDB) + except Exception as em: + err("estimateGas error: " & em.msg) + +proc blockEstimateGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let h = headerNode(parent) + let param = params[0].val + try: + let (callData, gasLimit) = toCallData(param) + let gasUsed = estimateGas(callData, h.header, ctx.chainDB, gasLimit) + longNode(gasUsed) + except Exception as em: + err("estimateGas error: " & em.msg) + +const blockProcs = { + "parent": blockParent, + "number": blockNumberImpl, + "hash": blockHashImpl, + "nonce": blockNonce, + "transactionsRoot": blockTransactionsRoot, + "transactionCount": blockTransactionCount, + "stateRoot": blockStateRoot, + "receiptsRoot": blockReceiptsRoot, + "miner": blockMiner, + "extraData": blockExtraData, + "gasLimit": blockGasLimit, + "gasUsed": blockGasUsed, + "timestamp": blockTimestamp, + "logsBloom": blockLogsBloom, + "mixHash": blockMixHash, + "difficulty": blockDifficulty, + "totalDifficulty": blockTotalDifficulty, + "ommerCount": blockOmmerCount, + "ommers": blockOmmers, + "ommerAt": blockOmmerAt, + "ommerHash": blockOmmerHash, + "transactions": blockTransactions, + "transactionAt": blockTransactionAt, + "blockLogs": blockLogs, + "account": blockAccount, + "call": blockCall, + "estimateGas": blockEstimateGas +} + +proc callResultData(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + ok(parent.map[0].val) + +proc callResultGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + ok(parent.map[1].val) + +proc callResultStatus(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + ok(parent.map[2].val) + +const callResultProcs = { + "data": callResultData, + "gasUsed": callResultGasUsed, + "status": callResultStatus +} + +proc syncStateStartingBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + longNode(ctx.chainDB.startingBlock) + +proc syncStateCurrentBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + longNode(ctx.chainDB.currentBlock) + +proc syncStateHighestBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + longNode(ctx.chainDB.highestBlock) + +proc syncStatePulledStates(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + # TODO: what is this ? + let ctx = GraphqlContextRef(ud) + ok(respNull()) + +proc syncStateKnownStates(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + # TODO: what is this ? + let ctx = GraphqlContextRef(ud) + ok(respNull()) + +const syncStateProcs = { + "startingBlock": syncStateStartingBlock, + "currentBlock": syncStateCurrentBlock, + "highestBlock": syncStateHighestBlock, + "pulledStates": syncStatePulledStates, + "knownStates": syncStateKnownStates +} + +proc pendingTransactionCount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc pendingTransactions(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc pendingAccount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc pendingCall(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc pendingEstimateGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +const pendingProcs = { + "transactionCount": pendingTransactionCount, + "transactions": pendingTransactions, + "account": pendingAccount, + "call": pendingCall, + "estimateGas": pendingEstimateGas +} + +proc queryBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let number = params[0].val + let hash = params[1].val + if number.kind == nkInt: + getBlockByNumber(ctx, number) + elif hash.kind == nkString: + getBlockByHash(ctx, hash) + else: + getLatestBlock(ctx) + +proc queryBlocks(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let fromNumber = parseU64(params[0].val).toBlockNumber + + let to = params[1].val + let toNumber = if to.kind == nkEmpty: + ctx.chainDB.highestBlock + else: + parseU64(to).toBlockNumber + + if fromNumber > toNumber: + return err("from($1) is bigger than to($2)" % [fromNumber.toString, toNumber.toString]) + + # TODO: what is the maximum number here? + if toNumber - fromNumber > 32.toBlockNumber: + return err("can't get more than 32 blocks at once") + + var list = respList() + var number = fromNumber + while number <= toNumber: + let n = getBlockByNumber(ctx, number) + if n.isErr: + list.add respNull() + else: + list.add n.get() + number += 1.toBlockNumber + + ok(list) + +proc queryPending(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc queryTransaction(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + let hash = toHash(params[0].val) + getTxByHash(ctx, hash) + +proc queryLogs(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + # TODO: stub, missing impl + +proc queryGasPrice(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + try: + bigIntNode(calculateMedianGasPrice(ctx.chainDB)) + except Exception as em: + err("can't get gasPrice: " & em.msg) + +proc queryProtocolVersion(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + for n in ctx.ethNode.capabilities: + if n.name == "eth": + return ok(resp(n.version)) + err("can't get protocol version") + +proc querySyncing(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + ok(respMap(ctx.ids[ethSyncState])) + +const queryProcs = { + "block": queryBlock, + "blocks": queryBlocks, + "pending": queryPending, + "transaction": queryTransaction, + "logs": queryLogs, + "gasPrice": queryGasPrice, + "protocolVersion": queryProtocolVersion, + "syncing": querySyncing +} + +proc sendRawTransaction(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = + let ctx = GraphqlContextRef(ud) + try: + let data = hexToSeqByte(params[0].val.stringVal) + let _ = rlp.decode(data, Transaction) # we want to know if it is a valid tx blob + let txHash = keccakHash(data) + resp(txHash) + except Exception as em: + return err("failed to process raw transaction") + +const mutationProcs = { + "sendRawTransaction": sendRawTransaction +} + +{.pop.} + +const + ethSchema = staticRead("ethapi.ql") + +proc initEthApi(ctx: GraphqlContextRef) = + ctx.customScalar("Bytes32", scalarBytes32) + ctx.customScalar("Address", scalarAddress) + ctx.customScalar("Bytes", scalarBytes) + ctx.customScalar("BigInt", scalarBigInt) + ctx.customScalar("Long", scalarLong) + + for n in EthTypes: + let name = ctx.createName($n) + ctx.ids[n] = name + + ctx.addResolvers(ctx, ctx.ids[ethAccount ], accountProcs) + ctx.addResolvers(ctx, ctx.ids[ethLog ], logProcs) + ctx.addResolvers(ctx, ctx.ids[ethTransaction], txProcs) + ctx.addResolvers(ctx, ctx.ids[ethBlock ], blockProcs) + ctx.addResolvers(ctx, ctx.ids[ethCallResult ], callResultProcs) + ctx.addResolvers(ctx, ctx.ids[ethSyncState ], syncStateProcs) + ctx.addResolvers(ctx, ctx.ids[ethPending ], pendingProcs) + ctx.addResolvers(ctx, ctx.ids[ethQuery ], queryProcs) + ctx.addResolvers(ctx, ctx.ids[ethMutation ], mutationProcs) + + let res = ctx.parseSchema(ethSchema) + if res.isErr: + echo res.error + quit(QuitFailure) + +proc setupGraphqlContext*(chainDB: BaseChainDB, ethNode: EthereumNode): GraphqlContextRef = + let ctx = GraphqlContextRef( + chainDB: chainDB, + ethNode: ethNode + ) + graphql.init(ctx) + ctx.initEthApi() + ctx + +proc setupGraphqlHttpServer*(conf: NimbusConfiguration, + chainDB: BaseChainDB, ethNode: EthereumNode): GraphqlHttpServerRef = + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let ctx = setupGraphqlContext(chainDB, ethNode) + let sres = GraphqlHttpServerRef.new(ctx, conf.graphql.address, socketFlags = socketFlags) + if sres.isErr(): + echo sres.error + quit(QuitFailure) + sres.get() diff --git a/nimbus/graphql/ethapi.ql b/nimbus/graphql/ethapi.ql new file mode 100644 index 000000000..f34e0cd62 --- /dev/null +++ b/nimbus/graphql/ethapi.ql @@ -0,0 +1,379 @@ +# Bytes32 is a 32 byte binary string, represented as 0x-prefixed hexadecimal. +scalar Bytes32 + +# Address is a 20 byte Ethereum address, represented as 0x-prefixed hexadecimal. +scalar Address + +# Bytes is an arbitrary length binary string, represented as 0x-prefixed hexadecimal. +# An empty byte string is represented as '0x'. Byte strings must have an even number of hexadecimal nybbles. +scalar Bytes + +# BigInt is a large integer. Input is accepted as either a JSON number or as a string. +# Strings may be either decimal or 0x-prefixed hexadecimal. Output values are all +# 0x-prefixed hexadecimal. +scalar BigInt + +# Long is a 64 bit unsigned integer. +scalar Long + +schema { + query: Query + mutation: Mutation +} + +# Account is an Ethereum account at a particular block. +type Account { + # Address is the address owning the account. + address: Address! + + # Balance is the balance of the account, in wei. + balance: BigInt! + + # TransactionCount is the number of transactions sent from this account, + # or in the case of a contract, the number of contracts created. Otherwise + # known as the nonce. + transactionCount: Long! + + # Code contains the smart contract code for this account, if the account + # is a (non-self-destructed) contract. + code: Bytes! + + # Storage provides access to the storage of a contract account, indexed + # by its 32 byte slot identifier. + storage(slot: Bytes32!): Bytes32! +} + +# Log is an Ethereum event log. +type Log { + # Index is the index of this log in the block. + index: Int! + + # Account is the account which generated this log - this will always + # be a contract account. + account(block: Long): Account! + + # Topics is a list of 0-4 indexed topics for the log. + topics: [Bytes32!]! + + # Data is unindexed data for this log. + data: Bytes! + + # Transaction is the transaction that generated this log entry. + transaction: Transaction! +} + +# Transaction is an Ethereum transaction. +type Transaction { + # Hash is the hash of this transaction. + hash: Bytes32! + + # Nonce is the nonce of the account this transaction was generated with. + nonce: Long! + + # Index is the index of this transaction in the parent block. This will + # be null if the transaction has not yet been mined. + index: Int + + # From is the account that sent this transaction - this will always be + # an externally owned account. + from(block: Long): Account! + + # To is the account the transaction was sent to. This is null for + # contract-creating transactions. + to(block: Long): Account + + # Value is the value, in wei, sent along with this transaction. + value: BigInt! + + # GasPrice is the price offered to miners for gas, in wei per unit. + gasPrice: BigInt! + + # Gas is the maximum amount of gas this transaction can consume. + gas: Long! + + # InputData is the data supplied to the target of the transaction. + inputData: Bytes! + + # Block is the block this transaction was mined in. This will be null if + # the transaction has not yet been mined. + block: Block + + #----------------------- Receipt fields --------------------------------- + + # Status is the return status of the transaction. This will be 1 if the + # transaction succeeded, or 0 if it failed (due to a revert, or due to + # running out of gas). If the transaction has not yet been mined, this + # field will be null. + status: Long + + # GasUsed is the amount of gas that was used processing this transaction. + # If the transaction has not yet been mined, this field will be null. + gasUsed: Long + + # CumulativeGasUsed is the total gas used in the block up to and including + # this transaction. If the transaction has not yet been mined, this field + # will be null. + cumulativeGasUsed: Long + + # CreatedContract is the account that was created by a contract creation + # transaction. If the transaction was not a contract creation transaction, + # or it has not yet been mined, this field will be null. + createdContract(block: Long): Account + + # Logs is a list of log entries emitted by this transaction. If the + # transaction has not yet been mined, this field will be null. + logs: [Log!] +} + +# BlockFilterCriteria encapsulates log filter criteria for a filter applied +# to a single block. +input BlockFilterCriteria { + # Addresses is list of addresses that are of interest. If this list is + # empty, results will not be filtered by address. + addresses: [Address!] + + # Topics list restricts matches to particular event topics. Each event has a list + # of topics. Topics matches a prefix of that list. An empty element array matches any + # topic. Non-empty elements represent an alternative that matches any of the + # contained topics. + # + # Examples: + # - [] or nil matches any topic list + # - [[A]] matches topic A in first position + # - [[], [B]] matches any topic in first position, B in second position + # - [[A], [B]] matches topic A in first position, B in second position + # - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + topics: [[Bytes32!]!] +} + +# Block is an Ethereum block. +type Block { + # Number is the number of this block, starting at 0 for the genesis block. + number: Long! + + # Hash is the block hash of this block. + hash: Bytes32! + + # Parent is the parent block of this block. + parent: Block + + # Nonce is the block nonce, an 8 byte sequence determined by the miner. + nonce: Bytes! + + # TransactionsRoot is the keccak256 hash of the root of the trie of transactions in this block. + transactionsRoot: Bytes32! + + # TransactionCount is the number of transactions in this block. if + # transactions are not available for this block, this field will be null. + transactionCount: Int + + # StateRoot is the keccak256 hash of the state trie after this block was processed. + stateRoot: Bytes32! + + # ReceiptsRoot is the keccak256 hash of the trie of transaction receipts in this block. + receiptsRoot: Bytes32! + + # Miner is the account that mined this block. + miner(block: Long): Account! + + # ExtraData is an arbitrary data field supplied by the miner. + extraData: Bytes! + + # GasLimit is the maximum amount of gas that was available to transactions in this block. + gasLimit: Long! + + # GasUsed is the amount of gas that was used executing transactions in this block. + gasUsed: Long! + + # Timestamp is the unix timestamp at which this block was mined. + timestamp: BigInt! + + # LogsBloom is a bloom filter that can be used to check if a block may + # contain log entries matching a filter. + logsBloom: Bytes! + + # MixHash is the hash that was used as an input to the PoW process. + mixHash: Bytes32! + + # Difficulty is a measure of the difficulty of mining this block. + difficulty: BigInt! + + # TotalDifficulty is the sum of all difficulty values up to and including + # this block. + totalDifficulty: BigInt! + + # OmmerCount is the number of ommers (AKA uncles) associated with this + # block. If ommers are unavailable, this field will be null. + ommerCount: Int + + # Ommers is a list of ommer (AKA uncle) blocks associated with this block. + # If ommers are unavailable, this field will be null. Depending on your + # node, the transactions, transactionAt, transactionCount, ommers, + # ommerCount and ommerAt fields may not be available on any ommer blocks. + ommers: [Block] + + # OmmerAt returns the ommer (AKA uncle) at the specified index. If ommers + # are unavailable, or the index is out of bounds, this field will be null. + ommerAt(index: Int!): Block + + # OmmerHash is the keccak256 hash of all the ommers (AKA uncles) + # associated with this block. + ommerHash: Bytes32! + + # Transactions is a list of transactions associated with this block. If + # transactions are unavailable for this block, this field will be null. + transactions: [Transaction!] + + # TransactionAt returns the transaction at the specified index. If + # transactions are unavailable for this block, or if the index is out of + # bounds, this field will be null. + transactionAt(index: Int!): Transaction + + # Logs returns a filtered set of logs from this block. + logs(filter: BlockFilterCriteria!): [Log!]! + + # Account fetches an Ethereum account at the current block's state. + account(address: Address!): Account + + # Call executes a local call operation at the current block's state. + call(data: CallData!): CallResult + + # EstimateGas estimates the amount of gas that will be required for + # successful execution of a transaction at the current block's state. + estimateGas(data: CallData!): Long! +} + +# CallData represents the data associated with a local contract call. +# All fields are optional. +input CallData { + # From is the address making the call. + from: Address + + # To is the address the call is sent to. + to: Address + + # Gas is the amount of gas sent with the call. + gas: Long + + # GasPrice is the price, in wei, offered for each unit of gas. + gasPrice: BigInt + + # Value is the value, in wei, sent along with the call. + value: BigInt + + # Data is the data sent to the callee. + data: Bytes +} + +# CallResult is the result of a local call operation. +type CallResult { + # Data is the return data of the called contract. + data: Bytes! + + # GasUsed is the amount of gas used by the call, after any refunds. + gasUsed: Long! + + # Status is the result of the call - 1 for success or 0 for failure. + status: Long! +} + +# FilterCriteria encapsulates log filter criteria for searching log entries. +input FilterCriteria { + # FromBlock is the block at which to start searching, inclusive. Defaults + # to the latest block if not supplied. + fromBlock: Long + + # ToBlock is the block at which to stop searching, inclusive. Defaults + # to the latest block if not supplied. + toBlock: Long + + # Addresses is a list of addresses that are of interest. If this list is + # empty, results will not be filtered by address. + addresses: [Address!] + + # Topics list restricts matches to particular event topics. Each event has a list + # of topics. Topics matches a prefix of that list. An empty element array matches any + # topic. Non-empty elements represent an alternative that matches any of the + # contained topics. + # + # Examples: + # - [] or nil matches any topic list + # - [[A]] matches topic A in first position + # - [[], [B]] matches any topic in first position, B in second position + # - [[A], [B]] matches topic A in first position, B in second position + # - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + topics: [[Bytes32!]!] +} + +# SyncState contains the current synchronisation state of the client. +type SyncState{ + # StartingBlock is the block number at which synchronisation started. + startingBlock: Long! + + # CurrentBlock is the point at which synchronisation has presently reached. + currentBlock: Long! + + # HighestBlock is the latest known block number. + highestBlock: Long! + + # PulledStates is the number of state entries fetched so far, or null + # if this is not known or not relevant. + pulledStates: Long + + # KnownStates is the number of states the node knows of so far, or null + # if this is not known or not relevant. + knownStates: Long +} + +# Pending represents the current pending state. +type Pending { + # TransactionCount is the number of transactions in the pending state. + transactionCount: Int! + + # Transactions is a list of transactions in the current pending state. + transactions: [Transaction!] + + # Account fetches an Ethereum account for the pending state. + account(address: Address!): Account + + # Call executes a local call operation for the pending state. + call(data: CallData!): CallResult + + # EstimateGas estimates the amount of gas that will be required for + # successful execution of a transaction for the pending state. + estimateGas(data: CallData!): Long! +} + +type Query { + # Block fetches an Ethereum block by number or by hash. If neither is + # supplied, the most recent known block is returned. + block(number: Long, hash: Bytes32): Block + + # Blocks returns all the blocks between two numbers, inclusive. If + # to is not supplied, it defaults to the most recent known block. + blocks(from: Long!, to: Long): [Block!]! + + # Pending returns the current pending state. + pending: Pending! + + # Transaction returns a transaction specified by its hash. + transaction(hash: Bytes32!): Transaction + + # Logs returns log entries matching the provided filter. + logs(filter: FilterCriteria!): [Log!]! + + # GasPrice returns the node's estimate of a gas price sufficient to + # ensure a transaction is mined in a timely fashion. + gasPrice: BigInt! + + # ProtocolVersion returns the current wire protocol version number. + protocolVersion: Int! + + # Syncing returns information on the current synchronisation state. + syncing: SyncState +} + +type Mutation { + # SendRawTransaction sends an RLP-encoded transaction to the network. + sendRawTransaction(data: Bytes!): Bytes32! +}