diff --git a/nimbus/rpc/hexstrings.nim b/nimbus/rpc/hexstrings.nim index 12e9bd4f1..7e411aff9 100644 --- a/nimbus/rpc/hexstrings.nim +++ b/nimbus/rpc/hexstrings.nim @@ -10,9 +10,18 @@ ## This module implements the Ethereum hexadecimal string formats for JSON ## See: https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding +import eth_common/eth_types, stint, byteutils, nimcrypto + type HexQuantityStr* = distinct string HexDataStr* = distinct string + EthAddressStr* = distinct string # Same as HexDataStr but must be less <= 20 bytes + EthHashStr* = distinct string # Same as HexDataStr but must be exactly 32 bytes + +func len*(quantity: HexQuantityStr): int = quantity.string.len +func len*(data: HexDataStr): int = data.string.len +func len*(data: EthAddressStr): int = data.string.len +func len*(data: EthHashStr): int = data.string.len # Hex validation @@ -60,9 +69,22 @@ func isValidHexData*(value: string): bool = return false return true +func isValidEthAddress*(value: string): bool = + # 20 bytes for EthAddress plus "0x" + # Addresses are allowed to be shorter than 20 bytes for convenience + result = value.len <= 42 and value.isValidHexData + +func isValidEthHash*(value: string): bool = + # 32 bytes for EthAddress plus "0x" + # Currently hashes are required to be exact lengths + # TODO: Allow shorter hashes (pad with zeros) for convenience? + result = value.len == 66 and value.isValidHexData + const SInvalidQuantity = "Invalid hex quantity format for Ethereum" SInvalidData = "Invalid hex data format for Ethereum" + SInvalidAddress = "Invalid address format for Ethereum" + SInvalidHash = "Invalid hash format for Ethereum" proc validateHexQuantity*(value: string) {.inline.} = if unlikely(not value.isValidHexQuantity): @@ -72,6 +94,14 @@ proc validateHexData*(value: string) {.inline.} = if unlikely(not value.isValidHexData): raise newException(ValueError, SInvalidData & ": " & value) +proc validateHexAddressStr*(value: string) {.inline.} = + if unlikely(not value.isValidEthAddress): + raise newException(ValueError, SInvalidAddress & ": " & value) + +proc validateHashStr*(value: string) {.inline.} = + if unlikely(not value.isValidEthHash): + raise newException(ValueError, SInvalidHash & ": " & value) + # Initialisation proc hexQuantityStr*(value: string): HexQuantityStr {.inline.} = @@ -82,6 +112,14 @@ proc hexDataStr*(value: string): HexDataStr {.inline.} = value.validateHexData result = value.HexDataStr +proc ethAddressStr*(value: string): EthAddressStr {.inline.} = + value.validateHexAddressStr + result = value.EthAddressStr + +proc ethHashStr*(value: string): EthHashStr {.inline.} = + value.validateHashStr + result = value.EthHashStr + # Converters for use in RPC import json @@ -93,6 +131,31 @@ proc `%`*(value: HexQuantityStr): JsonNode = proc `%`*(value: HexDataStr): JsonNode = result = %(value.string) +proc `%`*(value: EthAddressStr): JsonNode = + result = %(value.string) + +proc `%`*(value: EthHashStr): JsonNode = + result = %(value.string) + +# Overloads to support expected representation of hex data + +proc `%`*(value: EthAddress): JsonNode = + result = %("0x" & value.toHex) + +proc `%`*(value: Hash256): JsonNode = + result = %("0x" & $value) + +proc `%`*(value: UInt256): JsonNode = + result = %("0x" & value.toString) + +proc `%`*(value: openArray[seq]): JsonNode = + result = %("0x" & value.toHex) + +proc `%`*(value: ref BloomFilter): JsonNode = + result = %("0x" & toHex[256](value[])) + +# Marshalling from JSON to Nim types that includes format checking + proc fromJson*(n: JsonNode, argName: string, result: var HexQuantityStr) = # Note that '0x' is stripped after validation n.kind.expect(JString, argName) @@ -106,6 +169,22 @@ proc fromJson*(n: JsonNode, argName: string, result: var HexDataStr) = n.kind.expect(JString, argName) let hexStr = n.getStr() if not hexStr.isValidHexData: - raise newException(ValueError, "Parameter \"" & argName & "\" is not valid as a Ethereum data \"" & hexStr & "\"") + raise newException(ValueError, "Parameter \"" & argName & "\" is not valid as Ethereum data \"" & hexStr & "\"") result = hexStr.hexDataStr +proc fromJson*(n: JsonNode, argName: string, result: var EthAddressStr) = + # Note that '0x' is stripped after validation + n.kind.expect(JString, argName) + let hexStr = n.getStr() + if not hexStr.isValidEthAddress: + raise newException(ValueError, "Parameter \"" & argName & "\" is not valid as an Ethereum address \"" & hexStr & "\"") + result = hexStr.EthAddressStr + +proc fromJson*(n: JsonNode, argName: string, result: var EthHashStr) = + # Note that '0x' is stripped after validation + n.kind.expect(JString, argName) + let hexStr = n.getStr() + if not hexStr.isValidEthHash: + raise newException(ValueError, "Parameter \"" & argName & "\" is not valid as an Ethereum hash \"" & hexStr & "\"") + result = hexStr.EthHashStr + diff --git a/nimbus/rpc/p2p.nim b/nimbus/rpc/p2p.nim index e98fa2a43..e716d66d6 100644 --- a/nimbus/rpc/p2p.nim +++ b/nimbus/rpc/p2p.nim @@ -7,9 +7,25 @@ # This file may not be copied, modified, or distributed except according to # those terms. import - nimcrypto, json_rpc/server, eth_p2p, hexstrings, strutils, stint, + nimcrypto, json_rpc/rpcserver, eth_p2p, hexstrings, strutils, stint, ../config, ../vm_state, ../constants, eth_trie/[memdb, types], - ../db/[db_chain, state_db], eth_common + ../db/[db_chain, state_db], eth_common, rpc_types, byteutils, + ranges/typedranges, times, ../utils/header + +#[ + Note: + * Hexstring types (HexQuantitySt, HexDataStr, EthAddressStr, EthHashStr) + are parsed to check format before the RPC blocks are executed and will + raise an exception if invalid. + * Many of the RPC calls do not validate hex string types when output, only + type cast to avoid extra processing. +]# + +# Work around for https://github.com/nim-lang/Nim/issues/8645 +proc `%`*(value: Time): JsonNode = + result = %value.toSeconds + +func strToAddress(value: string): EthAddress = hexToPaddedByteArray[20](value) func headerFromTag(chain:BaseChainDB, blockTag: string): BlockHeader = let tag = blockTag.toLowerAscii @@ -25,23 +41,380 @@ func headerFromTag(chain:BaseChainDB, blockTag: string): BlockHeader = let blockNum = stint.fromHex(UInt256, tag) result = chain.getCanonicalBlockHeaderByNumber(blockNum) -proc setupP2PRPC*(server: EthereumNode, rpcsrv: RpcServer) = +proc setupP2PRPC*(node: EthereumNode, rpcsrv: RpcServer) = + template chain: untyped = BaseChainDB(node.chain) # TODO: Sensible casting + + proc accountDbFromTag(tag: string, readOnly = true): AccountStateDb = + # Note: This is a read only account + let + header = chain.headerFromTag(tag) + vmState = newBaseVMState(header, chain) + result = vmState.chaindb.getStateDb(vmState.blockHeader.hash, readOnly) + rpcsrv.rpc("net_version") do() -> uint: let conf = getConfiguration() result = conf.net.networkId + + rpcsrv.rpc("eth_syncing") do() -> JsonNode: + ## Returns SyncObject or false when not syncing. + # TODO: Requires PeerPool to check sync state. + # TODO: Use variant objects + var + res: JsonNode + sync: SyncState + if true: + # TODO: Populate sync state, this is a placeholder + sync.startingBlock = GENESIS_BLOCK_NUMBER + sync.currentBlock = chain.getCanonicalHead().blockNumber + sync.highestBlock = chain.getCanonicalHead().blockNumber + result = %sync + else: + result = newJBool(false) + + rpcsrv.rpc("eth_coinbase") do() -> EthAddress: + ## Returns the current coinbase address. + result = chain.getCanonicalHead().coinbase - rpcsrv.rpc("eth_getBalance") do(address: array[20, byte], quantityTag: string) -> int: + rpcsrv.rpc("eth_mining") do() -> bool: + ## Returns true if the client is mining, otherwise false. + discard + + rpcsrv.rpc("eth_hashrate") do() -> int: + ## Returns the number of hashes per second that the node is mining with. + discard + + rpcsrv.rpc("eth_gasPrice") do() -> int64: + ## Returns an integer of the current gas price in wei. + discard + + rpcsrv.rpc("eth_accounts") do() -> seq[EthAddressStr]: + ## Returns a list of addresses owned by client. + result = @[] + + rpcsrv.rpc("eth_blockNumber") do() -> BlockNumber: + ## Returns integer of the current block number the client is on. + result = chain.getCanonicalHead().blockNumber + + rpcsrv.rpc("eth_getBalance") do(data: EthAddressStr, quantityTag: string) -> int: ## Returns the balance of the account of given address. ## ## data: address to check for balance. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns integer of the current balance in wei. - template chain: untyped = BaseChainDB(server.chain) # TODO: Sensible casting + let + account_db = accountDbFromTag(quantityTag) + addrBytes = strToAddress(data.string) + balance = account_db.get_balance(addrBytes) + + result = balance.toInt + + rpcsrv.rpc("eth_getStorageAt") do(data: EthAddressStr, quantity: int, quantityTag: string) -> UInt256: + ## Returns the value from a storage position at a given address. + ## + ## data: address of the storage. + ## quantity: integer of the position in the storage. + ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. + ## Returns: the value at this storage position. + let + account_db = accountDbFromTag(quantityTag) + addrBytes = strToAddress(data.string) + storage = account_db.getStorage(addrBytes, quantity.u256) + if storage[1]: + result = storage[0] + + rpcsrv.rpc("eth_getTransactionCount") do(data: EthAddressStr, quantityTag: string) -> int: + ## Returns the number of transactions sent from an address. + ## + ## data: address. + ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. + ## Returns integer of the number of transactions send from this address. let header = chain.headerFromTag(quantityTag) - vmState = newBaseVMState(header, chain) - account_db = vmState.readOnlyStateDb - balance = account_db.get_balance(address) + body = chain.getBlockBody(header.hash) + result = body.transactions.len - return balance.toInt + rpcsrv.rpc("eth_getBlockTransactionCountByHash") do(data: HexDataStr) -> int: + ## Returns the number of transactions in a block from a block matching the given block hash. + ## + ## data: hash of a block + ## Returns integer of the number of transactions in this block. + var hashData: Hash256 + hashData.data = hexToPaddedByteArray[32](data.string) + let body = chain.getBlockBody(hashData) + result = body.transactions.len + + rpcsrv.rpc("eth_getBlockTransactionCountByNumber") do(quantityTag: string) -> int: + ## Returns the number of transactions in a block matching the given block number. + ## + ## data: integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. + ## Returns integer of the number of transactions in this block. + let + header = chain.headerFromTag(quantityTag) + body = chain.getBlockBody(header.hash) + result = body.transactions.len + + rpcsrv.rpc("eth_getUncleCountByBlockHash") do(data: HexDataStr) -> int: + ## Returns the number of uncles in a block from a block matching the given block hash. + ## + ## data: hash of a block. + ## Returns integer of the number of uncles in this block. + var hashData: Hash256 + hashData.data = hexToPaddedByteArray[32](data.string) + let body = chain.getBlockBody(hashData) + result = body.uncles.len + + rpcsrv.rpc("eth_getUncleCountByBlockNumber") do(quantityTag: string) -> int: + ## Returns the number of uncles in a block from a block matching the given block number. + ## + ## quantityTag: integer of a block number, or the string "latest", "earliest" or "pending", see the default block parameter. + ## Returns integer of uncles in this block. + let + header = chain.headerFromTag(quantityTag) + body = chain.getBlockBody(header.hash) + result = body.uncles.len + + rpcsrv.rpc("eth_getCode") do(data: EthAddressStr, quantityTag: string) -> HexDataStr: + ## Returns code at a given address. + ## + ## data: address + ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. + ## Returns the code from the given address. + let + account_db = accountDbFromTag(quantityTag) + addrBytes = strToAddress(data.string) + storage = account_db.getCode(addrBytes) + # Easier to return the string manually here rather than expect ByteRange to be marshalled + result = byteutils.toHex(storage.toOpenArray).HexDataStr + + rpcsrv.rpc("eth_sign") do(data: EthAddressStr, message: HexDataStr) -> HexDataStr: + ## The sign method calculates an Ethereum specific signature with: sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))). + ## By adding a prefix to the message makes the calculated signature recognisable as an Ethereum specific signature. + ## This prevents misuse where a malicious DApp can sign arbitrary data (e.g. transaction) and use the signature to impersonate the victim. + ## Note the address to sign with must be unlocked. + ## + ## data: address. + ## message: message to sign. + ## Returns signature. + discard + + rpcsrv.rpc("eth_sendTransaction") do(obj: EthSend) -> HexDataStr: + ## Creates new message call transaction or a contract creation, if the data field contains code. + ## + ## obj: the transaction object. + ## Returns the transaction hash, or the zero hash if the transaction is not yet available. + ## Note: Use eth_getTransactionReceipt to get the contract address, after the transaction was mined, when you created a contract. + discard + + rpcsrv.rpc("eth_sendRawTransaction") do(data: string, quantityTag: int) -> HexDataStr: + ## Creates new message call transaction or a contract creation for signed transactions. + ## + ## data: the signed transaction data. + ## Returns the transaction hash, or the zero hash if the transaction is not yet available. + ## Note: Use eth_getTransactionReceipt to get the contract address, after the transaction was mined, when you created a contract. + discard + + rpcsrv.rpc("eth_call") do(call: EthCall, quantityTag: string) -> HexDataStr: + ## Executes a new message call immediately without creating a transaction on the block chain. + ## + ## call: the transaction call object. + ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. + ## Returns the return value of executed contract. + discard + + rpcsrv.rpc("eth_estimateGas") do(call: EthCall, quantityTag: string) -> HexDataStr: # TODO: Int or U/Int256? + ## Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. + ## The transaction will not be added to the blockchain. Note that the estimate may be significantly more than + ## the amount of gas actually used by the transaction, for a variety of reasons including EVM mechanics and node performance. + ## + ## call: the transaction call object. + ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. + ## Returns the amount of gas used. + discard + + func populateBlockObject(header: BlockHeader, blockBody: BlockBodyRef): BlockObject = + result.number = new BlockNumber + result.number[] = header.blockNumber + result.hash = new Hash256 + result.hash[] = header.hash + result.parentHash = header.parentHash + result.nonce = header.nonce.toUint + + # Calculate hash for all uncle headers + var + rawdata = newSeq[byte](blockBody.uncles.len * 32) + startIdx = 0 + for i in 0 ..< blockBody.uncles.len: + rawData[startIdx .. startIdx + 32] = blockBody.uncles[i].hash.data + startIdx += 32 + result.sha3Uncles = keccak256.digest(rawData) + + result.logsBloom = nil # TODO: Create bloom filter for logs + result.transactionsRoot = header.txRoot + result.stateRoot = header.stateRoot + result.receiptsRoot = header.receiptRoot + result.miner = ZERO_ADDRESS # TODO: Get miner address + result.difficulty = header.difficulty + result.totalDifficulty = header.difficulty # TODO: Calculate + result.extraData = header.extraData + result.size = 0 # TODO: Calculate block size + result.gasLimit = header.gasLimit + result.gasUsed = header.gasUsed + result.timestamp = header.timeStamp + result.transactions = blockBody.transactions + result.uncles = blockBody.uncles + + rpcsrv.rpc("eth_getBlockByHash") do(data: HexDataStr, fullTransactions: bool) -> BlockObject: + ## Returns information about a block by hash. + ## + ## data: Hash of a block. + ## fullTransactions: If true it returns the full transaction objects, if false only the hashes of the transactions. + ## Returns BlockObject or nil when no block was found. + let + header = chain.getCanonicalHead() + body = chain.getBlockBody(header.hash) + populateBlockObject(header, body) + + rpcsrv.rpc("eth_getBlockByNumber") do(quantityTag: string, fullTransactions: bool) -> BlockObject: + ## Returns information about a block by block number. + ## + ## quantityTag: integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. + ## fullTransactions: If true it returns the full transaction objects, if false only the hashes of the transactions. + ## Returns BlockObject or nil when no block was found. + let + header = chain.headerFromTag(quantityTag) + body = chain.getBlockBody(header.hash) + populateBlockObject(header, body) + + rpcsrv.rpc("eth_getTransactionByHash") do(data: HexDataStr) -> TransactionObject: + ## Returns the information about a transaction requested by transaction hash. + ## + ## data: hash of a transaction. + ## Returns requested transaction information. + discard + + rpcsrv.rpc("eth_getTransactionByBlockHashAndIndex") do(data: HexDataStr, quantity: int) -> TransactionObject: + ## Returns information about a transaction by block hash and transaction index position. + ## + ## data: hash of a block. + ## quantity: integer of the transaction index position. + ## Returns requested transaction information. + discard + + rpcsrv.rpc("eth_getTransactionByBlockNumberAndIndex") do(quantityTag: string, quantity: int) -> TransactionObject: + ## Returns information about a transaction by block number and transaction index position. + ## + ## quantityTag: a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. + ## quantity: the transaction index position. + discard + + # Currently defined as a variant type so this might need rethinking + # See: https://github.com/status-im/nim-json-rpc/issues/29 + #[ + rpcsrv.rpc("eth_getTransactionReceipt") do(data: HexDataStr) -> ReceiptObject: + ## Returns the receipt of a transaction by transaction hash. + ## + ## data: hash of a transaction. + ## Returns transaction receipt. + discard + ]# + + rpcsrv.rpc("eth_getUncleByBlockHashAndIndex") do(data: HexDataStr, quantity: int64) -> BlockObject: + ## Returns information about a uncle of a block by hash and uncle index position. + ## + ## data: hash a block. + ## quantity: the uncle's index position. + ## Returns BlockObject or nil when no block was found. + discard + + rpcsrv.rpc("eth_getUncleByBlockNumberAndIndex") do(quantityTag: string, quantity: int64) -> BlockObject: + # Returns information about a uncle of a block by number and uncle index position. + ## + ## quantityTag: a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. + ## quantity: the uncle's index position. + ## Returns BlockObject or nil when no block was found. + discard + + # FilterOptions requires more layout planning. + # See: https://github.com/status-im/nim-json-rpc/issues/29 + #[ + rpcsrv.rpc("eth_newFilter") do(filterOptions: FilterOptions) -> int: + ## Creates a filter object, based on filter options, to notify when the state changes (logs). + ## To check if the state has changed, call eth_getFilterChanges. + ## Topics are order-dependent. A transaction with a log with topics [A, B] will be matched by the following topic filters: + ## [] "anything" + ## [A] "A in first position (and anything after)" + ## [null, B] "anything in first position AND B in second position (and anything after)" + ## [A, B] "A in first position AND B in second position (and anything after)" + ## [[A, B], [A, B]] "(A OR B) in first position AND (A OR B) in second position (and anything after)" + ## + ## filterOptions: settings for this filter. + ## Returns integer filter id. + discard + ]# + rpcsrv.rpc("eth_newBlockFilter") do() -> int: + ## Creates a filter in the node, to notify when a new block arrives. + ## To check if the state has changed, call eth_getFilterChanges. + ## + ## Returns integer filter id. + discard + + rpcsrv.rpc("eth_newPendingTransactionFilter") do() -> int: + ## Creates a filter in the node, to notify when a new block arrives. + ## To check if the state has changed, call eth_getFilterChanges. + ## + ## Returns integer filter id. + discard + + rpcsrv.rpc("eth_uninstallFilter") do(filterId: int) -> bool: + ## Uninstalls a filter with given id. Should always be called when watch is no longer needed. + ## Additonally Filters timeout when they aren't requested with eth_getFilterChanges for a period of time. + ## + ## filterId: The filter id. + ## Returns true if the filter was successfully uninstalled, otherwise false. + discard + + #[ + rpcsrv.rpc("eth_getFilterChanges") do(filterId: int) -> seq[LogObject]: + ## Polling method for a filter, which returns an list of logs which occurred since last poll. + ## + ## filterId: the filter id. + result = @[] + + rpcsrv.rpc("eth_getFilterLogs") do(filterId: int) -> seq[LogObject]: + ## filterId: the filter id. + ## Returns a list of all logs matching filter with given id. + result = @[] + + rpcsrv.rpc("eth_getLogs") do(filterOptions: FilterOptions) -> seq[LogObject]: + ## filterOptions: settings for this filter. + ## Returns a list of all logs matching a given filter object. + result = @[] + ]# + + rpcsrv.rpc("eth_getWork") do() -> seq[HexDataStr]: + ## Returns the hash of the current block, the seedHash, and the boundary condition to be met ("target"). + ## Returned list has the following properties: + ## DATA, 32 Bytes - current block header pow-hash. + ## DATA, 32 Bytes - the seed hash used for the DAG. + ## DATA, 32 Bytes - the boundary condition ("target"), 2^256 / difficulty. + result = @[] + + rpcsrv.rpc("eth_submitWork") do(nonce: int64, powHash: HexDataStr, mixDigest: HexDataStr) -> bool: + ## Used for submitting a proof-of-work solution. + ## + ## nonce: the nonce found. + ## headerPow: the header's pow-hash. + ## mixDigest: the mix digest. + ## Returns true if the provided solution is valid, otherwise false. + discard + + rpcsrv.rpc("eth_submitHashrate") do(hashRate: HexDataStr, id: HexDataStr) -> bool: + ## Used for submitting mining hashrate. + ## + ## hashRate: a hexadecimal string representation (32 bytes) of the hash rate. + ## id: a random hexadecimal(32 bytes) ID identifying the client. + ## Returns true if submitting went through succesfully and false otherwise. + discard + + diff --git a/nimbus/rpc/rpc_types.nim b/nimbus/rpc/rpc_types.nim new file mode 100644 index 000000000..ce86d7c2a --- /dev/null +++ b/nimbus/rpc/rpc_types.nim @@ -0,0 +1,90 @@ +import eth_common, hexstrings + +#[ + Notes: + * Some of the types suppose 'null' when there is no appropriate value. + To allow for this, currently these values are refs so the JSON transform can convert to `JNull`. + * Parameter objects from users must have their data verified so will use EthAddressStr instead of EthAddres, for example + * Objects returned to the user can use native Nimbus types, where hexstrings provides converters to hex strings. + This is because returned arrays in JSON is + a) not an efficient use of space + b) not the format the user expects (for example addresses are expected to be hex strings prefixed by "0x") +]# + +type + SyncState* = object + # Returned to user + startingBlock*: BlockNumber + currentBlock*: BlockNumber + highestBlock*: BlockNumber + + EthSend* = object + # Parameter from user + source*: EthAddressStr # the address the transaction is send from. + to*: EthAddressStr # (optional when creating new contract) the address the transaction is directed to. + gas*: GasInt # (optional, default: 90000) integer of the gas provided for the transaction execution. It will return unused gas. + gasPrice*: GasInt # (optional, default: To-Be-Determined) integer of the gasPrice used for each paid gas. + value*: int # (optional) integer of the value sent with this transaction. + data*: EthHashStr # TODO: Support more data. The compiled code of a contract OR the hash of the invoked method signature and encoded parameters. For details see Ethereum Contract ABI. + nonce*: int # (optional) integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce + + EthCall* = object + # Parameter from user + source*: EthAddressStr # (optional) The address the transaction is send from. + to*: EthAddressStr # The address the transaction is directed to. + gas*: GasInt # (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. + gasPrice*: GasInt # (optional) Integer of the gasPrice used for each paid gas. + value*: int # (optional) Integer of the value sent with this transaction. + data*: EthHashStr # (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. + + ## A block object, or null when no block was found + ## Note that this includes slightly different information from eth_common.BlockHeader + BlockObject* = ref object + # Returned to user + number*: ref BlockNumber # the block number. null when its pending block. + hash*: ref Hash256 # hash of the block. null when its pending block. + parentHash*: Hash256 # hash of the parent block. + nonce*: uint64 # hash of the generated proof-of-work. null when its pending block. + sha3Uncles*: Hash256 # SHA3 of the uncles data in the block. + logsBloom*: ref BloomFilter # the bloom filter for the logs of the block. null when its pending block. + transactionsRoot*: Hash256 # the root of the transaction trie of the block. + stateRoot*: Hash256 # the root of the final state trie of the block. + receiptsRoot*: Hash256 # the root of the receipts trie of the block. + miner*: EthAddress # the address of the beneficiary to whom the mining rewards were given. + difficulty*: UInt256 # integer of the difficulty for this block. + totalDifficulty*: UInt256 # integer of the total difficulty of the chain until this block. + extraData*: Blob # the "extra data" field of this block. + size*: int # integer the size of this block in bytes. + gasLimit*: GasInt # the maximum gas allowed in this block. + gasUsed*: GasInt # the total used gas by all transactions in this block. + timestamp*: EthTime # the unix timestamp for when the block was collated. + transactions*: seq[Transaction] # list of transaction objects, or 32 Bytes transaction hashes depending on the last given parameter. + uncles*: seq[BlockHeader] # list of uncle hashes. + + TransactionObject* = object # A transaction object, or null when no transaction was found: + # Returned to user + hash*: Hash256 # hash of the transaction. + nonce*: int64 # TODO: Is int? the number of transactions made by the sender prior to this one. + blockHash*: ref Hash256 # hash of the block where this transaction was in. null when its pending. + blockNumber*: ref BlockNumber # block number where this transaction was in. null when its pending. + transactionIndex*: ref int64 # integer of the transactions index position in the block. null when its pending. + source*: EthAddress # address of the sender. + to*: ref EthAddress # address of the receiver. null when its a contract creation transaction. + value*: int64 # value transferred in Wei. + gasPrice*: GasInt # gas price provided by the sender in Wei. + gas*: GasInt # gas provided by the sender. + input*: HexDataStr # the data send along with the transaction. + + LogObject* = object + # Returned to user + removed*: bool # true when the log was removed, due to a chain reorganization. false if its a valid log. + logIndex*: ref int # integer of the log index position in the block. null when its pending log. + transactionIndex*: ref int # integer of the transactions index position log was created from. null when its pending log. + transactionHash*: ref Hash256 # hash of the transactions this log was created from. null when its pending log. + blockHash*: ref Hash256 # hash of the block where this log was in. null when its pending. null when its pending log. + blockNumber*: ref BlockNumber # the block number where this log was in. null when its pending. null when its pending log. + address*: EthAddress # address from which this log originated. + data*: seq[Hash256] # contains one or more 32 Bytes non-indexed arguments of the log. + topics*: array[4, Hash256] # array of 0 to 4 32 Bytes DATA of indexed log arguments. + # (In solidity: The first topic is the hash of the signature of the event. + # (e.g. Deposit(address,bytes32,uint256)), except you declared the event with the anonymous specifier.) diff --git a/tests/rpcclient/ethcallsigs.nim b/tests/rpcclient/ethcallsigs.nim index 7fd109511..9527c36d1 100644 --- a/tests/rpcclient/ethcallsigs.nim +++ b/tests/rpcclient/ethcallsigs.nim @@ -1,7 +1,7 @@ ## This module contains signatures for the Ethereum client RPCs. ## The signatures are not imported directly, but read and processed with parseStmt, ## then a procedure body is generated to marshal native Nim parameters to json and visa versa. -import json, stint, eth_common +import json, stint, eth_common, ../../nimbus/rpc/hexstrings proc web3_clientVersion(): string proc web3_sha3(data: string): string @@ -10,21 +10,21 @@ proc net_peerCount(): int proc net_listening(): bool proc eth_protocolVersion(): string proc eth_syncing(): JsonNode -proc eth_coinbase(): string +proc eth_coinbase(): EthAddressStr proc eth_mining(): bool proc eth_hashrate(): int -proc eth_gasPrice(): int64 -proc eth_accounts(): seq[array[20, byte]] -proc eth_blockNumber(): int -proc eth_getBalance(data: array[20, byte], quantityTag: string): int -proc eth_getStorageAt(data: array[20, byte], quantity: int, quantityTag: string): seq[byte] -proc eth_getTransactionCount(data: array[20, byte], quantityTag: string) +proc eth_gasPrice(): GasInt +proc eth_accounts(): seq[EthAddressStr] +proc eth_blockNumber(): BlockNumber +proc eth_getBalance(data: EthAddressStr, quantityTag: string): int +proc eth_getStorageAt(data: EthAddressStr, quantity: int, quantityTag: string): seq[byte] +proc eth_getTransactionCount(data: EthAddressStr, quantityTag: string) proc eth_getBlockTransactionCountByHash(data: array[32, byte]) proc eth_getBlockTransactionCountByNumber(quantityTag: string) proc eth_getUncleCountByBlockHash(data: array[32, byte]) proc eth_getUncleCountByBlockNumber(quantityTag: string) -proc eth_getCode(data: array[20, byte], quantityTag: string): seq[byte] -proc eth_sign(data: array[20, byte], message: seq[byte]): seq[byte] +proc eth_getCode(data: EthAddressStr, quantityTag: string): HexDataStr +proc eth_sign(data:EthAddressStr, message: HexDataStr): HexDataStr # TODO: Use eth_common types diff --git a/tests/rpcclient/test_hexstrings.nim b/tests/rpcclient/test_hexstrings.nim index a7a7eff96..b5efd4014 100644 --- a/tests/rpcclient/test_hexstrings.nim +++ b/tests/rpcclient/test_hexstrings.nim @@ -97,3 +97,53 @@ proc doHexStrTests* = source = "0x0123" x = hexDataStr source check %x == %source + + suite "[RPC] Eth address strings": + test "Valid address": + let + e = "0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6" + e_addr = e.ethAddressStr + check e == e_addr.string + let + short_e = "0x0f572e5295c57f" + short_e_addr = short_e.ethAddressStr + check short_e == short_e_addr.string + test "Too long": + expect ValueError: + let + e = "0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec667" + e_addr = e.ethAddressStr + check e == e_addr.string + test "\"0x\" header": + expect ValueError: + let + # no 0x + e = "000f572e5295c57f15886f9b263e2f6d2d6c7b5ec6" + e_addr = e.ethAddressStr + check e == e_addr.string + + suite "[RPC] Eth hash strings": + test "Valid hash": + let + e = "0x1234567890123456789012345678901234567890123456789012345678901234" + e_addr = e.ethHashStr + check e == e_addr.string + test "Too short": + expect ValueError: + let + short_e = "0x12345678901234567890123456789012345678901234567890123456789012" + short_e_addr = short_e.ethHashStr + check short_e == short_e_addr.string + test "Too long": + expect ValueError: + let + e = "0x123456789012345678901234567890123456789012345678901234567890123456" + e_addr = e.ethHashStr + check e == e_addr.string + test "\"0x\" header": + expect ValueError: + let + # no 0x + e = "000x12345678901234567890123456789012" + e_addr = e.ethHashStr + check e == e_addr.string diff --git a/tests/test_rpc.nim b/tests/test_rpc.nim index 999e7af7f..3c81e6c13 100644 --- a/tests/test_rpc.nim +++ b/tests/test_rpc.nim @@ -1,7 +1,7 @@ import unittest, json, strformat, nimcrypto, rlp, json_rpc/[rpcserver, rpcclient], - ../nimbus/rpc/[common, p2p], + ../nimbus/rpc/[common, p2p, hexstrings], ../nimbus/constants, ../nimbus/nimbus/[account, vm_state, config], ../nimbus/db/[state_db, db_chain], eth_common, byteutils, @@ -35,6 +35,9 @@ proc setupEthNode: EthereumNode = result = newEthereumNode(keypair, srvAddress, conf.net.networkId, nil, "nimbus 0.1.0") +proc toEthAddressStr(address: EthAddress): EthAddressStr = + result = ("0x" & address.toHex).ethAddressStr + proc doTests = # TODO: Include other transports such as Http var ethNode = setupEthNode() @@ -46,8 +49,9 @@ proc doTests = state = newBaseVMState(header, chain) ethNode.chain = chain - let balance = 100.u256 - var address: EthAddress = hexToByteArray[20]("0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6") + let + balance = 100.u256 + address: EthAddress = hexToByteArray[20]("0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6") state.mutateStateDB: db.setBalance(address, balance) @@ -66,10 +70,10 @@ proc doTests = test "eth_getBalance": expect ValueError: # check error is raised on null address - var r = waitFor client.eth_getBalance(ZERO_ADDRESS, "0x0") + var r = waitFor client.eth_getBalance(ZERO_ADDRESS.toEthAddressStr, "0x0") let blockNum = state.blockheader.blockNumber - var r = waitFor client.eth_getBalance(address, "0x" & blockNum.toHex) + var r = waitFor client.eth_getBalance(address.toEthAddressStr, "0x" & blockNum.toHex) echo r rpcServer.stop()