# nim-graphql # Copyright (c) 2021-2024 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], stew/byteutils, stint, results, eth/common/transaction_utils, chronos, graphql, graphql/graphql as context, graphql/common/types, graphql/httpserver, graphql/instruments/query_complexity, ../db/[ledger], ../rpc/rpc_utils, ".."/[transaction, evm/state, config, constants], ../transaction/call_evm, ../core/[tx_pool, tx_pool/tx_item], ../core/chain/forked_chain, ../common/common, web3/eth_api_types 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" ethAccessTuple = "AccessTuple" ethWithdrawal = "Withdrawal" HeaderNode = ref object of Node header: Header AccountNode = ref object of Node address: Address account: Account db: LedgerRef TxNode = ref object of Node tx: Transaction index: uint64 blockNumber: base.BlockNumber receipt: Receipt gasUsed: GasInt baseFee: Opt[UInt256] LogNode = ref object of Node log: Log index: int tx: TxNode AclNode = ref object of Node acl: AccessPair WdNode = ref object of Node wd: Withdrawal GraphqlContextRef = ref GraphqlContextObj GraphqlContextObj = object of Graphql ids: array[EthTypes, Name] com: CommonRef chainDB: CoreDbRef ethNode: EthereumNode txPool: TxPoolRef chain: ForkedChainRef {.push gcsafe, raises: [].} {.pragma: apiRaises, raises: [].} {.pragma: apiPragma, cdecl, gcsafe, apiRaises.} proc toHash(n: Node): Hash32 {.gcsafe, raises: [ValueError].} = Hash32.fromHex(n.stringVal) proc toBlockNumber(n: Node): base.BlockNumber {.gcsafe, raises: [ValueError].} = if n.kind == nkInt: result = parse(n.intVal, UInt256, radix = 10).truncate(base.BlockNumber) elif n.kind == nkString: result = parse(n.stringVal, UInt256, radix = 16).truncate(base.BlockNumber) else: doAssert(false, "unknown node type: " & $n.kind) proc headerNode(ctx: GraphqlContextRef, header: Header): Node = HeaderNode( kind: nkMap, typeName: ctx.ids[ethBlock], pos: Pos(), header: header ) proc accountNode(ctx: GraphqlContextRef, acc: Account, address: Address, db: LedgerRef): Node = AccountNode( kind: nkMap, typeName: ctx.ids[ethAccount], pos: Pos(), account: acc, address: address, db: db ) proc txNode(ctx: GraphqlContextRef, tx: Transaction, index: uint64, blockNumber: base.BlockNumber, baseFee: Opt[UInt256]): Node = TxNode( kind: nkMap, typeName: ctx.ids[ethTransaction], pos: Pos(), tx: tx, index: index, blockNumber: blockNumber, baseFee: baseFee ) proc logNode(ctx: GraphqlContextRef, log: Log, index: int, tx: TxNode): Node = LogNode( kind: nkMap, typeName: ctx.ids[ethLog], pos: Pos(), log: log, index: index, tx: tx ) proc aclNode(ctx: GraphqlContextRef, accessPair: AccessPair): Node = AclNode( kind: nkMap, typeName: ctx.ids[ethAccessTuple], pos: Pos(), acl: accessPair ) proc wdNode(ctx: GraphqlContextRef, wd: Withdrawal): Node = WdNode( kind: nkMap, typeName: ctx.ids[ethWithdrawal], pos: Pos(), wd: wd ) proc getStateDB(com: CommonRef, header: Header): LedgerRef {.deprecated: "LedgerRef does not support loading a particular state".} = ## Retrieves the account db from canonical head ## we don't use accounst_cache here because it's read only operations # TODO the ledger initialized here refers to the base, not the given header! LedgerRef.init(com.db) proc getBlockByNumber(ctx: GraphqlContextRef, number: Node): RespResult = try: let header = ?ctx.chain.headerByNumber(toBlockNumber(number)) ok(headerNode(ctx, header)) except ValueError as exc: err(exc.msg) proc getBlockByNumber(ctx: GraphqlContextRef, number: base.BlockNumber): RespResult = let header = ?ctx.chain.headerByNumber(number) ok(headerNode(ctx, header)) proc getBlockByHash(ctx: GraphqlContextRef, hash: Node): RespResult = try: let header = ?ctx.chain.headerByHash(toHash(hash)) ok(headerNode(ctx, header)) except ValueError as exc: err(exc.msg) proc getBlockByHash(ctx: GraphqlContextRef, hash: Hash32): RespResult = let header = ?ctx.chain.headerByHash(hash) ok(headerNode(ctx, header)) proc getLatestBlock(ctx: GraphqlContextRef): RespResult = let header = ctx.chain.latestHeader ok(headerNode(ctx, header)) proc getTxCount(ctx: GraphqlContextRef, txRoot: Hash32): RespResult = let txCount = ctx.chainDB.getTransactionCount(txRoot) ok(resp(txCount)) 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 stripLeadingZeros(x: string): string = strip(x, leading = true, trailing = false, chars = {'0'}) proc bigIntNode(val: UInt256): RespResult = if val == 0.u256: ok(Node(kind: nkString, stringVal: "0x0", pos: Pos())) else: let hex = stripLeadingZeros(val.toHex) ok(Node(kind: nkString, stringVal: "0x" & hex, pos: Pos())) proc bigIntNode(x: uint64 | int64): RespResult = # stdlib toHex is not suitable for hive const HexChars = "0123456789abcdef" if x == 0: return ok(Node(kind: nkString, stringVal: "0x0", pos: Pos())) var n = cast[uint64](x) r: array[2*sizeof(uint64), char] i = 0 while n > 0: r[i] = HexChars[int(n and 0xF)] n = n shr 4 inc i var hex = newString(i+2) hex[0] = '0' hex[1] = 'x' while i > 0: hex[hex.len-i] = r[i-1] dec i ok(Node(kind: nkString, stringVal: hex, pos: Pos())) proc byte32Node(val: UInt256): RespResult = ok(Node(kind: nkString, stringVal: "0x" & val.dumpHex, pos: Pos())) proc resp(hash: Hash32 | Bytes32): RespResult = ok(resp(hash.to0xHex)) proc resp(data: openArray[byte]): RespResult = ok(resp("0x" & data.toHex)) proc getTotalDifficulty(ctx: GraphqlContextRef, blockHash: Hash32): RespResult = let score = getScore(ctx.chainDB, blockHash).valueOr: return err("can't get total difficulty") bigIntNode(score) proc getOmmerCount(ctx: GraphqlContextRef, ommersHash: Hash32): RespResult = let ommers = ?ctx.chainDB.getUnclesCount(ommersHash) ok(resp(ommers)) proc getOmmers(ctx: GraphqlContextRef, ommersHash: Hash32): RespResult = let uncles = ?ctx.chainDB.getUncles(ommersHash) when false: # EIP 1767 says no ommers == null # but hive test case want empty array [] if uncles.len == 0: return ok(respNull()) var list = respList() for n in uncles: list.add headerNode(ctx, n) ok(list) proc getOmmerAt(ctx: GraphqlContextRef, ommersHash: Hash32, index: int): RespResult = let uncles = ?ctx.chainDB.getUncles(ommersHash) if uncles.len == 0: return ok(respNull()) if index < 0 or index >= uncles.len: return ok(respNull()) ok(headerNode(ctx, uncles[index])) proc getTxs(ctx: GraphqlContextRef, header: Header): RespResult = let txCount = getTransactionCount(ctx.chainDB, header.txRoot) if txCount == 0: return ok(respNull()) var list = respList() var index = 0'u64 let txList = ?ctx.chainDB.getTransactions(header.txRoot) for tx in txList: list.add txNode(ctx, tx, index, header.number, header.baseFeePerGas) inc index index = 0'u64 var prevUsed = 0.GasInt let receiptList = ?ctx.chainDB.getReceipts(header.receiptsRoot) for r in receiptList: let tx = TxNode(list.sons[index]) tx.receipt = r tx.gasUsed = r.cumulativeGasUsed - prevUsed prevUsed = r.cumulativeGasUsed inc index ok(list) proc getWithdrawals(ctx: GraphqlContextRef, header: Header): RespResult = if header.withdrawalsRoot.isNone: return ok(respNull()) let wds = ?ctx.chainDB.getWithdrawals(header.withdrawalsRoot.get) var list = respList() for wd in wds: list.add wdNode(ctx, wd) ok(list) proc getTxAt(ctx: GraphqlContextRef, header: Header, index: uint64): RespResult = let tx = ctx.chainDB.getTransactionByIndex(header.txRoot, index.uint16).valueOr: return ok(respNull()) let txn = txNode(ctx, tx, index, header.number, header.baseFeePerGas) var i = 0'u64 var prevUsed = 0.GasInt let receiptList = ?ctx.chainDB.getReceipts(header.receiptsRoot) for r in receiptList: if i == index: let tx = TxNode(txn) tx.receipt = r tx.gasUsed = r.cumulativeGasUsed - prevUsed prevUsed = r.cumulativeGasUsed inc i ok(txn) proc getTxByHash(ctx: GraphqlContextRef, hash: Hash32): RespResult = let txKey = ?ctx.chainDB.getTransactionKey(hash) header = ?ctx.chainDB.getBlockHeader(txKey.blockNumber) getTxAt(ctx, header, txKey.index) proc accountNode(ctx: GraphqlContextRef, header: Header, address: Address): RespResult = try: let db = getStateDB(ctx.com, header) when false: # EIP 1767 unclear about non existent account # but hive test case demand something if not db.accountExists(address): return ok(respNull()) let acc = db.getEthAccount(address) ok(accountNode(ctx, acc, address, db)) except RlpError as ex: err(ex.msg) func hexCharToInt(c: char): uint64 = case c of 'a'..'f': return c.uint64 - 'a'.uint64 + 10'u64 of 'A'..'F': return c.uint64 - 'A'.uint64 + 10'u64 of '0'..'9': return c.uint64 - '0'.uint64 else: doAssert(false, "invalid hex digit: " & $c) proc parseU64(node: Node): uint64 = if node.kind == nkString: if node.stringVal.len > 2 and node.stringVal[1] == 'x': for i in 2.. expectedLen: return err("$1 len is too long: expect $2 got $3" % [kind, $expectedLen, $x.stringVal.len]) for i in prefixLen.. 2 and node.stringVal[1] == 'x': if node.stringVal[0] != '0': return err("Big Int hex malformed") if node.stringVal.len > 66: # 256 bits = 32 bytes = 64 hex nibbles # 64 hex nibbles + '0x' prefix = 66 bytes return err("Big Int hex should not exceed 66 bytes") for i in 2.. 64: return err("Big Int hex should not exceed 64 bytes") for i in 0.. 2 and node.stringVal[1] == 'x': let val = parse(node.stringVal, UInt256, radix = 16) if val > maxU64: return err("long value overflow") ok(node) else: let val = parse(node.stringVal, UInt256, radix = 10) if val > maxU64: return err("long value overflow") ok(Node(kind: nkString, pos: node.pos, stringVal: "0x" & val.toHex)) of nkInt: let val = parse(node.intVal, UInt256, radix = 10) if val > maxU64: return err("long value overflow") ok(Node(kind: nkString, pos: node.pos, stringVal: "0x" & val.toHex)) else: err("expect int, but got '" & $node.kind & "'") except CatchableError as e: err("scalar Long error: " & e.msg) proc accountAddress(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acc = AccountNode(parent) resp(acc.address.data) proc accountBalance(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acc = AccountNode(parent) bigIntNode(acc.account.balance) proc accountTxCount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acc = AccountNode(parent) longNode(acc.account.nonce) proc accountCode(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acc = AccountNode(parent) try: let code = acc.db.getCode(acc.address) resp(code.bytes()) except RlpError as ex: err(ex.msg) proc accountStorage(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acc = AccountNode(parent) try: let slot = parse(params[0].val.stringVal, UInt256, radix = 16) let val = acc.db.getStorage(acc.address, slot) byte32Node(val) except RlpError as ex: err(ex.msg) except ValueError as ex: err(ex.msg) const accountProcs = { # Note: Need to define it as ResolverProc else a proc with noSideEffect is # assumed and this fails for accountCode and accountStorage. "address": ResolverProc accountAddress, "balance": accountBalance, "transactionCount": accountTxCount, "code": accountCode, "storage": accountStorage } proc logIndex(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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 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 log = LogNode(parent) resp(log.log.data) proc logTransaction(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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) txHash = rlpHash(tx.tx) # beware EIP-4844 resp(txHash) proc txNonce(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) longNode(tx.tx.nonce) proc txIndex(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) ok(resp(tx.index.int)) proc txFrom(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) let tx = TxNode(parent) let blockNumber = if params[0].val.kind != nkEmpty: parseU64(params[0].val) else: tx.blockNumber let sender = tx.tx.recoverSender.valueOr: return ok(respNull()) let hres = ctx.getBlockByNumber(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.} = let ctx = GraphqlContextRef(ud) let tx = TxNode(parent) let blockNumber = if params[0].val.kind != nkEmpty: parseU64(params[0].val) else: tx.blockNumber if tx.tx.contractCreation: return ok(respNull()) let hres = ctx.getBlockByNumber(blockNumber) if hres.isErr: return hres let h = HeaderNode(hres.get()) ctx.accountNode(h.header, tx.tx.to.get()) proc txValue(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) bigIntNode(tx.tx.value) proc txGasPrice(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.tx.txType >= TxEip1559: if tx.baseFee.isNone: return bigIntNode(tx.tx.gasPrice) let baseFee = tx.baseFee.get().truncate(GasInt) let priorityFee = min(tx.tx.maxPriorityFeePerGas, tx.tx.maxFeePerGas - baseFee) bigIntNode(priorityFee + baseFee) else: bigIntNode(tx.tx.gasPrice) proc txMaxFeePerGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.tx.txType >= TxEip1559: bigIntNode(tx.tx.maxFeePerGas) else: ok(respNull()) proc txMaxPriorityFeePerGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.tx.txType >= TxEip1559: bigIntNode(tx.tx.maxPriorityFeePerGas) else: ok(respNull()) proc txEffectiveGasPrice(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.baseFee.isNone: return bigIntNode(tx.tx.gasPrice) let baseFee = tx.baseFee.get().truncate(GasInt) let priorityFee = min(tx.tx.maxPriorityFeePerGas, tx.tx.maxFeePerGas - baseFee) bigIntNode(priorityFee + baseFee) proc txChainId(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.tx.txType == TxLegacy: ok(respNull()) else: longNode(tx.tx.chainId.uint64) proc txGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) longNode(tx.tx.gasLimit) proc txInputData(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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 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 tx = TxNode(parent) longNode(tx.gasUsed) proc txCumulativeGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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) let sender = tx.tx.recoverSender.valueOr: return err("can't calculate sender") if not tx.tx.contractCreation: return ok(respNull()) let hres = getBlockByNumber(ctx, tx.blockNumber) if hres.isErr: return hres let h = HeaderNode(hres.get()) let contractAddress = generateAddress(sender, tx.tx.nonce) 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) proc txR(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) bigIntNode(tx.tx.R) proc txS(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) bigIntNode(tx.tx.S) proc txV(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) bigIntNode(tx.tx.V) proc txType(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) let typ = resp(ord(tx.tx.txType)) ok(typ) proc txAccessList(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) let tx = TxNode(parent) if tx.tx.txType == TxLegacy: ok(respNull()) else: var list = respList() for x in tx.tx.accessList: list.add aclNode(ctx, x) ok(list) proc txMaxFeePerBlobGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.tx.txType < TxEip4844: ok(respNull()) else: longNode(tx.tx.maxFeePerBlobGas) proc txVersionedHashes(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) if tx.tx.txType < TxEip4844: ok(respNull()) else: var list = respList() for hs in tx.tx.versionedHashes: list.add resp("0x" & hs.data.toHex) ok(list) proc txRaw(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) let txBytes = rlp.encode(tx.tx) resp(txBytes) proc txRawReceipt(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let tx = TxNode(parent) let recBytes = rlp.encode(tx.receipt) resp(recBytes) 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, "r": txR, "s": txS, "v": txV, "type": txType, "accessList": txAccessList, "maxFeePerGas": txMaxFeePerGas, "maxPriorityFeePerGas": txMaxPriorityFeePerGas, "effectiveGasPrice": txEffectiveGasPrice, "chainID": txChainId, "maxFeePerBlobGas": txMaxFeePerBlobGas, "versionedHashes": txVersionedHashes, "raw": txRaw, "rawReceipt": txRawReceipt } proc aclAddress(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acl = AclNode(parent) resp(acl.acl.address.data) proc aclStorageKeys(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let acl = AclNode(parent) if acl.acl.storageKeys.len == 0: ok(respNull()) else: var list = respList() for n in acl.acl.storageKeys: list.add resp(n.data).get() ok(list) const aclProcs = { "address": aclAddress, "storageKeys": aclStorageKeys } proc wdIndex(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let w = WdNode(parent) longNode(w.wd.index) proc wdValidator(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let w = WdNode(parent) longNode(w.wd.validatorIndex) proc wdAddress(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let w = WdNode(parent) resp(w.wd.address.data) proc wdAmount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let w = WdNode(parent) longNode(w.wd.amount) const wdProcs = { "index": wdIndex, "validator": wdValidator, "address": wdAddress, "amount": wdAmount } proc blockNumberImpl(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) longNode(h.header.number) proc blockHashImpl(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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 h = HeaderNode(parent) ok(resp("0x" & h.header.nonce.toHex)) proc blockTransactionsRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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 h = HeaderNode(parent) resp(h.header.stateRoot) proc blockReceiptsRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) resp(h.header.receiptsRoot) 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 h = HeaderNode(parent) resp(h.header.extraData) proc blockGasLimit(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) longNode(h.header.gasLimit) proc blockGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) longNode(h.header.gasUsed) proc blockTimestamp(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) bigIntNode(h.header.timestamp.uint64) proc blockLogsBloom(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) resp(h.header.logsBloom.data) proc blockMixHash(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) resp(h.header.mixHash) proc blockDifficulty(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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 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) {.cast(noSideEffect).}: getTxs(ctx, h.header) proc blockTransactionAt(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) let h = HeaderNode(parent) try: let index = parseU64(params[0].val) {.cast(noSideEffect).}: getTxAt(ctx, h.header, index) except ValueError as ex: err(ex.msg) proc blockLogs(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc blockAccount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) let h = HeaderNode(parent) try: let address = Address.fromHex(params[0].val.stringVal) ctx.accountNode(h.header, address) except ValueError as ex: err(ex.msg) const fFrom = 0 fTo = 1 fGasLimit = 2 fGasPrice = 3 fMaxFee = 4 fMaxPriorityFee = 5 fValue = 6 fData = 7 template isSome(n: Node, field: int): bool = # [0] is the field's name node # [1] is the field's value node n[field][1].kind != nkEmpty template fieldString(n: Node, field: int): string = n[field][1].stringVal template optionalAddress(dstField: untyped, n: Node, field: int) = if isSome(n, field): let address = addresses.Address.fromHex(fieldString(n, field)) dstField = Opt.some(primitives.Address address.data) template optionalGasInt(dstField: untyped, n: Node, field: int) = if isSome(n, field): dstField = Opt.some(parseU64(n[field][1]).Quantity) template optionalGasHex(dstField: untyped, n: Node, field: int) = if isSome(n, field): let gas = parse(fieldString(n, field), UInt256, radix = 16) dstField = Opt.some(gas.truncate(uint64).Quantity) template optionalHexU256(dstField: untyped, n: Node, field: int) = if isSome(n, field): dstField = Opt.some(parse(fieldString(n, field), UInt256, radix = 16)) template optionalBytes(dstField: untyped, n: Node, field: int) = if isSome(n, field): dstField = Opt.some(hexToSeqByte(fieldString(n, field))) proc toTxArgs(n: Node): TransactionArgs {.gcsafe, raises: [ValueError].} = optionalAddress(result.`from`, n, fFrom) optionalAddress(result.to, n, fTo) optionalGasInt(result.gas, n, fGasLimit) optionalGasHex(result.gasPrice, n, fGasPrice) optionalGasHex(result.maxFeePerGas, n, fMaxFee) optionalGasHex(result.maxPriorityFeePerGas, n, fMaxPriorityFee) optionalHexU256(result.value, n, fValue) optionalBytes(result.data, n, fData) proc makeCall(ctx: GraphqlContextRef, args: TransactionArgs, header: Header): RespResult = let res = rpcCallEvm(args, header, ctx.com).valueOr: return err("Failed to call rpcCallEvm") var map = respMap(ctx.ids[ethCallResult]) map["data"] = resp("0x" & res.output.toHex) map["gasUsed"] = longNode(res.gasUsed).get() map["status"] = longNode(if res.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 args = toTxArgs(param) {.cast(noSideEffect).}: ctx.makeCall(args, h.header) except CatchableError as em: err("call 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 args = toTxArgs(param) # TODO: DEFAULT_RPC_GAS_CAP should configurable {.cast(noSideEffect).}: let gasUsed = rpcEstimateGas(args, h.header, ctx.com, DEFAULT_RPC_GAS_CAP).valueOr: return err("Failed to call rpcEstimateGas") longNode(gasUsed) except CatchableError as em: err("estimateGas error: " & em.msg) proc blockBaseFeePerGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) if h.header.baseFeePerGas.isSome: bigIntNode(h.header.baseFeePerGas.get) else: ok(respNull()) proc blockWithdrawalsRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) if h.header.withdrawalsRoot.isSome: resp(h.header.withdrawalsRoot.get) else: ok(respNull()) proc blockWithdrawals(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) let h = HeaderNode(parent) getWithdrawals(ctx, h.header) proc blockBlobGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) if h.header.blobGasUsed.isSome: longNode(h.header.blobGasUsed.get) else: ok(respNull()) proc blockExcessBlobGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) if h.header.excessBlobGas.isSome: longNode(h.header.excessBlobGas.get) else: ok(respNull()) proc blockParentBeaconBlockRoot(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let h = HeaderNode(parent) if h.header.parentBeaconBlockRoot.isSome: resp(h.header.parentBeaconBlockRoot.get) else: ok(respNull()) 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, "logs": blockLogs, "account": blockAccount, "call": blockCall, "estimateGas": blockEstimateGas, "baseFeePerGas": blockBaseFeePerGas, "withdrawalsRoot": blockWithdrawalsRoot, "withdrawals": blockWithdrawals, "blobGasUsed": blockBlobGasUsed, "excessBlobGas": blockExcessBlobGas, "parentBeaconBlockRoot": blockParentBeaconBlockRoot, } proc callResultData(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = ok(parent.map[0].val) proc callResultGasUsed(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = ok(parent.map[1].val) proc callResultStatus(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = 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.com.syncStart) proc syncStateCurrentBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) longNode(ctx.com.syncCurrent) proc syncStateHighestBlock(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) longNode(ctx.com.syncHighest) proc syncStatePulledStates(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: what is this ? ok(respNull()) proc syncStateKnownStates(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: what is this ? ok(respNull()) const syncStateProcs = { "startingBlock": syncStateStartingBlock, "currentBlock": syncStateCurrentBlock, "highestBlock": syncStateHighestBlock, "pulledStates": syncStatePulledStates, "knownStates": syncStateKnownStates } proc pendingTransactionCount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc pendingTransactions(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc pendingAccount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc pendingCall(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc pendingEstimateGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") const pendingProcs = { "transactionCount": pendingTransactionCount, "transactions": pendingTransactions, "account": pendingAccount, "call": pendingCall, "estimateGas": pendingEstimateGas } proc pickBlockNumber(ctx: GraphqlContextRef, number: Node): base.BlockNumber = if number.kind == nkEmpty: ctx.com.syncCurrent else: parseU64(number) proc queryAccount(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) try: let address = Address.fromHex(params[0].val.stringVal) let blockNumber = pickBlockNumber(ctx, params[1].val) let hres = getBlockByNumber(ctx, blockNumber) if hres.isErr: return hres let h = HeaderNode(hres.get()) accountNode(ctx, h.header, address) except ValueError as ex: err(ex.msg) 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 != nkEmpty and hash.kind != nkEmpty: err("only one param allowed, number or hash, not both") elif number.kind == nkInt: getBlockByNumber(ctx, number) elif number.kind == nkString: try: let blockNumber = toBlockNumber(number) getBlockByNumber(ctx, blockNumber) except ValueError as ex: err(ex.msg) 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) let to = params[1].val let toNumber = pickBlockNumber(ctx, to) if fromNumber > toNumber: return err("from(" & $fromNumber & ") is bigger than to(" & $toNumber & ")") # TODO: what is the maximum number here? if toNumber - fromNumber > 32'u64: 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'u64 ok(list) proc queryPending(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc queryTransaction(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) try: let hash = toHash(params[0].val) {.cast(noSideEffect).}: getTxByHash(ctx, hash) except ValueError as ex: err(ex.msg) proc queryLogs(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc queryGasPrice(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) try: bigIntNode(calculateMedianGasPrice(ctx.chain)) except CatchableError 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 bigIntNode(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])) proc queryMaxPriorityFeePerGas(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # TODO: stub, missing impl err("not implemented") proc queryChainId(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = let ctx = GraphqlContextRef(ud) longNode(ctx.com.chainId.uint64) const queryProcs = { "account": queryAccount, "block": queryBlock, "blocks": queryBlocks, "pending": queryPending, "transaction": queryTransaction, "logs": queryLogs, "gasPrice": queryGasPrice, "protocolVersion": queryProtocolVersion, "syncing": querySyncing, "maxPriorityFeePerGas": queryMaxPriorityFeePerGas, "chainID": queryChainId } proc sendRawTransaction(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragma.} = # if tx validation failed, the result will be null let ctx = GraphqlContextRef(ud) try: let data = hexToSeqByte(params[0].val.stringVal) let tx = decodePooledTx(data) # we want to know if it is a valid tx blob let txHash = rlpHash(tx) ctx.txPool.add(tx) let res = ctx.txPool.inPoolAndReason(txHash) if res.isOk: return resp(txHash) else: return err(res.error) except CatchableError as em: return err("failed to process raw transaction: " & em.msg) const mutationProcs = { "sendRawTransaction": sendRawTransaction } const ethSchema = staticRead("ethapi.ql") type QcNames = enum qcType = "__Type" qcFields = "fields" qcBlock = "block" qcTransaction = "Transaction" EthQueryComplexity = ref object of QueryComplexity names: array[QcNames, Name] proc calcQC(qc: QueryComplexity, field: FieldRef): int {.cdecl, gcsafe, apiRaises.} = let qc = EthQueryComplexity(qc) if field.parentType.sym.name == qc.names[qcType] and field.field.name.name == qc.names[qcFields]: return 100 elif field.parentType.sym.name == qc.names[qcTransaction] and field.field.name.name == qc.names[qcBlock]: return 100 else: return 1 proc newQC(ctx: GraphqlContextRef): EthQueryComplexity = const MaxQueryComplexity = 200 var qc = EthQueryComplexity() qc.init(calcQC, MaxQueryComplexity) for n in QcNames: let name = ctx.createName($n) qc.names[n] = name qc 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) ctx.addResolvers(ctx, ctx.ids[ethAccessTuple], aclProcs) ctx.addResolvers(ctx, ctx.ids[ethWithdrawal ], wdProcs) var qc = newQC(ctx) ctx.addInstrument(qc) let res = ctx.parseSchema(ethSchema) if res.isErr: echo res.error quit(QuitFailure) proc setupGraphqlContext*(chain: ForkedChainRef, ethNode: EthereumNode, txPool: TxPoolRef): GraphqlContextRef = let ctx = GraphqlContextRef( chainDB: chain.com.db, com : chain.com, ethNode: ethNode, txPool : txPool, chain : chain, ) graphql.init(ctx) ctx.initEthApi() ctx proc setupGraphqlHttpHandler*(chain: ForkedChainRef, ethNode: EthereumNode, txPool: TxPoolRef): GraphqlHttpHandlerRef = let ctx = setupGraphqlContext(chain, ethNode, txPool) GraphqlHttpHandlerRef.new(ctx) {.pop.}