diff --git a/Cargo.toml b/Cargo.toml index 64a11a340d10..441b6bae9507 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -404,6 +404,7 @@ members = [ "substrate/frame/revive/mock-network", "substrate/frame/revive/proc-macro", "substrate/frame/revive/rpc", + "substrate/frame/revive/rpc/codegen", "substrate/frame/revive/uapi", "substrate/frame/root-offences", "substrate/frame/root-testing", diff --git a/prdoc/pr_5926.prdoc b/prdoc/pr_5926.prdoc new file mode 100644 index 000000000000..f05aeb93eb71 --- /dev/null +++ b/prdoc/pr_5926.prdoc @@ -0,0 +1,13 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: "[pallet-revive] add codegen for Ethereum RPC API" + +doc: + - audience: Runtime Dev + description: | + Add codegen crate for generating Ethereum RPC methods and types from the spec. + +crates: + - name: pallet-revive-rpc-codegen + bump: patch diff --git a/substrate/frame/revive/rpc/codegen/Cargo.toml b/substrate/frame/revive/rpc/codegen/Cargo.toml new file mode 100644 index 000000000000..65d8ba501b1c --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pallet-revive-rpc-codegen" +version = "0.1.0" +edition.workspace = true +publish = false + +[dependencies] +Inflector = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +anyhow = { workspace = true } + +[dev-dependencies] +pretty_assertions.workspace = true + +[features] +default = ["std"] +std = ["anyhow/std", "serde/std", "serde_json/std"] diff --git a/substrate/frame/revive/rpc/codegen/README.md b/substrate/frame/revive/rpc/codegen/README.md new file mode 100644 index 000000000000..2ca838f0db23 --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/README.md @@ -0,0 +1,5 @@ +Generates the Ethereum JSON-RPC API from the official specification. + +- See +- See building instructions to re-generate the openrpc.json +- Include fixes from diff --git a/substrate/frame/revive/rpc/codegen/openrpc.json b/substrate/frame/revive/rpc/codegen/openrpc.json new file mode 100644 index 000000000000..4a91ee18177c --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/openrpc.json @@ -0,0 +1,2271 @@ +{ + "openrpc": "1.2.4", + "info": { + "title": "Ethereum JSON-RPC Specification", + "description": "A specification of the standard interface for Ethereum clients.", + "license": { + "name": "CC0-1.0", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "version": "0.0.0" + }, + "methods": [ + { + "name": "eth_accounts", + "summary": "Returns a list of addresses owned by client.", + "params": [], + "result": { + "name": "Accounts", + "schema": { + "title": "Accounts", + "type": "array", + "items": { + "$ref": "#/components/schemas/address" + } + } + } + }, + { + "name": "eth_blobBaseFee", + "summary": "Returns the base fee per blob gas in wei.", + "params": [], + "result": { + "name": "Blob gas base fee", + "schema": { + "title": "Blob gas base fee", + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_blockNumber", + "summary": "Returns the number of most recent block.", + "params": [], + "result": { + "name": "Block number", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_call", + "summary": "Executes a new message call immediately without creating a transaction on the block chain.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/GenericTransaction" + } + }, + { + "name": "Block", + "required": false, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Return data", + "schema": { + "$ref": "#/components/schemas/bytes" + } + } + }, + { + "name": "eth_chainId", + "summary": "Returns the chain ID of the current network.", + "params": [], + "result": { + "name": "Chain ID", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_coinbase", + "summary": "Returns the client coinbase address.", + "params": [], + "result": { + "name": "Coinbase address", + "schema": { + "$ref": "#/components/schemas/address" + } + } + }, + { + "name": "eth_createAccessList", + "summary": "Generates an access list for a transaction.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/GenericTransaction" + } + }, + { + "name": "Block", + "required": false, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + } + ], + "result": { + "name": "Gas used", + "schema": { + "title": "Access list result", + "type": "object", + "additionalProperties": false, + "properties": { + "accessList": { + "title": "accessList", + "$ref": "#/components/schemas/AccessList" + }, + "error": { + "title": "error", + "type": "string" + }, + "gasUsed": { + "title": "Gas used", + "$ref": "#/components/schemas/uint" + } + } + } + } + }, + { + "name": "eth_estimateGas", + "summary": "Generates and returns an estimate of how much gas is necessary to allow the transaction to complete.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/GenericTransaction" + } + }, + { + "name": "Block", + "required": false, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + } + ], + "result": { + "name": "Gas used", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_feeHistory", + "summary": "Transaction fee history", + "description": "Returns transaction base fee per gas and effective priority fee per gas for the requested/supported block range.", + "params": [ + { + "name": "blockCount", + "description": "Requested range of blocks. Clients will return less than the requested range if not all blocks are available.", + "required": true, + "schema": { + "$ref": "#/components/schemas/uint" + } + }, + { + "name": "newestBlock", + "description": "Highest block of the requested range.", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + }, + { + "name": "rewardPercentiles", + "description": "A monotonically increasing list of percentile values. For each block in the requested range, the transactions will be sorted in ascending order by effective tip per gas and the coresponding effective tip for the percentile will be determined, accounting for gas consumed.", + "required": true, + "schema": { + "title": "rewardPercentiles", + "type": "array", + "items": { + "title": "rewardPercentile", + "description": "Floating point value between 0 and 100.", + "type": "number" + } + } + } + ], + "result": { + "name": "feeHistoryResult", + "description": "Fee history for the returned block range. This can be a subsection of the requested range if not all blocks are available.", + "schema": { + "title": "feeHistoryResults", + "description": "Fee history results.", + "type": "object", + "required": [ + "oldestBlock", + "baseFeePerGas", + "gasUsedRatio" + ], + "additionalProperties": false, + "properties": { + "oldestBlock": { + "title": "oldestBlock", + "description": "Lowest number block of returned range.", + "$ref": "#/components/schemas/uint" + }, + "baseFeePerGas": { + "title": "baseFeePerGasArray", + "description": "An array of block base fees per gas. This includes the next block after the newest of the returned range, because this value can be derived from the newest block. Zeroes are returned for pre-EIP-1559 blocks.", + "type": "array", + "items": { + "$ref": "#/components/schemas/uint" + } + }, + "baseFeePerBlobGas": { + "title": "baseFeePerBlobGasArray", + "description": "An array of block base fees per blob gas. This includes the next block after the newest of the returned range, because this value can be derived from the newest block. Zeroes are returned for pre-EIP-4844 blocks.", + "type": "array", + "items": { + "$ref": "#/components/schemas/uint" + } + }, + "gasUsedRatio": { + "title": "gasUsedRatio", + "description": "An array of block gas used ratios. These are calculated as the ratio of gasUsed and gasLimit.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ratio" + } + }, + "blobGasUsedRatio": { + "title": "blobGasUsedRatio", + "description": "An array of block blob gas used ratios. These are calculated as the ratio of blobGasUsed and the max blob gas per block.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ratio" + } + }, + "reward": { + "title": "rewardArray", + "description": "A two-dimensional array of effective priority fees per gas at the requested block percentiles.", + "type": "array", + "items": { + "title": "rewardPercentile", + "description": "An array of effective priority fee per gas data points from a single block. All zeroes are returned if the block is empty.", + "type": "array", + "items": { + "title": "rewardPercentile", + "description": "A given percentile sample of effective priority fees per gas from a single block in ascending order, weighted by gas used. Zeroes are returned if the block is empty.", + "$ref": "#/components/schemas/uint" + } + } + } + } + } + } + }, + { + "name": "eth_gasPrice", + "summary": "Returns the current price per gas in wei.", + "params": [], + "result": { + "name": "Gas price", + "schema": { + "title": "Gas price", + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_getBalance", + "summary": "Returns the balance of the account of given address.", + "params": [ + { + "name": "Address", + "required": true, + "schema": { + "$ref": "#/components/schemas/address" + } + }, + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Balance", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_getBlockByHash", + "summary": "Returns information about a block by hash.", + "params": [ + { + "name": "Block hash", + "required": true, + "schema": { + "$ref": "#/components/schemas/hash32" + } + }, + { + "name": "Hydrated transactions", + "required": true, + "schema": { + "title": "hydrated", + "type": "boolean" + } + } + ], + "result": { + "name": "Block information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "$ref": "#/components/schemas/Block" + } + ] + } + } + }, + { + "name": "eth_getBlockByNumber", + "summary": "Returns information about a block by number.", + "params": [ + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + }, + { + "name": "Hydrated transactions", + "required": true, + "schema": { + "title": "hydrated", + "type": "boolean" + } + } + ], + "result": { + "name": "Block information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "$ref": "#/components/schemas/Block" + } + ] + } + } + }, + { + "name": "eth_getBlockReceipts", + "summary": "Returns the receipts of a block by number or hash.", + "params": [ + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Receipts information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "title": "Receipts information", + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceiptInfo" + } + } + ] + } + } + }, + { + "name": "eth_getBlockTransactionCountByHash", + "summary": "Returns the number of transactions in a block from a block matching the given block hash.", + "params": [ + { + "name": "Block hash", + "schema": { + "$ref": "#/components/schemas/hash32" + } + } + ], + "result": { + "name": "Transaction count", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "title": "Transaction count", + "$ref": "#/components/schemas/uint" + } + ] + } + } + }, + { + "name": "eth_getBlockTransactionCountByNumber", + "summary": "Returns the number of transactions in a block matching the given block number.", + "params": [ + { + "name": "Block", + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + } + ], + "result": { + "name": "Transaction count", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "title": "Transaction count", + "$ref": "#/components/schemas/uint" + } + ] + } + } + }, + { + "name": "eth_getCode", + "summary": "Returns code at a given address.", + "params": [ + { + "name": "Address", + "required": true, + "schema": { + "$ref": "#/components/schemas/address" + } + }, + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Bytecode", + "schema": { + "$ref": "#/components/schemas/bytes" + } + } + }, + { + "name": "eth_getFilterChanges", + "summary": "Polling method for a filter, which returns an array of logs which occurred since last poll.", + "params": [ + { + "name": "Filter Identifier", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + ], + "result": { + "name": "Log objects", + "schema": { + "$ref": "#/components/schemas/FilterResults" + } + } + }, + { + "name": "eth_getFilterLogs", + "summary": "Returns an array of all logs matching filter with given id.", + "params": [ + { + "name": "Filter Identifier", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + ], + "result": { + "name": "Log objects", + "schema": { + "$ref": "#/components/schemas/FilterResults" + } + } + }, + { + "name": "eth_getLogs", + "summary": "Returns an array of all logs matching filter with given id.", + "params": [ + { + "name": "Filter", + "schema": { + "$ref": "#/components/schemas/Filter" + } + } + ], + "result": { + "name": "Log objects", + "schema": { + "$ref": "#/components/schemas/FilterResults" + } + } + }, + { + "name": "eth_getProof", + "summary": "Returns the merkle proof for a given account and optionally some storage keys.", + "params": [ + { + "name": "Address", + "required": true, + "schema": { + "$ref": "#/components/schemas/address" + } + }, + { + "name": "StorageKeys", + "required": true, + "schema": { + "title": "Storage keys", + "type": "array", + "items": { + "$ref": "#/components/schemas/bytesMax32" + } + } + }, + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Account", + "schema": { + "$ref": "#/components/schemas/AccountProof" + } + } + }, + { + "name": "eth_getStorageAt", + "summary": "Returns the value from a storage position at a given address.", + "params": [ + { + "name": "Address", + "required": true, + "schema": { + "$ref": "#/components/schemas/address" + } + }, + { + "name": "Storage slot", + "required": true, + "schema": { + "$ref": "#/components/schemas/uint256" + } + }, + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Value", + "schema": { + "$ref": "#/components/schemas/bytes" + } + } + }, + { + "name": "eth_getTransactionByBlockHashAndIndex", + "summary": "Returns information about a transaction by block hash and transaction index position.", + "params": [ + { + "name": "Block hash", + "required": true, + "schema": { + "$ref": "#/components/schemas/hash32" + } + }, + { + "name": "Transaction index", + "required": true, + "schema": { + "$ref": "#/components/schemas/uint" + } + } + ], + "result": { + "name": "Transaction information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "$ref": "#/components/schemas/TransactionInfo" + } + ] + } + } + }, + { + "name": "eth_getTransactionByBlockNumberAndIndex", + "summary": "Returns information about a transaction by block number and transaction index position.", + "params": [ + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + }, + { + "name": "Transaction index", + "required": true, + "schema": { + "$ref": "#/components/schemas/uint" + } + } + ], + "result": { + "name": "Transaction information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "$ref": "#/components/schemas/TransactionInfo" + } + ] + } + } + }, + { + "name": "eth_getTransactionByHash", + "summary": "Returns the information about a transaction requested by transaction hash.", + "params": [ + { + "name": "Transaction hash", + "required": true, + "schema": { + "$ref": "#/components/schemas/hash32" + } + } + ], + "result": { + "name": "Transaction information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "$ref": "#/components/schemas/TransactionInfo" + } + ] + } + } + }, + { + "name": "eth_getTransactionCount", + "summary": "Returns the number of transactions sent from an address.", + "params": [ + { + "name": "Address", + "required": true, + "schema": { + "$ref": "#/components/schemas/address" + } + }, + { + "name": "Block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Transaction count", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_getTransactionReceipt", + "summary": "Returns the receipt of a transaction by transaction hash.", + "params": [ + { + "name": "Transaction hash", + "required": true, + "schema": { + "$ref": "#/components/schemas/hash32" + } + } + ], + "result": { + "name": "Receipt information", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "$ref": "#/components/schemas/ReceiptInfo" + } + ] + } + } + }, + { + "name": "eth_getUncleCountByBlockHash", + "summary": "Returns the number of uncles in a block from a block matching the given block hash.", + "params": [ + { + "name": "Block hash", + "schema": { + "$ref": "#/components/schemas/hash32" + } + } + ], + "result": { + "name": "Uncle count", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "title": "Uncle count", + "$ref": "#/components/schemas/uint" + } + ] + } + } + }, + { + "name": "eth_getUncleCountByBlockNumber", + "summary": "Returns the number of transactions in a block matching the given block number.", + "params": [ + { + "name": "Block", + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + } + ], + "result": { + "name": "Uncle count", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/notFound" + }, + { + "title": "Uncle count", + "$ref": "#/components/schemas/uint" + } + ] + } + } + }, + { + "name": "eth_maxPriorityFeePerGas", + "summary": "Returns the current maxPriorityFeePerGas per gas in wei.", + "params": [], + "result": { + "name": "Max priority fee per gas", + "schema": { + "title": "Max priority fee per gas", + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_newBlockFilter", + "summary": "Creates a filter in the node, to notify when a new block arrives.", + "params": [], + "result": { + "name": "Filter Identifier", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_newFilter", + "summary": "Creates a filter object, based on filter options, to notify when the state changes (logs).", + "params": [ + { + "name": "Filter", + "schema": { + "$ref": "#/components/schemas/Filter" + } + } + ], + "result": { + "name": "Filter Identifier", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_newPendingTransactionFilter", + "summary": "Creates a filter in the node, to notify when new pending transactions arrive.", + "params": [], + "result": { + "name": "Filter Identifier", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + }, + { + "name": "eth_sendRawTransaction", + "summary": "Submits a raw transaction. For EIP-4844 transactions, the raw form must be the network form. This means it includes the blobs, KZG commitments, and KZG proofs.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/bytes" + } + } + ], + "result": { + "name": "Transaction hash", + "schema": { + "$ref": "#/components/schemas/hash32" + } + } + }, + { + "name": "eth_sendTransaction", + "summary": "Signs and submits a transaction.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/GenericTransaction" + } + } + ], + "result": { + "name": "Transaction hash", + "schema": { + "$ref": "#/components/schemas/hash32" + } + } + }, + { + "name": "eth_sign", + "summary": "Returns an EIP-191 signature over the provided data.", + "params": [ + { + "name": "Address", + "required": true, + "schema": { + "$ref": "#/components/schemas/address" + } + }, + { + "name": "Message", + "required": true, + "schema": { + "$ref": "#/components/schemas/bytes" + } + } + ], + "result": { + "name": "Signature", + "schema": { + "$ref": "#/components/schemas/bytes65" + } + } + }, + { + "name": "eth_signTransaction", + "summary": "Returns an RLP encoded transaction signed by the specified account.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/GenericTransaction" + } + } + ], + "result": { + "name": "Encoded transaction", + "schema": { + "$ref": "#/components/schemas/bytes" + } + } + }, + { + "name": "eth_syncing", + "summary": "Returns an object with data about the sync status or false.", + "params": [], + "result": { + "name": "Syncing status", + "schema": { + "$ref": "#/components/schemas/SyncingStatus" + } + } + }, + { + "name": "eth_uninstallFilter", + "summary": "Uninstalls a filter with given id.", + "params": [ + { + "name": "Filter Identifier", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + ], + "result": { + "name": "Success", + "schema": { + "type": "boolean" + } + } + } + ], + "components": { + "schemas": { + "address": { + "title": "hex encoded address", + "type": "string", + "pattern": "^0x[0-9a-fA-F]{40}$" + }, + "addresses": { + "title": "hex encoded address", + "type": "array", + "items": { + "$ref": "#/components/schemas/address" + } + }, + "byte": { + "title": "hex encoded byte", + "type": "string", + "pattern": "^0x([0-9a-fA-F]?){1,2}$" + }, + "bytes": { + "title": "hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]*$" + }, + "bytesMax32": { + "title": "32 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{0,64}$" + }, + "bytes8": { + "title": "8 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{16}$" + }, + "bytes32": { + "title": "32 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{64}$" + }, + "bytes48": { + "title": "48 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{96}$" + }, + "bytes96": { + "title": "96 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{192}$" + }, + "bytes256": { + "title": "256 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{512}$" + }, + "bytes65": { + "title": "65 hex encoded bytes", + "type": "string", + "pattern": "^0x[0-9a-f]{130}$" + }, + "ratio": { + "title": "normalized ratio", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "uint": { + "title": "hex encoded unsigned integer", + "type": "string", + "pattern": "^0x([1-9a-f]+[0-9a-f]*|0)$" + }, + "uint64": { + "title": "hex encoded 64 bit unsigned integer", + "type": "string", + "pattern": "^0x([1-9a-f]+[0-9a-f]{0,15})|0$" + }, + "uint256": { + "title": "hex encoded 256 bit unsigned integer", + "type": "string", + "pattern": "^0x([1-9a-f]+[0-9a-f]{0,31})|0$" + }, + "hash32": { + "title": "32 byte hex value", + "type": "string", + "pattern": "^0x[0-9a-f]{64}$" + }, + "notFound": { + "title": "Not Found (null)", + "type": "null" + }, + "Block": { + "title": "Block object", + "type": "object", + "required": [ + "hash", + "parentHash", + "sha3Uncles", + "miner", + "stateRoot", + "transactionsRoot", + "receiptsRoot", + "logsBloom", + "number", + "gasLimit", + "gasUsed", + "timestamp", + "extraData", + "mixHash", + "nonce", + "size", + "transactions", + "uncles" + ], + "additionalProperties": false, + "properties": { + "hash": { + "title": "Hash", + "$ref": "#/components/schemas/hash32" + }, + "parentHash": { + "title": "Parent block hash", + "$ref": "#/components/schemas/hash32" + }, + "sha3Uncles": { + "title": "Ommers hash", + "$ref": "#/components/schemas/hash32" + }, + "miner": { + "title": "Coinbase", + "$ref": "#/components/schemas/address" + }, + "stateRoot": { + "title": "State root", + "$ref": "#/components/schemas/hash32" + }, + "transactionsRoot": { + "title": "Transactions root", + "$ref": "#/components/schemas/hash32" + }, + "receiptsRoot": { + "title": "Receipts root", + "$ref": "#/components/schemas/hash32" + }, + "logsBloom": { + "title": "Bloom filter", + "$ref": "#/components/schemas/bytes256" + }, + "difficulty": { + "title": "Difficulty", + "$ref": "#/components/schemas/uint" + }, + "number": { + "title": "Number", + "$ref": "#/components/schemas/uint" + }, + "gasLimit": { + "title": "Gas limit", + "$ref": "#/components/schemas/uint" + }, + "gasUsed": { + "title": "Gas used", + "$ref": "#/components/schemas/uint" + }, + "timestamp": { + "title": "Timestamp", + "$ref": "#/components/schemas/uint" + }, + "extraData": { + "title": "Extra data", + "$ref": "#/components/schemas/bytes" + }, + "mixHash": { + "title": "Mix hash", + "$ref": "#/components/schemas/hash32" + }, + "nonce": { + "title": "Nonce", + "$ref": "#/components/schemas/bytes8" + }, + "totalDifficulty": { + "title": "Total difficulty", + "$ref": "#/components/schemas/uint" + }, + "baseFeePerGas": { + "title": "Base fee per gas", + "$ref": "#/components/schemas/uint" + }, + "withdrawalsRoot": { + "title": "Withdrawals root", + "$ref": "#/components/schemas/hash32" + }, + "blobGasUsed": { + "title": "Blob gas used", + "$ref": "#/components/schemas/uint" + }, + "excessBlobGas": { + "title": "Excess blob gas", + "$ref": "#/components/schemas/uint" + }, + "parentBeaconBlockRoot": { + "title": "Parent Beacon Block Root", + "$ref": "#/components/schemas/hash32" + }, + "size": { + "title": "Block size", + "$ref": "#/components/schemas/uint" + }, + "transactions": { + "anyOf": [ + { + "title": "Transaction hashes", + "type": "array", + "items": { + "$ref": "#/components/schemas/hash32" + } + }, + { + "title": "Full transactions", + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionInfo" + } + } + ] + }, + "withdrawals": { + "title": "Withdrawals", + "type": "array", + "items": { + "$ref": "#/components/schemas/Withdrawal" + } + }, + "uncles": { + "title": "Uncles", + "type": "array", + "items": { + "$ref": "#/components/schemas/hash32" + } + } + } + }, + "BlockTag": { + "title": "Block tag", + "type": "string", + "enum": [ + "earliest", + "finalized", + "safe", + "latest", + "pending" + ], + "description": "`earliest`: The lowest numbered block the client has available; `finalized`: The most recent crypto-economically secure block, cannot be re-orged outside of manual intervention driven by community coordination; `safe`: The most recent block that is safe from re-orgs under honest majority and certain synchronicity assumptions; `latest`: The most recent block in the canonical chain observed by the client, this block may be re-orged out of the canonical chain even under healthy/normal conditions; `pending`: A sample next block built by the client on top of `latest` and containing the set of transactions usually taken from local mempool. Before the merge transition is finalized, any call querying for `finalized` or `safe` block MUST be responded to with `-39001: Unknown block` error" + }, + "BlockNumberOrTag": { + "title": "Block number or tag", + "oneOf": [ + { + "title": "Block number", + "$ref": "#/components/schemas/uint" + }, + { + "title": "Block tag", + "$ref": "#/components/schemas/BlockTag" + } + ] + }, + "BlockNumberOrTagOrHash": { + "title": "Block number, tag, or block hash", + "anyOf": [ + { + "title": "Block number", + "$ref": "#/components/schemas/uint" + }, + { + "title": "Block tag", + "$ref": "#/components/schemas/BlockTag" + }, + { + "title": "Block hash", + "$ref": "#/components/schemas/hash32" + } + ] + }, + "BadBlock": { + "title": "Bad block", + "type": "object", + "required": [ + "block", + "hash", + "rlp" + ], + "additionalProperties": false, + "properties": { + "block": { + "title": "Block", + "$ref": "#/components/schemas/Block" + }, + "hash": { + "title": "Hash", + "$ref": "#/components/schemas/hash32" + }, + "rlp": { + "title": "RLP", + "$ref": "#/components/schemas/bytes" + } + } + }, + "SyncingStatus": { + "title": "Syncing status", + "oneOf": [ + { + "title": "Syncing progress", + "type": "object", + "additionalProperties": false, + "properties": { + "startingBlock": { + "title": "Starting block", + "$ref": "#/components/schemas/uint" + }, + "currentBlock": { + "title": "Current block", + "$ref": "#/components/schemas/uint" + }, + "highestBlock": { + "title": "Highest block", + "$ref": "#/components/schemas/uint" + } + } + }, + { + "title": "Not syncing", + "description": "Should always return false if not syncing.", + "type": "boolean" + } + ] + }, + "FilterResults": { + "title": "Filter results", + "oneOf": [ + { + "title": "new block or transaction hashes", + "type": "array", + "items": { + "$ref": "#/components/schemas/hash32" + } + }, + { + "title": "new logs", + "type": "array", + "items": { + "$ref": "#/components/schemas/Log" + } + } + ] + }, + "Filter": { + "title": "filter", + "type": "object", + "additionalProperties": false, + "properties": { + "fromBlock": { + "title": "from block", + "$ref": "#/components/schemas/uint" + }, + "toBlock": { + "title": "to block", + "$ref": "#/components/schemas/uint" + }, + "address": { + "title": "Address(es)", + "oneOf": [ + { + "title": "Any Address", + "type": "null" + }, + { + "title": "Address", + "$ref": "#/components/schemas/address" + }, + { + "title": "Addresses", + "$ref": "#/components/schemas/addresses" + } + ] + }, + "topics": { + "title": "Topics", + "$ref": "#/components/schemas/FilterTopics" + } + } + }, + "FilterTopics": { + "title": "Filter Topics", + "oneOf": [ + { + "title": "Any Topic Match", + "type": "null" + }, + { + "title": "Specified Filter Topics", + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterTopic" + } + } + ] + }, + "FilterTopic": { + "title": "Filter Topic List Entry", + "oneOf": [ + { + "title": "Single Topic Match", + "$ref": "#/components/schemas/bytes32" + }, + { + "title": "Multiple Topic Match", + "type": "array", + "items": { + "$ref": "#/components/schemas/bytes32" + } + } + ] + }, + "Log": { + "title": "log", + "type": "object", + "required": [ + "transactionHash", + "address" + ], + "additionalProperties": false, + "properties": { + "removed": { + "title": "removed", + "type": "boolean" + }, + "logIndex": { + "title": "log index", + "$ref": "#/components/schemas/uint" + }, + "transactionIndex": { + "title": "transaction index", + "$ref": "#/components/schemas/uint" + }, + "transactionHash": { + "title": "transaction hash", + "$ref": "#/components/schemas/hash32" + }, + "blockHash": { + "title": "block hash", + "$ref": "#/components/schemas/hash32" + }, + "blockNumber": { + "title": "block number", + "$ref": "#/components/schemas/uint" + }, + "address": { + "title": "address", + "$ref": "#/components/schemas/address" + }, + "data": { + "title": "data", + "$ref": "#/components/schemas/bytes" + }, + "topics": { + "title": "topics", + "type": "array", + "items": { + "$ref": "#/components/schemas/bytes32" + } + } + } + }, + "ReceiptInfo": { + "type": "object", + "title": "Receipt information", + "required": [ + "blockHash", + "blockNumber", + "from", + "cumulativeGasUsed", + "gasUsed", + "logs", + "logsBloom", + "transactionHash", + "transactionIndex", + "effectiveGasPrice" + ], + "additionalProperties": false, + "properties": { + "type": { + "title": "type", + "$ref": "#/components/schemas/byte" + }, + "transactionHash": { + "title": "transaction hash", + "$ref": "#/components/schemas/hash32" + }, + "transactionIndex": { + "title": "transaction index", + "$ref": "#/components/schemas/uint" + }, + "blockHash": { + "title": "block hash", + "$ref": "#/components/schemas/hash32" + }, + "blockNumber": { + "title": "block number", + "$ref": "#/components/schemas/uint" + }, + "from": { + "title": "from", + "$ref": "#/components/schemas/address" + }, + "to": { + "title": "to", + "description": "Address of the receiver or null in a contract creation transaction.", + "oneOf": [ + { + "title": "Contract Creation (null)", + "type": "null" + }, + { + "title": "Recipient Address", + "$ref": "#/components/schemas/address" + } + ] + }, + "cumulativeGasUsed": { + "title": "cumulative gas used", + "description": "The sum of gas used by this transaction and all preceding transactions in the same block.", + "$ref": "#/components/schemas/uint" + }, + "gasUsed": { + "title": "gas used", + "description": "The amount of gas used for this specific transaction alone.", + "$ref": "#/components/schemas/uint" + }, + "blobGasUsed": { + "title": "blob gas used", + "description": "The amount of blob gas used for this specific transaction. Only specified for blob transactions as defined by EIP-4844.", + "$ref": "#/components/schemas/uint" + }, + "contractAddress": { + "title": "contract address", + "description": "The contract address created, if the transaction was a contract creation, otherwise null.", + "oneOf": [ + { + "$ref": "#/components/schemas/address" + }, + { + "title": "Null", + "type": "null" + } + ] + }, + "logs": { + "title": "logs", + "type": "array", + "items": { + "$ref": "#/components/schemas/Log" + } + }, + "logsBloom": { + "title": "logs bloom", + "$ref": "#/components/schemas/bytes256" + }, + "root": { + "title": "state root", + "description": "The post-transaction state root. Only specified for transactions included before the Byzantium upgrade.", + "$ref": "#/components/schemas/hash32" + }, + "status": { + "title": "status", + "description": "Either 1 (success) or 0 (failure). Only specified for transactions included after the Byzantium upgrade.", + "$ref": "#/components/schemas/uint" + }, + "effectiveGasPrice": { + "title": "effective gas price", + "description": "The actual value per gas deducted from the sender's account. Before EIP-1559, this is equal to the transaction's gas price. After, it is equal to baseFeePerGas + min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas).", + "$ref": "#/components/schemas/uint" + }, + "blobGasPrice": { + "title": "blob gas price", + "description": "The actual value per gas deducted from the sender's account for blob gas. Only specified for blob transactions as defined by EIP-4844.", + "$ref": "#/components/schemas/uint" + } + } + }, + "AccountProof": { + "title": "Account proof", + "type": "object", + "required": [ + "address", + "accountProof", + "balance", + "codeHash", + "nonce", + "storageHash", + "storageProof" + ], + "additionalProperties": false, + "properties": { + "address": { + "title": "address", + "$ref": "#/components/schemas/address" + }, + "accountProof": { + "title": "accountProof", + "type": "array", + "items": { + "$ref": "#/components/schemas/bytes" + } + }, + "balance": { + "title": "balance", + "$ref": "#/components/schemas/uint256" + }, + "codeHash": { + "title": "codeHash", + "$ref": "#/components/schemas/hash32" + }, + "nonce": { + "title": "nonce", + "$ref": "#/components/schemas/uint64" + }, + "storageHash": { + "title": "storageHash", + "$ref": "#/components/schemas/hash32" + }, + "storageProof": { + "title": "Storage proofs", + "type": "array", + "items": { + "$ref": "#/components/schemas/StorageProof" + } + } + } + }, + "StorageProof": { + "title": "Storage proof", + "type": "object", + "required": [ + "key", + "value", + "proof" + ], + "additionalProperties": false, + "properties": { + "key": { + "title": "key", + "$ref": "#/components/schemas/bytesMax32" + }, + "value": { + "title": "value", + "$ref": "#/components/schemas/uint256" + }, + "proof": { + "title": "proof", + "type": "array", + "items": { + "$ref": "#/components/schemas/bytes" + } + } + } + }, + "Transaction4844Unsigned": { + "type": "object", + "title": "EIP-4844 transaction.", + "required": [ + "type", + "nonce", + "to", + "gas", + "value", + "input", + "maxPriorityFeePerGas", + "maxFeePerGas", + "maxFeePerBlobGas", + "accessList", + "blobVersionedHashes", + "chainId" + ], + "properties": { + "type": { + "title": "type", + "type": "string", + "pattern": "^0x3$" + }, + "nonce": { + "title": "nonce", + "$ref": "#/components/schemas/uint" + }, + "to": { + "title": "to address", + "$ref": "#/components/schemas/address" + }, + "gas": { + "title": "gas limit", + "$ref": "#/components/schemas/uint" + }, + "value": { + "title": "value", + "$ref": "#/components/schemas/uint" + }, + "input": { + "title": "input data", + "$ref": "#/components/schemas/bytes" + }, + "maxPriorityFeePerGas": { + "title": "max priority fee per gas", + "description": "Maximum fee per gas the sender is willing to pay to miners in wei", + "$ref": "#/components/schemas/uint" + }, + "maxFeePerGas": { + "title": "max fee per gas", + "description": "The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei", + "$ref": "#/components/schemas/uint" + }, + "maxFeePerBlobGas": { + "title": "max fee per blob gas", + "description": "The maximum total fee per gas the sender is willing to pay for blob gas in wei", + "$ref": "#/components/schemas/uint" + }, + "accessList": { + "title": "accessList", + "description": "EIP-2930 access list", + "$ref": "#/components/schemas/AccessList" + }, + "blobVersionedHashes": { + "title": "blobVersionedHashes", + "description": "List of versioned blob hashes associated with the transaction's EIP-4844 data blobs.", + "type": "array", + "items": { + "$ref": "#/components/schemas/hash32" + } + }, + "chainId": { + "title": "chainId", + "description": "Chain ID that this transaction is valid on.", + "$ref": "#/components/schemas/uint" + } + } + }, + "AccessListEntry": { + "title": "Access list entry", + "type": "object", + "additionalProperties": false, + "required": [ "address", "storageKeys" ], + "properties": { + "address": { + "$ref": "#/components/schemas/address" + }, + "storageKeys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/hash32" + } + } + } + }, + "AccessList": { + "title": "Access list", + "type": "array", + "items": { + "$ref": "#/components/schemas/AccessListEntry" + } + }, + "Transaction1559Unsigned": { + "type": "object", + "title": "EIP-1559 transaction.", + "required": [ + "type", + "nonce", + "gas", + "value", + "input", + "maxFeePerGas", + "maxPriorityFeePerGas", + "gasPrice", + "chainId", + "accessList" + ], + "properties": { + "type": { + "title": "type", + "type": "string", + "pattern": "^0x2$" + }, + "nonce": { + "title": "nonce", + "$ref": "#/components/schemas/uint" + }, + "to": { + "title": "to address", + "oneOf": [ + { + "title": "Contract Creation (null)", + "type": "null" + }, + { + "title": "Address", + "$ref": "#/components/schemas/address" + } + ] + }, + "gas": { + "title": "gas limit", + "$ref": "#/components/schemas/uint" + }, + "value": { + "title": "value", + "$ref": "#/components/schemas/uint" + }, + "input": { + "title": "input data", + "$ref": "#/components/schemas/bytes" + }, + "maxPriorityFeePerGas": { + "title": "max priority fee per gas", + "description": "Maximum fee per gas the sender is willing to pay to miners in wei", + "$ref": "#/components/schemas/uint" + }, + "maxFeePerGas": { + "title": "max fee per gas", + "description": "The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei", + "$ref": "#/components/schemas/uint" + }, + "gasPrice": { + "title": "gas price", + "description": "The effective gas price paid by the sender in wei. For transactions not yet included in a block, this value should be set equal to the max fee per gas. This field is DEPRECATED, please transition to using effectiveGasPrice in the receipt object going forward.", + "$ref": "#/components/schemas/uint" + }, + "accessList": { + "title": "accessList", + "description": "EIP-2930 access list", + "$ref": "#/components/schemas/AccessList" + }, + "chainId": { + "title": "chainId", + "description": "Chain ID that this transaction is valid on.", + "$ref": "#/components/schemas/uint" + } + } + }, + "Transaction2930Unsigned": { + "type": "object", + "title": "EIP-2930 transaction.", + "required": [ + "type", + "nonce", + "gas", + "value", + "input", + "gasPrice", + "chainId", + "accessList" + ], + "properties": { + "type": { + "title": "type", + "type": "string", + "pattern": "^0x1$" + }, + "nonce": { + "title": "nonce", + "$ref": "#/components/schemas/uint" + }, + "to": { + "title": "to address", + "oneOf": [ + { + "title": "Contract Creation (null)", + "type": "null" + }, + { + "title": "Address", + "$ref": "#/components/schemas/address" + } + ] + }, + "gas": { + "title": "gas limit", + "$ref": "#/components/schemas/uint" + }, + "value": { + "title": "value", + "$ref": "#/components/schemas/uint" + }, + "input": { + "title": "input data", + "$ref": "#/components/schemas/bytes" + }, + "gasPrice": { + "title": "gas price", + "description": "The gas price willing to be paid by the sender in wei", + "$ref": "#/components/schemas/uint" + }, + "accessList": { + "title": "accessList", + "description": "EIP-2930 access list", + "$ref": "#/components/schemas/AccessList" + }, + "chainId": { + "title": "chainId", + "description": "Chain ID that this transaction is valid on.", + "$ref": "#/components/schemas/uint" + } + } + }, + "TransactionLegacyUnsigned": { + "type": "object", + "title": "Legacy transaction.", + "required": [ + "type", + "nonce", + "gas", + "value", + "input", + "gasPrice" + ], + "properties": { + "type": { + "title": "type", + "type": "string", + "pattern": "^0x0$" + }, + "nonce": { + "title": "nonce", + "$ref": "#/components/schemas/uint" + }, + "to": { + "title": "to address", + "oneOf": [ + { + "title": "Contract Creation (null)", + "type": "null" + }, + { + "title": "Address", + "$ref": "#/components/schemas/address" + } + ] + }, + "gas": { + "title": "gas limit", + "$ref": "#/components/schemas/uint" + }, + "value": { + "title": "value", + "$ref": "#/components/schemas/uint" + }, + "input": { + "title": "input data", + "$ref": "#/components/schemas/bytes" + }, + "gasPrice": { + "title": "gas price", + "description": "The gas price willing to be paid by the sender in wei", + "$ref": "#/components/schemas/uint" + }, + "chainId": { + "title": "chainId", + "description": "Chain ID that this transaction is valid on.", + "$ref": "#/components/schemas/uint" + } + } + }, + "TransactionUnsigned": { + "oneOf": [ + { + "$ref": "#/components/schemas/Transaction4844Unsigned" + }, + { + "$ref": "#/components/schemas/Transaction1559Unsigned" + }, + { + "$ref": "#/components/schemas/Transaction2930Unsigned" + }, + { + "$ref": "#/components/schemas/TransactionLegacyUnsigned" + } + ] + }, + "Transaction4844Signed": { + "title": "Signed 4844 Transaction", + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Transaction4844Unsigned" + }, + { + "title": "EIP-4844 transaction signature properties.", + "required": [ + "r", + "s", + "yParity" + ], + "properties": { + "yParity": { + "title": "yParity", + "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature.", + "$ref": "#/components/schemas/uint" + }, + "r": { + "title": "r", + "$ref": "#/components/schemas/uint" + }, + "s": { + "title": "s", + "$ref": "#/components/schemas/uint" + } + } + } + ] + }, + "Transaction1559Signed": { + "title": "Signed 1559 Transaction", + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Transaction1559Unsigned" + }, + { + "title": "EIP-1559 transaction signature properties.", + "required": [ + "r", + "s", + "yParity" + ], + "properties": { + "yParity": { + "title": "yParity", + "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature.", + "$ref": "#/components/schemas/uint" + }, + "v": { + "title": "v", + "description": "For backwards compatibility, `v` is optionally provided as an alternative to `yParity`. This field is DEPRECATED and all use of it should migrate to `yParity`.", + "$ref": "#/components/schemas/uint" + }, + "r": { + "title": "r", + "$ref": "#/components/schemas/uint" + }, + "s": { + "title": "s", + "$ref": "#/components/schemas/uint" + } + } + } + ] + }, + "Transaction2930Signed": { + "title": "Signed 2930 Transaction", + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Transaction2930Unsigned" + }, + { + "title": "EIP-2930 transaction signature properties.", + "required": [ + "yParity", + "r", + "s" + ], + "properties": { + "yParity": { + "title": "yParity", + "description": "The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature.", + "$ref": "#/components/schemas/uint" + }, + "v": { + "title": "v", + "description": "For backwards compatibility, `v` is optionally provided as an alternative to `yParity`. This field is DEPRECATED and all use of it should migrate to `yParity`.", + "$ref": "#/components/schemas/uint" + }, + "r": { + "title": "r", + "$ref": "#/components/schemas/uint" + }, + "s": { + "title": "s", + "$ref": "#/components/schemas/uint" + } + } + } + ] + }, + "TransactionLegacySigned": { + "title": "Signed Legacy Transaction", + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TransactionLegacyUnsigned" + }, + { + "title": "Legacy transaction signature properties.", + "required": [ + "v", + "r", + "s" + ], + "properties": { + "v": { + "title": "v", + "$ref": "#/components/schemas/uint" + }, + "r": { + "title": "r", + "$ref": "#/components/schemas/uint" + }, + "s": { + "title": "s", + "$ref": "#/components/schemas/uint" + } + } + } + ] + }, + "TransactionSigned": { + "oneOf": [ + { + "$ref": "#/components/schemas/Transaction4844Signed" + }, + { + "$ref": "#/components/schemas/Transaction1559Signed" + }, + { + "$ref": "#/components/schemas/Transaction2930Signed" + }, + { + "$ref": "#/components/schemas/TransactionLegacySigned" + } + ] + }, + "TransactionInfo": { + "type": "object", + "title": "Transaction information", + "allOf": [ + { + "title": "Contextual information", + "required": [ + "blockHash", + "blockNumber", + "from", + "hash", + "transactionIndex" + ], + "unevaluatedProperties": false, + "properties": { + "blockHash": { + "title": "block hash", + "$ref": "#/components/schemas/hash32" + }, + "blockNumber": { + "title": "block number", + "$ref": "#/components/schemas/uint" + }, + "from": { + "title": "from address", + "$ref": "#/components/schemas/address" + }, + "hash": { + "title": "transaction hash", + "$ref": "#/components/schemas/hash32" + }, + "transactionIndex": { + "title": "transaction index", + "$ref": "#/components/schemas/uint" + } + } + }, + { + "$ref": "#/components/schemas/TransactionSigned" + } + ] + }, + "GenericTransaction": { + "type": "object", + "title": "Transaction object generic to all types", + "additionalProperties": false, + "properties": { + "type": { + "title": "type", + "$ref": "#/components/schemas/byte" + }, + "nonce": { + "title": "nonce", + "$ref": "#/components/schemas/uint" + }, + "to": { + "title": "to address", + "oneOf": [ + { + "title": "Contract Creation (null)", + "type": "null" + }, + { + "title": "Address", + "$ref": "#/components/schemas/address" + } + ] + }, + "from": { + "title": "from address", + "$ref": "#/components/schemas/address" + }, + "gas": { + "title": "gas limit", + "$ref": "#/components/schemas/uint" + }, + "value": { + "title": "value", + "$ref": "#/components/schemas/uint" + }, + "input": { + "title": "input data", + "$ref": "#/components/schemas/bytes" + }, + "gasPrice": { + "title": "gas price", + "description": "The gas price willing to be paid by the sender in wei", + "$ref": "#/components/schemas/uint" + }, + "maxPriorityFeePerGas": { + "title": "max priority fee per gas", + "description": "Maximum fee per gas the sender is willing to pay to miners in wei", + "$ref": "#/components/schemas/uint" + }, + "maxFeePerGas": { + "title": "max fee per gas", + "description": "The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei", + "$ref": "#/components/schemas/uint" + }, + "maxFeePerBlobGas": { + "title": "max fee per blob gas", + "description": "The maximum total fee per gas the sender is willing to pay for blob gas in wei", + "$ref": "#/components/schemas/uint" + }, + "accessList": { + "title": "accessList", + "description": "EIP-2930 access list", + "$ref": "#/components/schemas/AccessList" + }, + "blobVersionedHashes": { + "title": "blobVersionedHashes", + "description": "List of versioned blob hashes associated with the transaction's EIP-4844 data blobs.", + "type": "array", + "items": { + "$ref": "#/components/schemas/hash32" + } + }, + "blobs": { + "title": "blobs", + "description": "Raw blob data.", + "type": "array", + "items": { + "$ref": "#/components/schemas/bytes" + } + }, + "chainId": { + "title": "chainId", + "description": "Chain ID that this transaction is valid on.", + "$ref": "#/components/schemas/uint" + } + } + }, + "Withdrawal": { + "type": "object", + "title": "Validator withdrawal", + "required": [ + "index", + "validatorIndex", + "address", + "amount" + ], + "additionalProperties": false, + "properties": { + "index": { + "title": "index of withdrawal", + "$ref": "#/components/schemas/uint64" + }, + "validatorIndex": { + "title": "index of validator that generated withdrawal", + "$ref": "#/components/schemas/uint64" + }, + "address": { + "title": "recipient address for withdrawal value", + "$ref": "#/components/schemas/address" + }, + "amount": { + "title": "value contained in withdrawal", + "$ref": "#/components/schemas/uint256" + } + } + } + } + } +} diff --git a/substrate/frame/revive/rpc/codegen/src/LICENSE.txt b/substrate/frame/revive/rpc/codegen/src/LICENSE.txt new file mode 100644 index 000000000000..ecd364a6d62e --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/src/LICENSE.txt @@ -0,0 +1,16 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/substrate/frame/revive/rpc/codegen/src/generator.rs b/substrate/frame/revive/rpc/codegen/src/generator.rs new file mode 100644 index 000000000000..c4881a186c50 --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/src/generator.rs @@ -0,0 +1,758 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use inflector::Inflector; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + mem, + sync::LazyLock, +}; + +use crate::{ + open_rpc::*, + printer::{ + doc_str_from_schema, Fields, Required, TypeContent, TypeInfo, TypeNameProvider, + TypePrinter, Variants, + }, + writeln, +}; + +pub const LICENSE: &str = include_str!("LICENSE.txt"); + +/// List of supported Ethereum RPC methods we want to generate. +pub static SUPPORTED_ETH_METHODS: LazyLock> = LazyLock::new(|| { + vec![ + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_estimateGas", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_syncing", + "net_version", + ] +}); + +/// Mapping of primitive schema types to their Rust counterparts. +pub static PRIMITIVE_MAPPINGS: LazyLock> = + LazyLock::new(|| { + HashMap::from([ + ("#/components/schemas/address", "Address"), + ("#/components/schemas/byte", "Byte"), + ("#/components/schemas/bytes", "Bytes"), + ("#/components/schemas/bytes256", "Bytes256"), + ("#/components/schemas/hash32", "H256"), + ("#/components/schemas/bytes32", "H256"), + ("#/components/schemas/bytes8", "Bytes8"), + ("#/components/schemas/uint", "U256"), + ("#/components/schemas/uint256", "U256"), + ("#/components/schemas/uint64", "U256"), + ]) + }); + +/// Mapping of legacy aliases to their new names. +pub static LEGACY_ALIASES: LazyLock>> = + LazyLock::new(|| { + HashMap::from([ + // We accept "data" and "input" for backwards-compatibility reasons. + // Issue detail: https://github.com/ethereum/go-ethereum/issues/15628 + ("#/components/schemas/GenericTransaction", HashMap::from([("input", "data")])), + ]) + }); + +/// Custom Default impl +pub static CUSTOM_DEFAULT_VARIANTS: LazyLock> = + LazyLock::new(|| { + HashMap::from([ + ("TransactionUnsigned", "TransactionLegacyUnsigned"), + ("TransactionSigned", "TransactionLegacySigned"), + ("BlockNumberOrTagOrHash", "BlockTag"), + ("BlockNumberOrTag", "BlockTag"), + ("BlockTag", "Latest"), + ]) + }); + +/// Read the OpenRPC specs, and inject extra methods and legacy aliases. +pub fn read_specs() -> anyhow::Result { + let content = include_str!("../openrpc.json"); + let mut specs: OpenRpc = serde_json::from_str(content)?; + + // Inject legacy aliases. + inject_legacy_aliases(&mut specs); + + // Inject extra methods. + specs.methods.push(RefOr::Inline(Method { + name: "net_version".to_string(), + summary: Some("The string value of current network id".to_string()), + result: Some(RefOr::Reference { reference: "String".to_string() }), + ..Default::default() + })); + + Ok(specs) +} + +// Inject legacy aliases declared by [`LEGACY_ALIASES`]. +pub fn inject_legacy_aliases(specs: &mut OpenRpc) { + for (alias, mapping) in LEGACY_ALIASES.iter() { + let schema = specs.get_schema_mut(alias).unwrap(); + match &mut schema.contents { + SchemaContents::Object(o) | SchemaContents::Literal(Literal::Object(o)) => { + o.legacy_aliases = + mapping.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); + }, + _ => { + panic!("Alias should be an object got {:?} instead", schema.contents); + }, + } + } +} + +/// Format the given code using rustfmt. +pub fn format_code(code: &str) -> anyhow::Result { + use std::{io::Write, process::*}; + let mut rustfmt = Command::new("rustup") + .args(["run", "nightly", "rustfmt"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + let stdin = rustfmt.stdin.as_mut().expect("Failed to open stdin"); + stdin.write_all(code.as_bytes())?; + + let output = rustfmt.wait_with_output()?; + if !output.status.success() { + anyhow::bail!("rustfmt failed: {}", String::from_utf8_lossy(&output.stderr)); + } + + let formatted_code = String::from_utf8_lossy(&output.stdout).to_string(); + Ok(formatted_code) +} + +/// Type generator for generating RPC methods and types. +#[derive(Default)] +pub struct TypeGenerator { + /// List of collected types, that are not yet generated. + collected: BTreeMap, + /// List of already generated types. + generated: HashSet, + /// List of filtered method names, we want to generate. + filtered_method_names: HashSet, + /// Stripped prefix for the generated method names. + prefix: String, +} + +/// Reference or schema +pub enum ReferenceOrSchema { + // A reference to a schema such as `#/components/schemas/Foo`. + Reference(String), + // A schema definition. + Schema(Schema), +} + +impl ReferenceOrSchema { + /// Return the schema for the reference or the schema itself. + fn schema<'a>(&'a self, specs: &'a OpenRpc) -> &'a Schema { + match self { + Self::Schema(schema) => schema, + Self::Reference(reference) => specs.get_schema(reference).unwrap(), + } + } +} + +impl TypeGenerator { + /// Create a new type generator. + pub fn new() -> Self { + let mut generated = + HashSet::from_iter(["notFound"].into_iter().map(|name| name.to_pascal_case())); + + generated.extend(PRIMITIVE_MAPPINGS.keys().map(|name| reference_to_name(name))); + generated.extend(PRIMITIVE_MAPPINGS.values().map(|name| name.to_string())); + let filtered_method_names = + SUPPORTED_ETH_METHODS.iter().map(|name| name.to_string()).collect(); + + Self { + collected: Default::default(), + filtered_method_names, + generated, + prefix: "eth".to_string(), + } + } + + /// Generate the RPC method, and add the collected types. + pub fn generate_rpc_methods(&mut self, specs: &OpenRpc) -> String { + let methods = specs + .methods + .iter() + .map(RefOr::unwrap_inline) + .filter(|method| self.filtered_method_names.contains(&method.name)) + .collect::>(); + + if methods.len() != self.filtered_method_names.len() { + let available = + methods.iter().map(|method| method.name.clone()).collect::>(); + let missing = self.filtered_method_names.difference(&available).collect::>(); + panic!("Missing methods: {missing:?}"); + } + + let mut code = LICENSE.to_string(); + code.push_str( + r#" + //! Generated JSON-RPC methods. + #![allow(missing_docs)] + + use super::*; + use jsonrpsee::core::RpcResult; + use jsonrpsee::proc_macros::rpc; + + #[rpc(server, client)] + pub trait EthRpc { + "#, + ); + + for method in methods { + self.generate_rpc_method(&mut code, method); + code.push('\n'); + } + code.push('}'); + code.push('\n'); + code + } + + pub fn collect_extra_type(&mut self, type_name: &str) { + self.collect( + type_name, + ReferenceOrSchema::Reference(format!("#/components/schemas/{}", type_name)), + ); + } + + /// Recursively collect the types and generate them. + /// + /// Note: This should be called after [`TypeGenerator::generate_rpc_methods`] to collect the + /// types used in the RPC methods. + pub fn generate_types(&mut self, specs: &OpenRpc) -> String { + let mut code = LICENSE.to_string(); + code.push_str( + r#"//! Generated JSON-RPC types. + #![allow(missing_docs)] + + use super::{byte::*, TypeEip1559, TypeEip2930, TypeEip4844, TypeLegacy}; + use alloc::vec::Vec; + use codec::{Decode, Encode}; + use derive_more::{From, TryInto}; + pub use ethereum_types::*; + use scale_info::TypeInfo; + use serde::{Deserialize, Serialize}; + + "#, + ); + loop { + let collected = mem::take(&mut self.collected); + self.generated.extend(collected.keys().cloned()); + + if collected.is_empty() { + break; + } + + for (name, ref_or_schema) in collected { + let r#type = self.generate_type(name, ref_or_schema.schema(specs)); + r#type.print(&mut code); + code.push('\n'); + } + } + + code + } + + /// Return the type printer for the given schema. + fn generate_type(&mut self, name: String, schema: &Schema) -> TypePrinter { + let doc = doc_str_from_schema(schema); + + let content = match &schema.contents { + &SchemaContents::Literal(Literal::Object(ref o)) | &SchemaContents::Object(ref o) => + TypeContent::Struct(Fields::from(o, self)), + SchemaContents::AllOf { all_of } => + TypeContent::Struct(Fields::from_all_of(all_of, self)), + &SchemaContents::AnyOf { any_of: ref items } | + &SchemaContents::OneOf { one_of: ref items } => + TypeContent::Enum(Variants::from_one_of(items, self)), + &SchemaContents::Literal(Literal::Array(ArrayLiteral { items: Some(ref schema) })) => { + let mut type_info = + self.type_info(schema).expect("Anonymous array type not supported"); + type_info.array = true; + + TypeContent::TypeAlias(type_info) + }, + &SchemaContents::Literal(Literal::String(StringLiteral { + min_length: None, + max_length: None, + pattern: None, + format: None, + enumeration: Some(ref enumeration), + })) => TypeContent::UntaggedEnum(enumeration.clone()), + v => { + panic!("Unsupported type {name} {v:#?}") + }, + }; + + let default_variant = CUSTOM_DEFAULT_VARIANTS.get(&name.as_str()).map(|v| v.to_string()); + TypePrinter::new(doc, name, content, default_variant) + } + + fn generate_rpc_method(&mut self, buffer: &mut String, method: &Method) { + let Method { ref summary, ref name, ref params, ref result, .. } = method; + writeln!(@doc buffer, summary); + + let result = result + .as_ref() + .map(|content| match content { + RefOr::Inline(descriptor) => self + .type_info(&descriptor.schema) + .expect("Result type should be defined") + .get_type(), + RefOr::Reference { reference } => reference.clone(), + }) + .unwrap_or("()".to_string()); + + let parameters = params + .iter() + .map(RefOr::unwrap_inline) + .map(|ContentDescriptor { name, required, schema, .. }| { + let name_arg = name.to_snake_case().replace(' ', "_"); + let name_type = self + .type_info(schema) + .expect("Parameter type should be defined") + .set_required(*required) + .get_type(); + format!("{name_arg}: {name_type}") + }) + .collect::>() + .join(", "); + + writeln!(buffer, "#[method(name = \"{name}\")]"); + let method_name = name.trim_start_matches(&self.prefix).to_snake_case(); + writeln!(buffer, "async fn {method_name}(&self, {parameters}) -> RpcResult<{result}>;"); + } + + /// Collect the type if it's not yet generated or collected. + fn collect(&mut self, type_name: &str, ref_or_schema: ReferenceOrSchema) { + if !self.generated.contains(type_name) && !self.collected.contains_key(type_name) { + self.collected.insert(type_name.to_string(), ref_or_schema); + } + } +} + +/// Convert a reference to a type name. +fn reference_to_name(reference: &str) -> String { + if PRIMITIVE_MAPPINGS.contains_key(reference) { + return PRIMITIVE_MAPPINGS[reference].to_string(); + } + reference.split('/').last().unwrap().to_pascal_case() +} + +impl TypeNameProvider for TypeGenerator { + fn record_inline_type(&mut self, type_name: String, schema: &Schema) -> TypeInfo { + self.collect(&type_name, ReferenceOrSchema::Schema(schema.clone())); + TypeInfo { name: type_name, required: Required::Yes, array: false } + } + + fn type_info(&mut self, schema: &Schema) -> Option { + match &schema.contents { + SchemaContents::Reference { reference } => { + let type_name = reference_to_name(reference); + self.collect(&type_name, ReferenceOrSchema::Reference(reference.to_string())); + Some(type_name.into()) + }, + SchemaContents::Literal(Literal::Array(ArrayLiteral { items: Some(ref schema) })) => { + let mut type_info = + self.type_info(schema).expect("Anonymous array type not supported"); + type_info.array = true; + Some(type_info) + }, + SchemaContents::AllOf { all_of } => Some( + all_of + .iter() + .map(|s| self.type_info(s).expect("Anonymous all_of type not supported").name) + .collect::>() + .join("And") + .into(), + ), + SchemaContents::AnyOf { any_of: ref items } | + SchemaContents::OneOf { one_of: ref items } => { + let mut required = Required::Yes; + let items = items + .iter() + .filter_map(|s| { + let info = self.type_info(s).expect("Anonymous any_of type not supported"); + let name = info.name; + + if name == "Null" || name == "NotFound" { + required = Required::No { skip_if_null: false }; + None + } else { + Some(name) + } + }) + .collect::>(); + + let name = items.join("Or"); + if items.len() > 1 { + self.collect(&name, ReferenceOrSchema::Schema(schema.clone())); + } + + Some(TypeInfo { name, required, array: false }) + }, + SchemaContents::Literal(Literal::Null) => Some("Null".into()), + + // Use Type0, Type1, Type2, ... for String that have a single digit pattern. + SchemaContents::Literal(Literal::String(StringLiteral { + min_length: None, + max_length: None, + pattern: Some(ref pattern), + format: None, + enumeration: None, + })) if ["^0x0$", "^0x1$", "^0x2$", "^0x3$"].contains(&pattern.as_str()) => + match pattern.as_str() { + "^0x0$" => Some("TypeLegacy".into()), + "^0x1$" => Some("TypeEip2930".into()), + "^0x2$" => Some("TypeEip1559".into()), + "^0x3$" => Some("TypeEip4844".into()), + _ => unreachable!(), + }, + + SchemaContents::Literal(Literal::Boolean) => Some("bool".into()), + SchemaContents::Object(_) => None, + SchemaContents::Literal(Literal::Object(_)) => None, + v => { + panic!("No type name for {v:#?}"); + }, + } + } +} + +#[cfg(test)] +pub fn assert_code_match(expected: &str, actual: &str) { + pretty_assertions::assert_eq!( + format_code(expected).unwrap().trim(), + format_code(actual).unwrap().trim() + ); +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn generate_works() { + let specs = read_specs().unwrap(); + + let mut generator = TypeGenerator::new(); + SUPPORTED_ETH_METHODS.iter().for_each(|name| { + generator.filtered_method_names.insert(name.to_string()); + }); + + let buffer = generator.generate_rpc_methods(&specs); + println!("{}", buffer); + } + + #[test] + fn generate_rpc_works() { + let method = serde_json::from_str::( + r###" + { + "name": "eth_estimateGas", + "summary": "Generates and returns an estimate of how much gas is necessary to allow the transaction to complete.", + "params": [ + { + "name": "Transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/GenericTransaction" + } + }, + { + "name": "Block", + "required": false, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTag" + } + } + ], + "result": { + "name": "Gas used", + "schema": { + "$ref": "#/components/schemas/uint" + } + } + } + "###, + ) + .unwrap(); + + let mut buffer = String::new(); + let mut generator = TypeGenerator::new(); + + generator.generate_rpc_method(&mut buffer, &method); + assert_code_match( + &buffer, + r#" + /// Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. + #[method(name = "eth_estimateGas")] + async fn estimate_gas(&self, transaction: GenericTransaction, block: Option) -> RpcResult; + "#, + ); + } + + #[test] + fn generate_type_name_works() { + let mut generator = TypeGenerator::new(); + + let schema: Schema = serde_json::from_str( + r###" + { + "title": "to address", + "oneOf": [ + { "title": "Contract Creation (null)", "type": "null" }, + { "title": "Address", "$ref": "#/components/schemas/address" } + ] + } + "###, + ) + .unwrap(); + + assert_eq!(&generator.type_info(&schema).unwrap().get_type(), "Option
"); + } + + #[test] + fn generate_all_off_type_works() { + let specs = read_specs().unwrap(); + let mut generator = TypeGenerator::new(); + let res = generator.generate_type( + "Transaction4844Signed".to_string(), + specs.get_schema("#/components/schemas/Transaction4844Signed").unwrap(), + ); + let mut buffer = String::new(); + res.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// Signed 4844 Transaction + #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq)] + pub struct Transaction4844Signed { + #[serde(flatten)] + pub transaction_4844_unsigned: Transaction4844Unsigned, + /// r + pub r: U256, + /// s + pub s: U256, + /// yParity + /// The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature. + #[serde(rename = "yParity")] + pub y_parity: U256, + } + "#, + ); + } + + #[test] + fn generate_one_of_type_works() { + let specs = read_specs().unwrap(); + let mut generator = TypeGenerator::new(); + let res = generator.generate_type( + "TransactionUnsigned".to_string(), + specs.get_schema("#/components/schemas/TransactionUnsigned").unwrap(), + ); + let mut buffer = String::new(); + res.print(&mut buffer); + assert_code_match( + &buffer, + r#" + #[derive(Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq)] + #[serde(untagged)] + pub enum TransactionUnsigned { + Transaction4844Unsigned(Transaction4844Unsigned), + Transaction1559Unsigned(Transaction1559Unsigned), + Transaction2930Unsigned(Transaction2930Unsigned), + TransactionLegacyUnsigned(TransactionLegacyUnsigned), + } + impl Default for TransactionUnsigned { + fn default() -> Self { + TransactionUnsigned::TransactionLegacyUnsigned(Default::default()) + } + } + "#, + ); + } + + #[test] + fn generate_type_with_inline_variant_works() { + let specs = read_specs().unwrap(); + let mut generator = TypeGenerator::new(); + let res = generator.generate_type( + "SyncingStatus".to_string(), + specs.get_schema("#/components/schemas/SyncingStatus").unwrap(), + ); + let mut buffer = String::new(); + res.print(&mut buffer); + + assert_code_match( + &buffer, + r#" + /// Syncing status + #[derive(Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq)] + #[serde(untagged)] + pub enum SyncingStatus { + /// Syncing progress + SyncingProgress(SyncingProgress), + /// Not syncing + /// Should always return false if not syncing. + Bool(bool), + } + impl Default for SyncingStatus { + fn default() -> Self { + SyncingStatus::SyncingProgress(Default::default()) + } + } + "#, + ); + } + + #[test] + fn generate_array_type_works() { + let specs = read_specs().unwrap(); + let mut generator = TypeGenerator::new(); + let res = generator.generate_type( + "AccessList".to_string(), + specs.get_schema("#/components/schemas/AccessList").unwrap(), + ); + let mut buffer = String::new(); + res.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// Access list + pub type AccessList = Vec; + "#, + ); + } + + #[test] + fn generate_one_of_with_null_variant_works() { + let specs = read_specs().unwrap(); + let mut generator = TypeGenerator::new(); + let res = generator.generate_type( + "FilterTopics".to_string(), + specs.get_schema("#/components/schemas/FilterTopics").unwrap(), + ); + let mut buffer = String::new(); + res.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// Filter Topics + pub type FilterTopics = Vec; + "#, + ); + } + + #[test] + fn generate_object_type_works() { + let specs = read_specs().unwrap(); + let mut generator = TypeGenerator::new(); + let res = generator.generate_type( + "Transaction".to_string(), + specs.get_schema("#/components/schemas/GenericTransaction").unwrap(), + ); + + let mut buffer = String::new(); + res.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// Transaction object generic to all types + #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq)] + pub struct Transaction { + /// accessList + /// EIP-2930 access list + #[serde(rename = "accessList", skip_serializing_if = "Option::is_none")] + pub access_list: Option, + /// blobVersionedHashes + /// List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. + #[serde(rename = "blobVersionedHashes", skip_serializing_if = "Vec::is_empty")] + pub blob_versioned_hashes: Vec, + /// blobs + /// Raw blob data. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub blobs: Vec, + /// chainId + /// Chain ID that this transaction is valid on. + #[serde(rename = "chainId", skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + /// from address + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option
, + /// gas limit + #[serde(skip_serializing_if = "Option::is_none")] + pub gas: Option, + /// gas price + /// The gas price willing to be paid by the sender in wei + #[serde(rename = "gasPrice", skip_serializing_if = "Option::is_none")] + pub gas_price: Option, + /// input data + #[serde(alias = "data", skip_serializing_if = "Option::is_none")] + pub input: Option, + /// max fee per blob gas + /// The maximum total fee per gas the sender is willing to pay for blob gas in wei + #[serde(rename = "maxFeePerBlobGas", skip_serializing_if = "Option::is_none")] + pub max_fee_per_blob_gas: Option, + /// max fee per gas + /// The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei + #[serde(rename = "maxFeePerGas", skip_serializing_if = "Option::is_none")] + pub max_fee_per_gas: Option, + /// max priority fee per gas + /// Maximum fee per gas the sender is willing to pay to miners in wei + #[serde(rename = "maxPriorityFeePerGas", skip_serializing_if = "Option::is_none")] + pub max_priority_fee_per_gas: Option, + /// nonce + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, + /// to address + pub to: Option
, + /// type + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + /// value + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + } + "#, + ); + } +} diff --git a/substrate/frame/revive/rpc/codegen/src/main.rs b/substrate/frame/revive/rpc/codegen/src/main.rs new file mode 100644 index 000000000000..e0b660ea84e5 --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/src/main.rs @@ -0,0 +1,64 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::generator::{format_code, TypeGenerator}; +use anyhow::Context; +use std::path::Path; + +mod generator; +mod open_rpc; +mod printer; + +fn main() -> anyhow::Result<()> { + let specs = generator::read_specs()?; + + let mut generator = TypeGenerator::new(); + generator.collect_extra_type("TransactionUnsigned"); + + let out_dir = if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { + Path::new(&dir).join("../src") + } else { + "../src".into() + } + .canonicalize() + .with_context(|| "Failed to find the api directory")?; + + let out = out_dir.join("rpc_methods_gen.rs"); + println!("Generating rpc_methods at {out:?}"); + format_and_write_file(&out, &generator.generate_rpc_methods(&specs)) + .with_context(|| format!("Failed to generate code to {out:?}"))?; + + let out_dir = if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { + Path::new(&dir).join("../../src/evm/api") + } else { + "../../src/evm/api".into() + } + .canonicalize() + .with_context(|| "Failed to find the api directory")?; + + let out = std::fs::canonicalize(out_dir.join("rpc_types_gen.rs"))?; + println!("Generating rpc_types at {out:?}"); + format_and_write_file(&out, &generator.generate_types(&specs)) + .with_context(|| format!("Failed to generate code to {out:?}"))?; + + Ok(()) +} + +fn format_and_write_file(path: &Path, content: &str) -> anyhow::Result<()> { + let code = format_code(content)?; + std::fs::write(path, code).expect("Unable to write file"); + Ok(()) +} diff --git a/substrate/frame/revive/rpc/codegen/src/open_rpc.rs b/substrate/frame/revive/rpc/codegen/src/open_rpc.rs new file mode 100644 index 000000000000..fa7510a50561 --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/src/open_rpc.rs @@ -0,0 +1,834 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Defines the types defined by the [`OpenRPC`](https://spec.open-rpc.org) specification. + +#![warn(missing_docs, missing_debug_implementations)] + +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; + +/// Represents an OpenRPC document. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OpenRpc { + /// The semantic version number of the OpenRPC Specification version that the OpenRPC document + /// uses. + /// + /// This field should be used by tooling specifications and clients to interpret the OpenRPC + /// document. + pub openrpc: String, + /// Provides metadata about the API. + /// + /// This metadata may be used by tooling as required. + pub info: Info, + /// An array of [`Server`] objects, which provide connectivity information to a target server. + /// + /// If the `servers` property is not provided, or is an empty array, the default value would + /// be a [`Server`] with a `url` value of `localhost`. This is taken care of by the + /// [`open-rpc`](crate) crate. + #[serde(default = "serde_fns::servers")] + pub servers: Vec, + /// The available methods for the API. While this field is required, it is legal to leave it + /// empty. + pub methods: Vec>, + /// Holds various schemas for the specification. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub components: Option, + /// Contains additional documentation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_docs: Option, +} + +impl OpenRpc { + /// Returns the [`Method`] with the given path reference. + /// + /// # Examples + /// + /// ```no_run + /// let path = "#/components/schemas/MY_SCHEMA"; + /// let schema = openrpc.get_schema(path).unwrap(); + /// ``` + pub fn get_schema(&self, reference: &str) -> Option<&Schema> { + let mut components = reference.split('/'); + + if !matches!(components.next(), Some("#")) { + return None; + } + + if !matches!(components.next(), Some("components")) { + return None; + } + + if !matches!(components.next(), Some("schemas")) { + return None; + } + + let name = components.next()?; + self.components.as_ref()?.schemas.get(name) + } + + /// Same as [`OpenRpc::get_schema`] but returns a &mut reference + pub fn get_schema_mut(&mut self, reference: &str) -> Option<&mut Schema> { + let mut components = reference.split('/'); + + if !matches!(components.next(), Some("#")) { + return None; + } + + if !matches!(components.next(), Some("components")) { + return None; + } + + if !matches!(components.next(), Some("schemas")) { + return None; + } + + let name = components.next()?; + self.components.as_mut()?.schemas.get_mut(name) + } +} + +/// Provides metadata about the API. +/// +/// The metadata may be used by clients if needed, and may be presented in editing or +/// documentation generation tools for convenience. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Info { + /// The title of the application. + #[serde(default)] + pub title: String, + /// A verbose description of the application. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A URL to the Terms of Service for the API. + /// + /// This must contain an URL. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub terms_of_service: Option, + /// contact information for the exposed API. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub contact: Option, + /// License information for the exposed API. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub license: Option, + /// The version of the OpenRPC document. + /// + /// Note that this is distinct from the `openrpc` field of [`OpenRpc`] which specifies the + /// version of the OpenRPC Specification used. + #[serde(default)] + pub version: String, +} + +/// Contact information for the exposed API. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Contact { + /// The identifying name of the contact person/organization. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + /// The URL pointing to the contact information. + /// + /// This must contain an URL. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + /// The email address of the contact person/organization. + /// + /// This must contain an email address. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub email: Option, +} + +/// License information for the exposed API. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct License { + /// The name of the license used for the API. + #[serde(default)] + pub name: String, + /// The URL pointing to the license used for the API. + /// + /// This must contain an URL. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// A server. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Server { + /// A name to be used as the canonical name for the server. + #[serde(default)] + pub name: String, + /// A URL to the target host. + /// + /// This URL supports Server Variables and may be relative to indicate that the host location + /// is relative to the location where the OpenRPC document is being served. + /// + /// Server Variables are passed into the Runtime Expression to produce a server URL. + pub url: RuntimeExpression, + /// A short description of what the server is. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// Describes the host designated by the URL. + /// + /// GitHub Flavored Markdown may be used for rich text presentation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The values of this object are passed to the [`RuntimeExpression`] to produce an actual + /// URL. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub variables: BTreeMap, +} + +/// An object representing a Server Variable for server URL template substitution. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct ServerVariable { + /// An enumeration of string values to be used if the substitution options are from a limited + /// set. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub enum_: Vec, + /// The default value to use for substitution, which shall be sent if an alternate value is + /// not supplied. + /// + /// Note this behavior is different than the Schema Object's treatment of default values, + /// because in those cases parameter values are optional. + #[serde(default)] + pub default: String, + /// An optional description for the server variable. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Describes the interface for the given method name. +/// +/// The method name is used as the `method` field of the JSON-RPC body. It therefore must be +/// unique. +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Method { + /// The canonical name of the method. + /// + /// This name must be unique within the methods array. + #[serde(default)] + pub name: String, + /// A list of tags for API documentation control. Tags can be used for logical grouping + /// of methods by resources or any other qualifier. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec>, + /// A short summary of what the method does. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// A verbose explanation of the method behavior. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Additional external documentation for this method. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_docs: Option, + /// A list of parameters that are applicable for this method. + /// + /// The list must not include duplicated parameters and therefore require `name` to be + /// unique. + /// + /// All required parameters must be listed *before* any optional parameters. + #[serde(default)] + pub params: Vec>, + /// The description of the result returned by the method. + /// + /// If defined, it must be a [`ContentDescriptor`] or a Reference. + /// + /// If undefined, the method must only be used as a *notification*. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub result: Option>, + /// Declares this method as deprecated. + /// + /// Consumers should refrain from usage of the declared method. + /// + /// The default value is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub deprecated: bool, + /// An alternative `servers` array to service this method. + /// + /// If specified, it overrides the `servers` array defined at the root level. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub servers: Option>, + /// A list of custom application-defined errors that may be returned. + /// + /// The errors must have unique error codes. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub errors: Vec>, + /// A list of possible links from this method call. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub links: Vec>, + /// The expected format of the parameters. + /// + /// The parameters of a method may be an array, an object, or either. When a method + /// has a `param_structure` value of [`ByName`], callers of the method must pass an + /// object as the parameters. When a method has a `param_structure` value of [`ByPosition`], + /// callers of the method must pass an array as the parameters. Otherwise, callers may + /// pass either an array or an object as the parameters. + /// + /// The default value is [`Either`]. + /// + /// [`ByName`]: ParamStructure::ByName + /// [`ByPosition`]: ParamStructure::ByPosition + /// [`Either`]: ParamStructure::Either + #[serde(default, skip_serializing_if = "serde_fns::is_default")] + pub param_structure: ParamStructure, + /// An array of [`ExamplePairing`] objects, where each example includes a valid + /// params-to-result [`ContentDescriptor`] pairing. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub examples: Vec>, +} + +/// A possible value for the `param_structure` field of [`Method`]. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] +#[serde(rename_all = "kebab-case")] +pub enum ParamStructure { + /// Parameters must be passed as a JSON object. + ByName, + /// Parameters must be passed as a JSON array. + ByPosition, + /// Parameters may be passed as either a JSON object or a JSON array. + #[default] + Either, +} + +/// Content descriptors are that do just as they suggest - describe content. They are reusable +/// ways of describing either parameters or results. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ContentDescriptor { + /// The name of the content being described. + /// + /// If the content described is a method parameter assignable + /// [`ByName`](ParamStructure::ByName), this field must be the name of the parameter. + #[serde(default)] + pub name: String, + /// A short summary of the content that is being described. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// A verbose explanation of the content being described. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Determines if the content is a required field. + /// + /// Default is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub required: bool, + /// A [`Schema`] that describes what is allowed in the content. + #[serde(default)] + pub schema: Schema, + /// Whether the content is deprecated. + /// + /// Default is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub deprecated: bool, +} + +/// Allows the definition of input and output data types. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + /// The title of the schema. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// The description of the schema. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The contents of the schema. + #[serde(flatten)] + pub contents: SchemaContents, +} + +/// The content of a schema. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum SchemaContents { + /// The schema contains a reference to another schema. + Reference { + /// The reference string. + #[serde(rename = "$ref")] + reference: String, + }, + /// The schema is made of a combination of other schemas. + /// + /// The final object must match *all* of the schemas. + AllOf { + /// The schemas that the final object must match. + #[serde(rename = "allOf")] + all_of: Vec, + }, + /// The schema is made of a combination of other schemas. + /// + /// The final object must match *any* of the schemas. + AnyOf { + /// The schemas that the final object must match. + #[serde(rename = "anyOf")] + any_of: Vec, + }, + /// The schema is made of a combination of other schemas. + /// + /// The final object must match exactly *one* of the schemas. + OneOf { + /// The schemas that the final object must match. + #[serde(rename = "oneOf")] + one_of: Vec, + }, + /// The schema contains a literal value. + Literal(Literal), + /// The schema contains an Object. + /// + /// Note this is a workaround to parse Literal(Literal::ObjectLiteral), that don't havethe + /// type: "object" field. + Object(ObjectLiteral), +} + +impl Default for SchemaContents { + #[inline] + fn default() -> Self { + Self::Literal(Literal::Null) + } +} + +/// A literal value. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum Literal { + /// The literal is a boolean. + Boolean, + /// The literal is an integer. + Integer(IntegerLiteral), + /// The literal is a number. + Number(NumberLiteral), + /// The literal is a string. + String(StringLiteral), + // The literal is an object. + Object(ObjectLiteral), + /// The literal is an array. + Array(ArrayLiteral), + /// The literal is a null value. + Null, +} + +/// The constraints that may be applied to an integer literal schema. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] +pub struct IntegerLiteral { + /// The integer must be a multiple of this value. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub multiple_of: Option, + /// The minimum value of the integer. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub minimum: Option, + /// The maximum value of the integer. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub maximum: Option, + /// Whether the minimum value is exclusive. + /// + /// Default is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub exclusive_minimum: bool, + /// Whether the maximum value is exclusive. + /// + /// Default is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub exclusive_maximum: bool, +} + +/// The constraints that may be applied to a number literal schema. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct NumberLiteral { + /// The number must be a multiple of this value. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub multiple_of: Option, + /// The minimum value of the number. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub minimum: Option, + /// The maximum value of the number. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub maximum: Option, + /// Whether the minimum value is exclusive. + /// + /// Default is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub exclusive_minimum: bool, + /// Whether the maximum value is exclusive. + /// + /// Default is `false`. + #[serde(default, skip_serializing_if = "serde_fns::is_false")] + pub exclusive_maximum: bool, +} + +/// The constraints that may be applied to an array literal schema. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ArrayLiteral { + /// The schema that the items in the array must match. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub items: Option>, +} + +/// The constraints that may be applied to an string literal schema. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StringLiteral { + /// The minimum length of the string. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_length: Option, + /// The maximum length of the string.s + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_length: Option, + /// The pattern that the string must match. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pattern: Option, + /// The format that the string must be in. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub format: Option, + /// A list of possible values for the string. + #[serde(default, skip_serializing_if = "Option::is_none", rename = "enum")] + pub enumeration: Option>, +} + +/// A string format. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum StringFormat { + /// Date and time together, for example, `2018-11-13T20:20:39+00:00`. + DateTime, + /// Time, for example, `20:20:39+00:00`. + Time, + /// Date, for example, `2018-11-13`. + Date, + /// A duration as defined by the [ISO 8601 ABNF](https://datatracker.ietf.org/doc/html/rfc3339#appendix-A). + Duration, + /// An email. See [RFC 5321](http://tools.ietf.org/html/rfc5321#section-4.1.2). + Email, + /// The internationalized version of an email. See [RFC 6531](https://tools.ietf.org/html/rfc6531). + IdnEmail, + /// A host name. See [RFC 1123](https://datatracker.ietf.org/doc/html/rfc1123#section-2.1). + Hostname, + /// The internationalized version of a host name. See [RFC 5890](https://tools.ietf.org/html/rfc5890#section-2.3.2.3). + IdnHostname, + /// An IP v4. See [RFC 2673](http://tools.ietf.org/html/rfc2673#section-3.2). + #[serde(rename = "ipv4")] + IpV4, + /// An IP v6. See [RFC 2373](http://tools.ietf.org/html/rfc2373#section-2.2). + #[serde(rename = "ipv6")] + IpV6, + /// A universally unique identifier. See [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122). + Uuid, + /// A universal resource identifier . See [RFC 3986](http://tools.ietf.org/html/rfc3986). + Uri, + /// A URI reference. See (RFC 3986)[]. + UriReference, + /// The internationalized version of a URI. See [RFC 3987](https://tools.ietf.org/html/rfc3987). + Iri, + /// The internationalized version of a URI reference. See [RFC 3987](https://tools.ietf.org/html/rfc3987). + IriReference, + /// A URI template. See [RFC 6570](https://tools.ietf.org/html/rfc6570). + UriTemplate, + /// A JSON pointer. See [RFC 6901](https://tools.ietf.org/html/rfc6901). + JsonPointer, + /// A relative JSON pointer. See [Relative JSON Pointer](https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01). + RelativeJsonPointer, + /// A regular expression. See [ECMA 262](https://www.ecma-international.org/publications-and-standards/standards/ecma-262/). + Regex, +} + +/// The constraints that may be applied to an object literal schema. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ObjectLiteral { + /// The properties that the object might have. + pub properties: BTreeMap, + + /// List of legacy aliases for properties. + #[serde(skip)] + pub legacy_aliases: HashMap, + + /// A list of properties that the object must have. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required: Vec, +} + +/// A set of example parameters and a result. +/// +/// This result is what you'd expect from the JSON-RPC service given the exact params. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExamplePairing { + /// The name for the example pairing. + #[serde(default)] + pub name: String, + /// A verbose description of the example pairing. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A short summary of the example pairing. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// Example parameters. + #[serde(default)] + pub params: Vec>, + /// Example result. + /// + /// When undefined, shows the usage of the method as a notification. + #[serde(default)] + pub result: RefOr, +} + +/// Defines an example that is intended to match a [`Schema`] of a given [`ContentDescriptor`]. +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExampleObject { + /// Canonical name of the example. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + /// A verbose description of the example + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A short summary of the example. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// The value of the example. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +/// The example value of an [`ExampleObject`]. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ExampleValue { + /// The value is a JSON object embedded in the document. + /// A link to an external document containing the value. + #[serde(rename = "externalValue")] + External(String), +} + +/// Represents a possible design-time link for a result. +/// +/// The presence of a link does not guarantee the caller's ability to successfully invoke it, +/// rather it provides a known relationship and traversal mechanism between results and other +/// methods. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] +pub struct Link { + /// Canonical name for the link. + #[serde(default)] + pub name: String, + /// A description of the link. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Short description for the link. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// The name of an *existing*, resolvable OpenRPC method, as defined with a unique + /// `method`. This field must resolve to a unique [`Method`] object. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub method: Option, + /// The parameters to pass to a method as specified with `method`. The key is the parameter + /// name to be used, whereas the value can be a constant or a [`RuntimeExpression`] to be + /// evaluated and passed to the linked method. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub params: Option, + /// A server object to be used by the target method. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server: Option, +} + +/// The content of the `params` field of a [`Link`]. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum LinkParams { + /// A [`RuntimeExpression`] that evaluates to the parameters. + Dynamic(RuntimeExpression), +} + +/// Runtime expressions allow the user to define an expression which will evaluate to a +/// string once the desired value(s) are known. +/// +/// They are used when the desired value of a link or server can only be constructed at +/// run time. This mechanism is used by [`Link`] objects and [`ServerVariable`]s. +/// +/// This runtime expression makes use of JSON template strings. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +pub struct RuntimeExpression(pub String); + +/// An application-level error. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Error { + /// An application-defined error code. + #[serde(default)] + pub code: i64, + /// A string providing a short description of the error. + /// + /// The message should be limited to a concise single sentence. + #[serde(default)] + pub message: String, +} + +/// Holds a set of reusable objects for different aspects of the OpenRPC document. +/// +/// All objects defined within the [`Components`] object will have no effect on the API +/// unless they are explicitly referenced from properties outside of the [`Components`] +/// object. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Components { + /// A list of reusable [`ContentDescriptor`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub content_descriptors: BTreeMap, + /// A list of reusable [`Schema`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub schemas: BTreeMap, + /// A list of reusable [`ExampleObject`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub examples: BTreeMap, + /// A list of reusable [`Link`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub links: BTreeMap, + /// A list of reusable [`Error`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub errors: BTreeMap, + /// A list of reusable [`ExamplePairing`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty", rename = "examplePairingObjects")] + pub example_pairings: BTreeMap, + /// A list of reusable [`Tag`]s. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub tags: BTreeMap, +} + +/// Adds metadata to a single tag that is used by the [`Method`] Object. +/// +/// It is not mandatory to have a [`Tag`] Object per tag defined in the [`Method`] +/// Object instances. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Tag { + /// The name of the tag. + #[serde(default)] + pub name: String, + /// A short summary of the tag. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// A verbose explanation of the tag. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Additional external documentation for this tag. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_docs: Option, +} + +/// Allows referencing an external resource for extended documentation. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExternalDocumentation { + /// A verbose explanation of the target documentation. + /// + /// GitHub Flavored Markdown syntax may be used for rich text representation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A URL for the target documentation. + /// + /// This must contain an URL. + #[serde(default)] + pub url: String, +} + +/// Either a reference or an inline object. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum RefOr { + /// A reference to an object defined elsewhere. + Reference { + /// The reference string. + #[serde(rename = "$ref")] + reference: String, + }, + /// An inline object. + Inline(T), +} + +impl RefOr { + /// Unwraps the inlined object. + pub fn unwrap_inline(&self) -> &T { + match self { + RefOr::Reference { reference } => panic!("Unexpected reference: {reference}"), + RefOr::Inline(v) => v, + } + } +} + +impl Default for RefOr { + #[inline] + fn default() -> Self { + RefOr::Inline(T::default()) + } +} + +/// Functions used by `serde`, such as predicates and default values. +mod serde_fns { + use std::collections::BTreeMap; + + use super::{RuntimeExpression, Server}; + + /// Returns the default value of the `servers` field. + pub fn servers() -> Vec { + vec![Server { + name: "default".into(), + url: RuntimeExpression("localhost".into()), + summary: None, + description: None, + variables: BTreeMap::new(), + }] + } + + /// Returns whether `b` is `false`. + pub fn is_false(b: &bool) -> bool { + !*b + } + + /// Returns whether the given value is the default value of its type. + pub fn is_default(t: &T) -> bool { + *t == T::default() + } +} + +#[test] +fn parsing_works() { + let content = include_str!("../openrpc.json"); + let _: OpenRpc = dbg!(serde_json::from_str(content).unwrap()); +} diff --git a/substrate/frame/revive/rpc/codegen/src/printer.rs b/substrate/frame/revive/rpc/codegen/src/printer.rs new file mode 100644 index 000000000000..8a8933b4f432 --- /dev/null +++ b/substrate/frame/revive/rpc/codegen/src/printer.rs @@ -0,0 +1,527 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::open_rpc::*; +use inflector::Inflector; + +/// Type information used for generating the type. +#[derive(Debug, Clone)] +pub struct TypeInfo { + /// The type name. + pub name: String, + /// Whether the type is an array. + pub array: bool, + /// Whether the type is required. + pub required: Required, +} + +impl TypeInfo { + pub fn set_required(mut self, required: bool) -> Self { + if required { + self.required = Required::Yes; + } else { + self.required = Required::No { skip_if_null: true }; + } + self + } + + /// Return Whether the type is optional. + pub fn is_optional(&self) -> bool { + matches!(self.required, Required::No { .. }) + } +} + +/// A trait to provide type names. +pub trait TypeNameProvider { + /// Returns type information for a schema. + fn type_info(&mut self, schema: &Schema) -> Option; + + /// Record an inline type. + fn record_inline_type(&mut self, name: String, schema: &Schema) -> TypeInfo; +} + +/// Describes whether the type is required or not. +#[derive(Debug, Clone)] +pub enum Required { + /// The type is required. + Yes, + /// The type is not required, and may be skipped when serializing if it's None and skip_if_null + /// is true. + No { skip_if_null: bool }, +} + +impl TypeInfo { + //// Convert the type info to a string we can use in the generated code. + pub fn get_type(&self) -> String { + let mut type_name = self.name.clone(); + if self.array { + type_name = format!("Vec<{}>", type_name) + } else if self.is_optional() { + type_name = format!("Option<{}>", type_name) + } + type_name + } +} + +impl From for TypeInfo +where + T: Into, +{ + fn from(name: T) -> Self { + Self { name: name.into(), required: Required::Yes, array: false } + } +} +/// Represents a field in a struct. +#[derive(Debug)] +pub struct Field { + /// The documentation for the field. + doc: Option, + /// The name of the field. + name: String, + /// the type information for the field. + type_info: TypeInfo, + /// Whether to flatten the field, when serializing. + flatten: bool, + /// Legacy alias for the field. + alias: Option, +} + +/// Represents a collection of fields. +#[derive(Debug)] +pub struct Fields(Vec); + +impl From> for Fields { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl IntoIterator for Fields { + type Item = Field; + type IntoIter = std::vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Fields { + /// Creates a collection of fields from an [`ObjectLiteral]. + /// + /// The methods also takes a [`TypeNameProvider`] to resolve the types of the fields, and to + /// collect child types. + pub fn from(value: &ObjectLiteral, provider: &mut impl TypeNameProvider) -> Self { + let ObjectLiteral { properties, legacy_aliases, required } = value; + + properties + .iter() + .map(|(name, schema)| { + let mut type_info = provider.type_info(schema).expect("Type should be defined"); + if matches!(type_info.required, Required::Yes) && !required.contains(name) { + type_info.required = Required::No { skip_if_null: true }; + } + + let doc = doc_str_from_schema(schema); + Field { + doc, + name: name.clone(), + type_info, + alias: legacy_aliases.get(name).cloned(), + flatten: false, + } + }) + .collect::>() + .into() + } + + /// Creates a collection of fields from the items of a [`SchemaContents::AllOf`] schema. + pub fn from_all_of(all_of: &[Schema], provider: &mut impl TypeNameProvider) -> Fields { + all_of + .iter() + .flat_map(|schema| { + let doc = doc_str_from_schema(schema); + if let Some(type_info) = provider.type_info(schema) { + vec![Field { + doc, + name: type_info.name.clone(), + type_info, + alias: None, + flatten: true, + }] + } else { + let object = match &schema.contents { + SchemaContents::Object(object) => object, + SchemaContents::Literal(Literal::Object(object)) => object, + v => panic!("Unsupported anonymous all_of type {:?}", v), + }; + + Fields::from(object, provider).0 + } + }) + .collect::>() + .into() + } +} + +/// The variant of an enum. +#[derive(Debug)] +pub struct Variant { + /// The documentation for the variant. + doc: Option, + /// The type information for the variant. + type_info: TypeInfo, +} + +impl Variant { + pub fn name(&self) -> String { + let name = self.type_info.name.to_pascal_case(); + if self.type_info.array { + format!("{}s", name) + } else { + name + } + } +} + +pub fn doc_str_from_schema(schema: &Schema) -> Option { + let mut doc = schema.title.clone(); + + if let Some(description) = &schema.description { + doc = Some(doc.map_or_else(|| description.clone(), |doc| format!("{doc}\n{description}"))); + } + + doc +} + +#[derive(Debug)] +pub struct Variants(Vec); +impl Variants { + /// Creates a collection of variants from the items of a [`SchemaContents::OneOf`] schema. + pub(crate) fn from_one_of(one_of: &[Schema], provider: &mut impl TypeNameProvider) -> Variants { + one_of + .iter() + .filter_map(|schema| { + let doc = doc_str_from_schema(schema); + if let Some(type_info) = provider.type_info(schema) { + if type_info.name == "Null" || type_info.name == "NotFound" { + return None; + } + + Some(Variant { doc, type_info }) + } else { + let name = schema + .title + .clone() + .expect("Title should be defined for inline variant") + .to_pascal_case(); + + let type_info = provider.record_inline_type(name.clone(), schema); + Some(Variant { doc, type_info }) + } + }) + .collect::>() + .into() + } +} + +impl From> for Variants { + fn from(value: Vec) -> Self { + Self(value) + } +} + +/// The content of a type. +#[derive(Debug)] +pub enum TypeContent { + /// A struct type. + Struct(Fields), + /// A unit struct type. + TypeAlias(TypeInfo), + /// An enum type. + Enum(Variants), + /// A serde untagged enum type. + UntaggedEnum(Vec), +} + +/// A type printer. +#[derive(Debug)] +pub struct TypePrinter { + pub doc: Option, + pub name: String, + pub content: TypeContent, + custom_default_variant: Option, +} + +/// A macro to write a formatted line to a buffer. +#[macro_export] +macro_rules! writeln { + (@doc $s: ident, $doc: ident) => { + $crate::writeln!(@doc $s, $doc, 0) + }; + (@doc $s: ident, $doc: ident, $indent: literal) => { + if let Some(doc) = $doc { + for line in doc.lines() { + writeln!($s, "{:indent$}/// {}", "", line, indent = $indent); + } + } + }; + ($s: ident, $($arg: tt)*) => { + $s.push_str(&format!($($arg)*)); + $s.push_str("\n"); + }; + + + +} + +impl TypePrinter { + pub fn new( + doc: Option, + name: String, + content: TypeContent, + custom_default_variant: Option, + ) -> Self { + Self { doc, name, content, custom_default_variant } + } + + /// Prints the type to a buffer. + pub fn print(self, buffer: &mut String) { + let Self { doc, name, content, .. } = self; + + writeln!(@doc buffer, doc); + match content { + TypeContent::Enum(variants) if variants.0.len() == 1 => { + let type_info = &variants.0[0].type_info; + writeln!(buffer, "pub type {name} = {};", type_info.get_type()); + }, + TypeContent::TypeAlias(type_info) => { + writeln!(buffer, "pub type {name} = {};", type_info.get_type()); + }, + TypeContent::Enum(variants) => { + writeln!( + buffer, + "#[derive(Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq)]" + ); + writeln!(buffer, "#[serde(untagged)]"); + writeln!(buffer, "pub enum {name} {{"); + for variant in variants.0.iter() { + let doc = &variant.doc; + writeln!(@doc buffer, doc, 2); + writeln!(buffer, " {}({}),", variant.name(), variant.type_info.get_type()); + } + writeln!(buffer, "}}"); + + // Implement Default trait + let default_variant = self + .custom_default_variant + .map(|s| s.to_string()) + .unwrap_or_else(|| variants.0[0].name()); + + writeln!(buffer, "impl Default for {name} {{"); + writeln!(buffer, " fn default() -> Self {{"); + writeln!(buffer, " {name}::{default_variant}(Default::default())"); + writeln!(buffer, " }}"); + writeln!(buffer, "}}"); + }, + TypeContent::UntaggedEnum(variants) => { + writeln!( + buffer, + "#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq)]" + ); + writeln!(buffer, "pub enum {name} {{"); + + let default_variant_index = self.custom_default_variant.map_or(0, |v| { + variants + .iter() + .position(|x| x.eq_ignore_ascii_case(&v)) + .expect("Default variant not found") + }); + + for (i, name) in variants.iter().enumerate() { + writeln!(buffer, " #[serde(rename = \"{name}\")]"); + if i == default_variant_index { + writeln!(buffer, " #[default]"); + } + let pascal_name = name.to_pascal_case(); + writeln!(buffer, " {pascal_name},"); + } + writeln!(buffer, "}}"); + }, + TypeContent::Struct(fields) => { + writeln!( + buffer, + "#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq)]" + ); + + writeln!(buffer, "pub struct {name} {{"); + for Field { doc, name, type_info, alias, flatten } in fields { + writeln!(@doc buffer, doc, 2); + let mut snake_name = name.to_snake_case(); + let mut serde_params = vec![]; + + if flatten { + serde_params.push("flatten".to_string()); + } else if snake_name != name { + serde_params.push(format!("rename = \"{}\"", name)); + } + + if let Some(alias) = alias { + serde_params.push(format!("alias = \"{}\"", alias)); + } + + if matches!(type_info.required, Required::No { skip_if_null: true }) { + if type_info.array { + serde_params.push( + "default, skip_serializing_if = \"Vec::is_empty\"".to_string(), + ); + } else { + serde_params + .push("skip_serializing_if = \"Option::is_none\"".to_string()); + } + } + + if !serde_params.is_empty() { + writeln!(buffer, " #[serde({})]", serde_params.join(", ")); + } + + let type_name = type_info.get_type(); + + if snake_name == "type" { + snake_name = "r#type".to_string() + } + writeln!(buffer, " pub {snake_name}: {type_name},"); + } + writeln!(buffer, "}}"); + }, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::generator::assert_code_match; + + #[test] + fn print_struct_works() { + let gen = TypePrinter { + doc: Some("A simple struct".to_string()), + name: "SimpleStruct".to_string(), + content: TypeContent::Struct( + vec![ + Field { + doc: Some("The first field".to_string()), + name: "firstField".to_string(), + type_info: "u32".into(), + flatten: false, + alias: None, + }, + Field { + doc: None, + name: "second".to_string(), + type_info: TypeInfo { + name: "String".to_string(), + required: Required::No { skip_if_null: true }, + array: false, + }, + flatten: true, + alias: None, + }, + ] + .into(), + ), + custom_default_variant: None, + }; + let mut buffer = String::new(); + gen.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// A simple struct + #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq)] + pub struct SimpleStruct { + /// The first field + #[serde(rename = "firstField")] + pub first_field: u32, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub second: Option, + } + "#, + ); + } + + #[test] + fn print_untagged_enum_works() { + let gen = TypePrinter { + doc: Some("A simple untagged enum".to_string()), + name: "SimpleUntaggedEnum".to_string(), + content: TypeContent::UntaggedEnum(vec!["first".to_string(), "second".to_string()]), + custom_default_variant: None, + }; + let mut buffer = String::new(); + gen.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// A simple untagged enum + #[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq)] + pub enum SimpleUntaggedEnum { + #[serde(rename = "first")] + #[default] + First, + #[serde(rename = "second")] + Second, + } + "#, + ); + } + + #[test] + fn print_enum_works() { + let gen = TypePrinter { + doc: Some("A simple enum".to_string()), + name: "SimpleEnum".to_string(), + content: TypeContent::Enum( + vec![ + Variant { doc: Some("The Foo variant".to_string()), type_info: "Foo".into() }, + Variant { doc: Some("The Bar variant".to_string()), type_info: "Bar".into() }, + ] + .into(), + ), + custom_default_variant: None, + }; + let mut buffer = String::new(); + gen.print(&mut buffer); + assert_code_match( + &buffer, + r#" + /// A simple enum + #[derive(Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq)] + #[serde(untagged)] + pub enum SimpleEnum { + /// The Foo variant + Foo(Foo), + /// The Bar variant + Bar(Bar), + } + impl Default for SimpleEnum { + fn default() -> Self { + SimpleEnum::Foo(Default::default()) + } + } + "#, + ); + } +}