diff --git a/package-lock.json b/package-lock.json index 63b8c2590..fccde036b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,11 @@ "dependencies": { "@graphql-inspector/core": "~3.3.0", "@mdx-js/react": "~3.0.0", + "@open-rpc/extensions": "^0.0.2", "@open-rpc/generator": "^2.1.0", + "@open-rpc/meta-schema": "^1.14.2", "@open-rpc/schema-utils-js": "^2.1.2", + "@open-rpc/specification-extension-spec": "^1.0.2", "gatsby": "^5.14.3", "graphql": "~16.3.0", "graphql-request": "~4.1.0", @@ -2976,6 +2979,12 @@ "node": ">= 8" } }, + "node_modules/@open-rpc/extensions": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@open-rpc/extensions/-/extensions-0.0.2.tgz", + "integrity": "sha512-+sbOGMiY4dFsl8hvdizpdpxQmdjJIYJP87lz9uRKc71RIZGDPngve0Ad4BIyqQ37EuVbRejOXdwWGi8IjL6dLw==", + "license": "Apache-2.0" + }, "node_modules/@open-rpc/generator": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@open-rpc/generator/-/generator-2.1.0.tgz", diff --git a/package.json b/package.json index a7fa1934f..e20d9c1a0 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "@graphql-inspector/core": "~3.3.0", "@open-rpc/generator": "^2.1.0", "@open-rpc/schema-utils-js": "^2.1.2", + "@open-rpc/extensions": "^0.0.2", + "@open-rpc/specification-extension-spec": "^1.0.2", + "@open-rpc/meta-schema": "^1.14.2", "gatsby": "^5.14.3", "graphql": "~16.3.0", "graphql-request": "~4.1.0", diff --git a/scripts/build.js b/scripts/build.js index 639081f7e..83be4c5a7 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -2,6 +2,7 @@ import fs from "fs"; import yaml from "js-yaml"; import mergeAllOf from "json-schema-merge-allof"; import { dereferenceDocument } from "@open-rpc/schema-utils-js"; +import { XErrorGroupsJSON } from "@open-rpc/extensions"; function sortByMethodName(methods) { return methods.slice().sort((a, b) => { @@ -79,6 +80,37 @@ schemaFiles.forEach(file => { }; }); +let extensionSpecs = []; + +// Enhance the existing XErrorGroupsJSON extension with conditional validation for different error categories +const enhancedErrorGroupSchema = JSON.parse(fs.readFileSync('src/extensions/schemas/x-error-category-ranges.json', 'utf8')); +XErrorGroupsJSON.schema = enhancedErrorGroupSchema; + +extensionSpecs.push(XErrorGroupsJSON); + +let extensions = []; +let extensionsBase = "src/extensions/components/" +let extensionsFiles = fs.readdirSync(extensionsBase); +extensionsFiles.forEach(file => { + console.log(file); + let raw = fs.readFileSync(extensionsBase + file); + let parsed = yaml.load(raw); + extensions.push(parsed); +}); + +// if extensions key matches with extensionSpecs name, then add it to an array of extensionSpec name +let extensionsDef = {}; +extensionSpecs.forEach((extensionSpec) => { + extensions.forEach((extension) => { + if (extension.hasOwnProperty(extensionSpec.name)) { + extensionsDef[extensionSpec.name] ={ + ...extensionsDef[extensionSpec.name], + ...extension[extensionSpec.name] + } + } + }); +}); + const doc = { openrpc: "1.2.4", info: { @@ -91,7 +123,9 @@ const doc = { version: "0.0.0" }, methods: sortByMethodName(methods), + "x-extensions": extensionSpecs, components: { + ...extensionsDef, schemas: schemas } } diff --git a/scripts/validate.js b/scripts/validate.js index c5f2efaab..eb88e2f0f 100644 --- a/scripts/validate.js +++ b/scripts/validate.js @@ -1,13 +1,19 @@ import fs from "fs"; import { parseOpenRPCDocument, + dereferenceDocument, validateOpenRPCDocument } from "@open-rpc/schema-utils-js"; +import OpenrpcDocument from "@open-rpc/meta-schema"; let rawdata = fs.readFileSync("openrpc.json"); let openrpc = JSON.parse(rawdata); -const error = validateOpenRPCDocument(openrpc); +/** @type {OpenrpcDocument} */ +const document = openrpc; +const dereffed = await dereferenceDocument(document); + +const error = validateOpenRPCDocument(dereffed); if (error != true) { console.log(error.name); console.log(error.message); diff --git a/src/eth/submit.yaml b/src/eth/submit.yaml index dc5a93dfa..fe8f98983 100644 --- a/src/eth/submit.yaml +++ b/src/eth/submit.yaml @@ -5,6 +5,13 @@ required: true schema: $ref: '#/components/schemas/GenericTransaction' + x-error-group: + - $ref: '#/components/x-error-group/JSONRPCNonStandardErrors' + - $ref: '#/components/x-error-group/JSONRPCStandardErrors' + - $ref: '#/components/x-error-group/GasErrors' + - $ref: '#/components/x-error-group/ExecutionErrors' + - $ref: '#/components/x-error-group/TxPoolErrors' + - $ref: '#/components/x-error-group/ZKExecutionErrors' result: name: Transaction hash schema: @@ -45,6 +52,13 @@ required: true schema: $ref: '#/components/schemas/bytes' + x-error-group: + - $ref: '#/components/x-error-group/JSONRPCNonStandardErrors' + - $ref: '#/components/x-error-group/JSONRPCStandardErrors' + - $ref: '#/components/x-error-group/GasErrors' + - $ref: '#/components/x-error-group/ExecutionErrors' + - $ref: '#/components/x-error-group/TxPoolErrors' + - $ref: '#/components/x-error-group/ZKExecutionErrors' result: name: Transaction hash schema: diff --git a/src/extensions/README.md b/src/extensions/README.md new file mode 100644 index 000000000..d660d76c1 --- /dev/null +++ b/src/extensions/README.md @@ -0,0 +1,49 @@ +# Extensions overview + +## Proposal +#### Goal +A standard for JSON-RPC error codes & ship a shared catalog of JSON-RPC error codes and messages for EVM clients to unlock consistent tooling and developer ergonomics. + +#### Motivation +Client implementations and EVM-compatible chains currently reuse codes or return generic error messages, making cross-client debugging brittle. + +#### Solution +The solution incorporates [OpenRPC's extension schemas](https://github.com/open-rpc/specification-extension-spec) feature, specifically `x-error-group` [extension](https://github.com/open-rpc/tools/blob/main/packages/extensions/src/x-error-groups/x-error-groups.json), so common scenarios can be bundled into reusable categories, each backed by a reserved range of **200 codes** outside the JSON-RPC 2.0 reserved bands. +With the error grouping and inline provisioning offered by the extension schemas, we could onboard methods over time with granular control over the errors or groups each method would need to handle without copy pasting in the final spec. + +The corresponding PR definition frames these groups as the canonical vocabulary for wallets, infra providers, and execution clients. + +## Solution Layout +- `components/` – YAML fragments exposing each error family as an OpenRPC `x-error-group` definition. +- `schemas/x-error-category-ranges.json` – Extension to official `x-error-groups` that enforces the reserved integer windows per category during validation. + - This is to achieve inbuild validation of the reserved ranges per category using native `minimum` & `maximum` properties of the extended schema. + - Validation happens while running `scripts/validate.js` after building the final `refs-openrpc.json` / `openrpc.json`. +- `scripts/build.js` – Loads the schema above, augments the `XErrorGroupsJSON` extension, and merges the groups into `refs-openrpc.json` / `openrpc.json`. + +## Implemented Methods +Currently, only below methods import all the error groups via `$ref` and may include inline method-specific codes while still inheriting the standard set. +- `eth_sendTransaction` in `src/eth/submit.yaml` +- `eth_sendRawTransaction` in `src/eth/submit.yaml` + + +## Reserved ranges at a glance +| Extension group | Category label | Reserved range | Source | +| --- | --- | --- | --- | +| JSON-RPC standard | — | $-32768$ to $-32000$ | JSON-RPC 2.0 spec | +| JSON-RPC non-standard | Client-specific | $-32099$ to $-32000$ | JSON-RPC 2.0 addendum | +| Gas errors | `GAS_ERRORS` | $800$ to $999$ | `gas-error-groups.yaml` | +| Execution errors | `EXECUTION_ERRORS` | $1$ to $999$ | `execution-errors.yaml` | +| TxPool errors | `TXPOOL_ERRORS` | $1000$ to $1200$ | `txpool-errors.yaml` | +| ZK execution errors | `ZK_EXECUTION_ERRORS` | $2000$ to $2200$ | `zk-execution-errors.yaml` | + + +**Validation** of these bands happens through `XErrorGroupsJSON.schema` in `scripts/build.js`, so build failures flag any out-of-range additions early. + + +## Extending the catalog +1. Pick or create a YAML fragment under `components/` and add the new entry with `code`, `message`, `data`, and `x-error-category` per the proposal. +2. Stay within the reserved window; the JSON Schema guard in `schemas/x-error-category-ranges.json` will break the build if you drift. +3. Reference the group from the relevant method spec via `$ref: '#/components/x-error-group/'` and layer any bespoke errors inline. +4. Run the documentation build (e.g. `node scripts/build.js`) to regenerate `refs-openrpc.json` / `openrpc.json` and confirm validation passes. + +Following this flow keeps the execution client surface aligned with the standard and preserves interoperability for downstream consumers. \ No newline at end of file diff --git a/src/extensions/components/execution-errors.yaml b/src/extensions/components/execution-errors.yaml new file mode 100644 index 000000000..9cc3bfe10 --- /dev/null +++ b/src/extensions/components/execution-errors.yaml @@ -0,0 +1,18 @@ +x-error-group: + ExecutionErrors: + - code: 1 + message: "NONCE_TOO_LOW" + data: "Nonce too low" + x-error-category: "EXECUTION_ERRORS" + - code: 2 + message: "NONCE_TOO_HIGH" + data: "Nonce too high" + x-error-category: "EXECUTION_ERRORS" + - code: 3 + message: "EXECUTION_REVERTED" + data: "Execution reverted by REVERT Opcode" + x-error-category: "EXECUTION_ERRORS" + - code: 4 + message: "INVALID_OPCODE" + data: "Invalid opcode" + x-error-category: "EXECUTION_ERRORS" diff --git a/src/extensions/components/gas-error-groups.yaml b/src/extensions/components/gas-error-groups.yaml new file mode 100644 index 000000000..0b93893e0 --- /dev/null +++ b/src/extensions/components/gas-error-groups.yaml @@ -0,0 +1,34 @@ +x-error-group: + GasErrors: + - code: 800 + message: "GAS_TOO_LOW" + data: "Intrinsic gas too low / Intrinsic gas exceeds gas limit" + x-error-category: "GAS_ERRORS" + - code: 801 + message: "OUT_OF_GAS" + data: "Insufficient gas for floor data gas cost" + x-error-category: "GAS_ERRORS" + - code: 802 + message: "BLOCK_GAS_LIMIT_EXCEEDED" + data: "Tx gas limit exceeds max block gas limit / intrinsic gas exceeds gas limit" + x-error-category: "GAS_ERRORS" + - code: 803 + message: "TRANSACTION_GAS_LIMIT_EXCEEDED" + data: "Transaction gas limit too high" + - code: 804 + message: "GAS_PRICE_TOO_LOW" + data: "Gas price below configured minimum gas price / transaction gas price below minimum" + x-error-category: "GAS_ERRORS" + - code: 805 + message: "INSUFFICIENT_FUNDS" + data: "Insufficient funds for gas * price + value / Upfront cost exceeds account balance" + x-error-category: "GAS_ERRORS" + - code: 806 + message: "TIP_ABOVE_FEE_CAP" + data: "Max priority fee per gas higher than max fee per gas" + x-error-category: "GAS_ERRORS" + - code: 807 + message: "FEE_CAP_EXCEEDED" + data: "Tx fee exceeds the configured cap / Transaction fee cap exceeded" + x-error-category: "GAS_ERRORS" + diff --git a/src/extensions/components/rpc-non-standard-errors.yaml b/src/extensions/components/rpc-non-standard-errors.yaml new file mode 100644 index 000000000..13533f266 --- /dev/null +++ b/src/extensions/components/rpc-non-standard-errors.yaml @@ -0,0 +1,23 @@ +x-error-group: + JSONRPCNonStandardErrors: + - code: -32000 + message: "Invalid input" + data: "Missing or invalid parameters" + - code: -32001 + message: "Resource not found" + data: "Requested resource not found" + - code: -32002 + message: "Resource unavailable" + data: "Requested resource not available" + - code: -32003 + message: "Transaction rejected" + data: "Transaction creation failed" + - code: -32004 + message: "Method not supported" + data: "Method is not implemented" + - code: -32005 + message: "Limit exceeded" + data: "Request exceeds defined limit" + - code: -32006 + message: "JSON-RPC version not supported" + data: "Version of JSON-RPC protocol is not supported" diff --git a/src/extensions/components/rpc-standard-errors.yaml b/src/extensions/components/rpc-standard-errors.yaml new file mode 100644 index 000000000..8807dac43 --- /dev/null +++ b/src/extensions/components/rpc-standard-errors.yaml @@ -0,0 +1,17 @@ +x-error-group: + JSONRPCStandardErrors: + - code: -32700 + message: "Parse error" + data: "An error occurred on the server while parsing the JSON text" + - code: -32600 + message: "Invalid request" + data: "The JSON sent is not a valid request object" + - code: -32601 + message: "Method not found" + data: "The method does not exist / is not available" + - code: -32602 + message: "Invalid params" + data: "Invalid method parameter(s)" + - code: -32603 + message: "Internal error" + data: "Internal JSON-RPC error" \ No newline at end of file diff --git a/src/extensions/components/txpool-errors.yaml b/src/extensions/components/txpool-errors.yaml new file mode 100644 index 000000000..1b2040797 --- /dev/null +++ b/src/extensions/components/txpool-errors.yaml @@ -0,0 +1,46 @@ +x-error-group: + TxPoolErrors: + - code: 1000 + message: "ALREADY_KNOWN" + data: "Transaction is already known to the transaction pool" + x-error-category: "TXPOOL_ERRORS" + - code: 1001 + message: "REPLACEMENT_TRANSACTION_UNDERPRICED" + data: "Replacement transaction is sent without the required price bump" + x-error-category: "TXPOOL_ERRORS" + - code: 1002 + message: "OVERSIZED_DATA" + data: "Oversized data: Transaction input data exceeds the allowed limit" + x-error-category: "TXPOOL_ERRORS" + - code: 1003 + message: "TX_NOT_PERMITTED" + data: "Only replay-protected (EIP-155) transactions allowed over RPC" + x-error-category: "TXPOOL_ERRORS" + - code: 1004 + message: "TXPOOL_FULL" + data: "Transaction pool is full" + x-error-category: "TXPOOL_ERRORS" + - code: 1005 + message: "INVALID_RLP_DATA" + data: "Transaction Data contains invalid RLP encoding" + x-error-category: "TXPOOL_ERRORS" + - code: 1006 + message: "INVALID_SENDER" + data: "Transaction sender is invalid" + x-error-category: "TXPOOL_ERRORS" + - code: 1007 + message: "NEGATIVE_VALUE" + data: "Transaction with negative value" + x-error-category: "TXPOOL_ERRORS" + - code: 1008 + message: "SENDER_DENYLISTED" + data: "Transaction sender is denylisted" + x-error-category: "TXPOOL_ERRORS" + - code: 1009 + message: "RECEIVER_DENYLISTED" + data: "Transaction receiver is denylisted" + x-error-category: "TXPOOL_ERRORS" + - code: 1010 + message: "CHAIN_ID_MISMATCH" + data: "Transaction chain ID does not match the expected chain ID" + x-error-category: "TXPOOL_ERRORS" diff --git a/src/extensions/components/zk-execution-errors.yaml b/src/extensions/components/zk-execution-errors.yaml new file mode 100644 index 000000000..3f71f37cd --- /dev/null +++ b/src/extensions/components/zk-execution-errors.yaml @@ -0,0 +1,6 @@ +x-error-group: + ZKExecutionErrors: + - code: 2000 + message: "OUT_OF_COUNTERS" + data: "Not enough step counters to continue the execution" + x-error-category: "ZK_EXECUTION_ERRORS" \ No newline at end of file diff --git a/src/extensions/schemas/x-error-category-ranges.json b/src/extensions/schemas/x-error-category-ranges.json new file mode 100644 index 000000000..d7fbdad5f --- /dev/null +++ b/src/extensions/schemas/x-error-category-ranges.json @@ -0,0 +1,83 @@ +{ + "type": "array", + "items": { + "oneOf": [ + { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "code": { "type": "integer", "description": "The code of the error." }, + "message": { "type": "string", "description": "The message of the error." }, + "data": {"description": "The data of the error." }, + "x-error-category": { "type": "string"} + }, + "required": ["code", "message"] + }, + { + "if": { + "properties": { + "x-error-category": { "const": "GAS_ERRORS" } + }, + "required": ["x-error-category"] + }, + "then": { + "properties": { + "code": { "type": "integer", "minimum": 800, "maximum": 999 } + } + } + }, + { + "if": { + "properties": { + "x-error-category": { "const": "EXECUTION_ERRORS" } + }, + "required": ["x-error-category"] + }, + "then": { + "properties": { + "code": { "type": "integer", "minimum": 1, "maximum": 199 } + } + } + }, + { + "if": { + "properties": { + "x-error-category": { "const": "TXPOOL_ERRORS" } + }, + "required": ["x-error-category"] + }, + "then": { + "properties": { + "code": { "type": "integer", "minimum": 1000, "maximum": 1200 } + } + } + }, + { + "if": { + "properties": { + "x-error-category": { "const": "ZK_EXECUTION_ERRORS" } + }, + "required": ["x-error-category"] + }, + "then": { + "properties": { + "code": { "type": "integer", "minimum": 2000, "maximum": 2200 } + } + } + } + ] + } + }, + { + "type": "object", + "properties": { + "$ref": { "type": "string" } + }, + "required": ["$ref"] + } + ] + } +}