diff --git a/docs-v2/openapi-v1.yaml b/docs-v2/openapi-v1.yaml index cd7d1ae74..4f5654243 100755 --- a/docs-v2/openapi-v1.yaml +++ b/docs-v2/openapi-v1.yaml @@ -2672,9 +2672,23 @@ paths: tags: - transaction summary: Get the metadata blob for offline signers. - description: Returns the minimal metadata ("metadata blob" or "proof") needed by offline + description: >- + Returns the minimal metadata ("metadata blob" or "proof") needed by offline signers to decode a transaction's signing payload, along with the metadata hash for the CheckMetadataHash signed extension per RFC-0078. + + + The metadata blob contains type definitions needed to decode the specific + transaction, Merkle proofs verifying these types are part of the full metadata, + and extra chain info (specVersion, specName, base58Prefix, decimals, tokenSymbol). + + + Offline signers can use this to decode the transaction to display what the user + is signing and verify the metadata subset matches the on-chain metadata via + Merkle proofs. + + + Reference: RFC-0078 https://polkadot-fellows.github.io/RFCs/approved/0078-merkleized-metadata.html operationId: getTransactionMetadataBlob requestBody: $ref: '#/components/requestBodies/TransactionMetadataBlob' @@ -3677,6 +3691,88 @@ paths: application/json: schema: $ref: '#/components/schemas/RuntimeSpec' + /runtime/apis: + get: + tags: + - runtime + summary: List runtime APIs available in metadata. + description: Returns runtime API summaries (name, id, method count) for the selected block. Requires metadata v15 or higher. + parameters: + - name: at + in: query + description: Block at which to retrieve runtime API metadata. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApis' + /runtime/apis/{apiId}: + get: + tags: + - runtime + summary: Get a specific runtime API with method signatures. + description: Returns full method metadata (inputs, output, docs) for one runtime API. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: at + in: query + description: Block at which to retrieve runtime API metadata. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApi' + /runtime/apis/{apiId}/{methodId}: + post: + tags: + - runtime + summary: Call a runtime API method with named JSON parameters. + description: Invokes a runtime API method exposed by metadata for the selected block. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: methodId + in: path + description: Runtime API method id or name. + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCallRequest' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCall' /rc/runtime/spec: get: tags: @@ -3699,6 +3795,88 @@ paths: application/json: schema: $ref: '#/components/schemas/RuntimeSpec' + /rc/runtime/apis: + get: + tags: + - rc runtime + summary: List relay chain runtime APIs available in metadata. + description: Returns runtime API summaries (name, id, method count) for the selected relay chain block. Requires metadata v15 or higher. + parameters: + - name: at + in: query + description: Relay chain block hash or height at which to query. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApis' + /rc/runtime/apis/{apiId}: + get: + tags: + - rc runtime + summary: Get a specific relay chain runtime API with method signatures. + description: Returns full method metadata (inputs, output, docs) for one relay chain runtime API. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: at + in: query + description: Relay chain block hash or height at which to query. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApi' + /rc/runtime/apis/{apiId}/{methodId}: + post: + tags: + - rc runtime + summary: Call a relay chain runtime API method with named JSON parameters. + description: Invokes a relay chain runtime API method exposed by metadata for the selected block. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: methodId + in: path + description: Runtime API method id or name. + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCallRequest' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCall' /rc/runtime/metadata: get: tags: @@ -7895,6 +8073,173 @@ components: code: type: string format: hex + RuntimeApiMethodInput: + type: object + properties: + name: + type: string + description: Input argument name as presented by metadata. + id: + type: string + description: Camel-cased identifier used for argument matching. + typeId: + type: string + description: Runtime lookup type id. + type: + type: string + description: Resolved type name when lookup resolution is available. + RuntimeApiMethodOutput: + type: object + properties: + typeId: + type: string + description: Runtime lookup type id. + type: + type: string + description: Resolved type name when lookup resolution is available. + DeprecationInfo: + type: object + nullable: true + description: Deprecation status of a runtime API or method. Null when not available (metadata v15). Present in metadata v16+. + properties: + type: + type: string + enum: + - NotDeprecated + - DeprecatedWithoutNote + - Deprecated + description: Deprecation variant. + note: + type: string + nullable: true + description: Deprecation note. Only set when type is Deprecated. + since: + type: string + nullable: true + description: Version since which the item is deprecated. Only set when type is Deprecated and a since value was provided. + RuntimeApiMethodDescription: + type: object + properties: + name: + type: string + description: Runtime method name as presented by metadata. + id: + type: string + description: Camel-cased runtime method identifier. + docs: + type: string + description: Runtime method documentation joined into a single string. + inputs: + type: array + items: + $ref: '#/components/schemas/RuntimeApiMethodInput' + output: + $ref: '#/components/schemas/RuntimeApiMethodOutput' + deprecationInfo: + $ref: '#/components/schemas/DeprecationInfo' + RuntimeApiSummary: + type: object + properties: + name: + type: string + description: Runtime API name as presented by metadata. + id: + type: string + description: Camel-cased runtime API identifier. + docs: + type: string + description: Runtime API documentation joined into a single string. + methodCount: + type: integer + description: Number of methods exposed by this runtime API. + methods: + type: array + items: + type: object + properties: + name: + type: string + id: + type: string + RuntimeApiDescription: + type: object + properties: + name: + type: string + description: Runtime API name as presented by metadata. + id: + type: string + description: Camel-cased runtime API identifier. + docs: + type: string + description: Runtime API documentation joined into a single string. + version: + type: integer + nullable: true + description: Runtime API version number. Null when not available (metadata v15). Present in metadata v16+. + deprecationInfo: + $ref: '#/components/schemas/DeprecationInfo' + methods: + type: array + items: + $ref: '#/components/schemas/RuntimeApiMethodDescription' + RuntimeApis: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + apis: + type: array + items: + $ref: '#/components/schemas/RuntimeApiSummary' + RuntimeApi: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + api: + $ref: '#/components/schemas/RuntimeApiDescription' + RuntimeApiCallRequest: + type: object + properties: + at: + type: string + description: Optional block hash or height to execute the call at. Defaults to finalized head. + format: unsignedInteger or $hex + params: + type: object + additionalProperties: true + description: Named JSON parameters keyed by method argument names or arg{index}. Omit this field entirely for runtime API methods that do not take parameters. For Option arguments, either omit the argument key (defaults to null) or pass it explicitly as null. + RuntimeApiCall: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + api: + type: object + properties: + name: + type: string + id: + type: string + method: + type: object + properties: + name: + type: string + id: + type: string + result: + description: Runtime API call result serialized via codec.toJSON(). Can be any JSON value. + nullable: true + oneOf: + - type: object + additionalProperties: true + - type: array + items: {} + - type: string + - type: number + - type: boolean RuntimeDispatchInfo: type: object properties: @@ -8625,6 +8970,12 @@ components: schema: $ref: '#/components/schemas/DryRunBody' required: true + RuntimeApiCall: + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCallRequest' + required: true TransactionMetadataBlob: content: application/json: diff --git a/docs/src/openapi-v1.yaml b/docs/src/openapi-v1.yaml index d8b054b13..4f5654243 100755 --- a/docs/src/openapi-v1.yaml +++ b/docs/src/openapi-v1.yaml @@ -3691,6 +3691,88 @@ paths: application/json: schema: $ref: '#/components/schemas/RuntimeSpec' + /runtime/apis: + get: + tags: + - runtime + summary: List runtime APIs available in metadata. + description: Returns runtime API summaries (name, id, method count) for the selected block. Requires metadata v15 or higher. + parameters: + - name: at + in: query + description: Block at which to retrieve runtime API metadata. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApis' + /runtime/apis/{apiId}: + get: + tags: + - runtime + summary: Get a specific runtime API with method signatures. + description: Returns full method metadata (inputs, output, docs) for one runtime API. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: at + in: query + description: Block at which to retrieve runtime API metadata. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApi' + /runtime/apis/{apiId}/{methodId}: + post: + tags: + - runtime + summary: Call a runtime API method with named JSON parameters. + description: Invokes a runtime API method exposed by metadata for the selected block. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: methodId + in: path + description: Runtime API method id or name. + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCallRequest' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCall' /rc/runtime/spec: get: tags: @@ -3713,6 +3795,88 @@ paths: application/json: schema: $ref: '#/components/schemas/RuntimeSpec' + /rc/runtime/apis: + get: + tags: + - rc runtime + summary: List relay chain runtime APIs available in metadata. + description: Returns runtime API summaries (name, id, method count) for the selected relay chain block. Requires metadata v15 or higher. + parameters: + - name: at + in: query + description: Relay chain block hash or height at which to query. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApis' + /rc/runtime/apis/{apiId}: + get: + tags: + - rc runtime + summary: Get a specific relay chain runtime API with method signatures. + description: Returns full method metadata (inputs, output, docs) for one relay chain runtime API. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: at + in: query + description: Relay chain block hash or height at which to query. + required: false + schema: + type: string + description: Block identifier, as the block height or block hash. + format: unsignedInteger or $hex + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApi' + /rc/runtime/apis/{apiId}/{methodId}: + post: + tags: + - rc runtime + summary: Call a relay chain runtime API method with named JSON parameters. + description: Invokes a relay chain runtime API method exposed by metadata for the selected block. + parameters: + - name: apiId + in: path + description: Runtime API id or name. + required: true + schema: + type: string + - name: methodId + in: path + description: Runtime API method id or name. + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCallRequest' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCall' /rc/runtime/metadata: get: tags: @@ -7909,6 +8073,173 @@ components: code: type: string format: hex + RuntimeApiMethodInput: + type: object + properties: + name: + type: string + description: Input argument name as presented by metadata. + id: + type: string + description: Camel-cased identifier used for argument matching. + typeId: + type: string + description: Runtime lookup type id. + type: + type: string + description: Resolved type name when lookup resolution is available. + RuntimeApiMethodOutput: + type: object + properties: + typeId: + type: string + description: Runtime lookup type id. + type: + type: string + description: Resolved type name when lookup resolution is available. + DeprecationInfo: + type: object + nullable: true + description: Deprecation status of a runtime API or method. Null when not available (metadata v15). Present in metadata v16+. + properties: + type: + type: string + enum: + - NotDeprecated + - DeprecatedWithoutNote + - Deprecated + description: Deprecation variant. + note: + type: string + nullable: true + description: Deprecation note. Only set when type is Deprecated. + since: + type: string + nullable: true + description: Version since which the item is deprecated. Only set when type is Deprecated and a since value was provided. + RuntimeApiMethodDescription: + type: object + properties: + name: + type: string + description: Runtime method name as presented by metadata. + id: + type: string + description: Camel-cased runtime method identifier. + docs: + type: string + description: Runtime method documentation joined into a single string. + inputs: + type: array + items: + $ref: '#/components/schemas/RuntimeApiMethodInput' + output: + $ref: '#/components/schemas/RuntimeApiMethodOutput' + deprecationInfo: + $ref: '#/components/schemas/DeprecationInfo' + RuntimeApiSummary: + type: object + properties: + name: + type: string + description: Runtime API name as presented by metadata. + id: + type: string + description: Camel-cased runtime API identifier. + docs: + type: string + description: Runtime API documentation joined into a single string. + methodCount: + type: integer + description: Number of methods exposed by this runtime API. + methods: + type: array + items: + type: object + properties: + name: + type: string + id: + type: string + RuntimeApiDescription: + type: object + properties: + name: + type: string + description: Runtime API name as presented by metadata. + id: + type: string + description: Camel-cased runtime API identifier. + docs: + type: string + description: Runtime API documentation joined into a single string. + version: + type: integer + nullable: true + description: Runtime API version number. Null when not available (metadata v15). Present in metadata v16+. + deprecationInfo: + $ref: '#/components/schemas/DeprecationInfo' + methods: + type: array + items: + $ref: '#/components/schemas/RuntimeApiMethodDescription' + RuntimeApis: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + apis: + type: array + items: + $ref: '#/components/schemas/RuntimeApiSummary' + RuntimeApi: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + api: + $ref: '#/components/schemas/RuntimeApiDescription' + RuntimeApiCallRequest: + type: object + properties: + at: + type: string + description: Optional block hash or height to execute the call at. Defaults to finalized head. + format: unsignedInteger or $hex + params: + type: object + additionalProperties: true + description: Named JSON parameters keyed by method argument names or arg{index}. Omit this field entirely for runtime API methods that do not take parameters. For Option arguments, either omit the argument key (defaults to null) or pass it explicitly as null. + RuntimeApiCall: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + api: + type: object + properties: + name: + type: string + id: + type: string + method: + type: object + properties: + name: + type: string + id: + type: string + result: + description: Runtime API call result serialized via codec.toJSON(). Can be any JSON value. + nullable: true + oneOf: + - type: object + additionalProperties: true + - type: array + items: {} + - type: string + - type: number + - type: boolean RuntimeDispatchInfo: type: object properties: @@ -8639,6 +8970,12 @@ components: schema: $ref: '#/components/schemas/DryRunBody' required: true + RuntimeApiCall: + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeApiCallRequest' + required: true TransactionMetadataBlob: content: application/json: diff --git a/src/chains-config/acalaControllers.ts b/src/chains-config/acalaControllers.ts index b24c5cb13..c9bec3e2f 100644 --- a/src/chains-config/acalaControllers.ts +++ b/src/chains-config/acalaControllers.ts @@ -32,6 +32,7 @@ export const acalaControllers: ControllerConfig = { 'NodeVersion', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/assetHubKusamaControllers.ts b/src/chains-config/assetHubKusamaControllers.ts index 834b4d9b1..8f4771b5b 100644 --- a/src/chains-config/assetHubKusamaControllers.ts +++ b/src/chains-config/assetHubKusamaControllers.ts @@ -72,6 +72,7 @@ export const assetHubKusamaControllers: ControllerConfig = { 'RcPalletsStakingValidators', 'RcPalletsStakingProgress', 'RcPalletsStorage', + 'RcRuntimeApis', 'RcRuntimeCode', 'RcRuntimeMetadata', 'RcRuntimeSpec', @@ -82,6 +83,7 @@ export const assetHubKusamaControllers: ControllerConfig = { 'RcTransactionFeeEstimate', 'RcTransactionMaterial', 'RcTransactionSubmit', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/assetHubNextWestendControllers.ts b/src/chains-config/assetHubNextWestendControllers.ts index 2c004023a..5de054ee0 100644 --- a/src/chains-config/assetHubNextWestendControllers.ts +++ b/src/chains-config/assetHubNextWestendControllers.ts @@ -70,6 +70,7 @@ export const assetHubNextWestendControllers: ControllerConfig = { 'RcPalletsStakingProgress', 'RcPalletsStakingValidators', 'RcPalletsStorage', + 'RcRuntimeApis', 'RcRuntimeCode', 'RcRuntimeMetadata', 'RcRuntimeSpec', @@ -80,6 +81,7 @@ export const assetHubNextWestendControllers: ControllerConfig = { 'RcTransactionFeeEstimate', 'RcTransactionMaterial', 'RcTransactionSubmit', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/assetHubPolkadotControllers.ts b/src/chains-config/assetHubPolkadotControllers.ts index cd1eecd1f..9e9705616 100644 --- a/src/chains-config/assetHubPolkadotControllers.ts +++ b/src/chains-config/assetHubPolkadotControllers.ts @@ -72,6 +72,7 @@ export const assetHubPolkadotControllers: ControllerConfig = { 'RcPalletsStakingValidators', 'RcPalletsStakingProgress', 'RcPalletsStorage', + 'RcRuntimeApis', 'RcRuntimeCode', 'RcRuntimeMetadata', 'RcRuntimeSpec', @@ -82,6 +83,7 @@ export const assetHubPolkadotControllers: ControllerConfig = { 'RcTransactionFeeEstimate', 'RcTransactionMaterial', 'RcTransactionSubmit', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/assetHubWestendControllers.ts b/src/chains-config/assetHubWestendControllers.ts index 34687a47a..dcf7c5917 100644 --- a/src/chains-config/assetHubWestendControllers.ts +++ b/src/chains-config/assetHubWestendControllers.ts @@ -71,6 +71,7 @@ export const assetHubWestendControllers: ControllerConfig = { 'RcPalletsStakingValidators', 'RcPalletsStakingProgress', 'RcPalletsStorage', + 'RcRuntimeApis', 'RcRuntimeCode', 'RcRuntimeMetadata', 'RcRuntimeSpec', @@ -81,6 +82,7 @@ export const assetHubWestendControllers: ControllerConfig = { 'RcTransactionFeeEstimate', 'RcTransactionMaterial', 'RcTransactionSubmit', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/astarControllers.ts b/src/chains-config/astarControllers.ts index a6b10e6ed..9b5e0dbe8 100644 --- a/src/chains-config/astarControllers.ts +++ b/src/chains-config/astarControllers.ts @@ -35,6 +35,7 @@ export const astarControllers: ControllerConfig = { 'PalletsAssets', 'PalletsErrors', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/bifrostControllers.ts b/src/chains-config/bifrostControllers.ts index 039d34497..28482a096 100644 --- a/src/chains-config/bifrostControllers.ts +++ b/src/chains-config/bifrostControllers.ts @@ -31,6 +31,7 @@ export const bifrostControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/bifrostPolkadotControllers.ts b/src/chains-config/bifrostPolkadotControllers.ts index c5e7fa826..03c7d032c 100644 --- a/src/chains-config/bifrostPolkadotControllers.ts +++ b/src/chains-config/bifrostPolkadotControllers.ts @@ -31,6 +31,7 @@ export const bifrostPolkadotControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/calamariControllers.ts b/src/chains-config/calamariControllers.ts index 1fe619f80..64cc53097 100644 --- a/src/chains-config/calamariControllers.ts +++ b/src/chains-config/calamariControllers.ts @@ -35,6 +35,7 @@ export const calamariControllers: ControllerConfig = { 'NodeVersion', 'PalletsStakingProgress', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/coretimeControllers.ts b/src/chains-config/coretimeControllers.ts index 80686b497..fb8cebb0c 100644 --- a/src/chains-config/coretimeControllers.ts +++ b/src/chains-config/coretimeControllers.ts @@ -27,6 +27,7 @@ export const coretimeControllers: ControllerConfig = { 'BlocksRawExtrinsics', 'NodeNetwork', 'NodeVersion', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/crustControllers.ts b/src/chains-config/crustControllers.ts index 5bc51a340..a71cf0e21 100644 --- a/src/chains-config/crustControllers.ts +++ b/src/chains-config/crustControllers.ts @@ -31,6 +31,7 @@ export const crustControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/defaultControllers.ts b/src/chains-config/defaultControllers.ts index 705a1146c..20412d41c 100644 --- a/src/chains-config/defaultControllers.ts +++ b/src/chains-config/defaultControllers.ts @@ -48,6 +48,7 @@ export const defaultControllers: ControllerConfig = { 'PalletsStakingProgress', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/dockMainnetControllers.ts b/src/chains-config/dockMainnetControllers.ts index 2ee022906..7e8295f25 100644 --- a/src/chains-config/dockMainnetControllers.ts +++ b/src/chains-config/dockMainnetControllers.ts @@ -31,6 +31,7 @@ export const dockMainnetControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/dockPoSMainnetControllers.ts b/src/chains-config/dockPoSMainnetControllers.ts index 7c610a0d2..90d7c27b4 100644 --- a/src/chains-config/dockPoSMainnetControllers.ts +++ b/src/chains-config/dockPoSMainnetControllers.ts @@ -34,6 +34,7 @@ export const dockPoSMainnetControllers: ControllerConfig = { 'NodeVersion', 'PalletsStakingProgress', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/dockPoSTestnetControllers.ts b/src/chains-config/dockPoSTestnetControllers.ts index 8c1f7e46a..e9fd5f8d4 100644 --- a/src/chains-config/dockPoSTestnetControllers.ts +++ b/src/chains-config/dockPoSTestnetControllers.ts @@ -34,6 +34,7 @@ export const dockTestnetControllers: ControllerConfig = { 'NodeVersion', 'PalletsStakingProgress', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/heikoControllers.ts b/src/chains-config/heikoControllers.ts index c4599346b..fe222978e 100644 --- a/src/chains-config/heikoControllers.ts +++ b/src/chains-config/heikoControllers.ts @@ -35,6 +35,7 @@ export const heikoControllers: ControllerConfig = { 'PalletsAssets', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/karuraControllers.ts b/src/chains-config/karuraControllers.ts index 668042047..14264a45f 100644 --- a/src/chains-config/karuraControllers.ts +++ b/src/chains-config/karuraControllers.ts @@ -31,6 +31,7 @@ export const karuraControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/kiltControllers.ts b/src/chains-config/kiltControllers.ts index 787feea20..f45f6bbf1 100644 --- a/src/chains-config/kiltControllers.ts +++ b/src/chains-config/kiltControllers.ts @@ -32,6 +32,7 @@ export const kiltControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeSpec', 'RuntimeMetadata', diff --git a/src/chains-config/kulupuControllers.ts b/src/chains-config/kulupuControllers.ts index eafaae286..5a60fc900 100644 --- a/src/chains-config/kulupuControllers.ts +++ b/src/chains-config/kulupuControllers.ts @@ -28,6 +28,7 @@ export const kulupuControllers: ControllerConfig = { 'NodeTransactionPool', 'NodeVersion', 'PalletsAssets', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/kusamaControllers.ts b/src/chains-config/kusamaControllers.ts index 1710d922e..f3772d3ca 100644 --- a/src/chains-config/kusamaControllers.ts +++ b/src/chains-config/kusamaControllers.ts @@ -50,6 +50,7 @@ export const kusamaControllers: ControllerConfig = { 'PalletsStakingValidators', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/mandalaControllers.ts b/src/chains-config/mandalaControllers.ts index 395daaebb..24ed2648a 100644 --- a/src/chains-config/mandalaControllers.ts +++ b/src/chains-config/mandalaControllers.ts @@ -36,6 +36,7 @@ export const mandalaControllers: ControllerConfig = { 'PalletsStakingProgress', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/mantaControllers.ts b/src/chains-config/mantaControllers.ts index 505df0404..1125d9e3f 100644 --- a/src/chains-config/mantaControllers.ts +++ b/src/chains-config/mantaControllers.ts @@ -34,6 +34,7 @@ export const mantaControllers: ControllerConfig = { 'NodeVersion', 'PalletsStakingProgress', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/parallelControllers.ts b/src/chains-config/parallelControllers.ts index 68f1d0e48..efa0aa367 100644 --- a/src/chains-config/parallelControllers.ts +++ b/src/chains-config/parallelControllers.ts @@ -35,6 +35,7 @@ export const parallelControllers: ControllerConfig = { 'PalletsAssets', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/polkadotControllers.ts b/src/chains-config/polkadotControllers.ts index 807d76d2d..333570c6c 100644 --- a/src/chains-config/polkadotControllers.ts +++ b/src/chains-config/polkadotControllers.ts @@ -49,6 +49,7 @@ export const polkadotControllers: ControllerConfig = { 'PalletsStakingValidators', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/polymeshControllers.ts b/src/chains-config/polymeshControllers.ts index d6b217028..2f0db3c77 100644 --- a/src/chains-config/polymeshControllers.ts +++ b/src/chains-config/polymeshControllers.ts @@ -42,6 +42,7 @@ export const polymeshControllers: ControllerConfig = { 'PalletsStakingProgress', 'PalletsStakingValidators', 'PalletsStorage', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/shidenControllers.ts b/src/chains-config/shidenControllers.ts index 42ec916f5..9130742e7 100644 --- a/src/chains-config/shidenControllers.ts +++ b/src/chains-config/shidenControllers.ts @@ -35,6 +35,7 @@ export const shidenControllers: ControllerConfig = { 'PalletsAssets', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/soraControllers.ts b/src/chains-config/soraControllers.ts index 719b71d7d..1f7261ad3 100644 --- a/src/chains-config/soraControllers.ts +++ b/src/chains-config/soraControllers.ts @@ -36,6 +36,7 @@ export const soraControllers: ControllerConfig = { 'PalletsStakingProgress', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/chains-config/westendControllers.ts b/src/chains-config/westendControllers.ts index 40cc08527..7f5440059 100644 --- a/src/chains-config/westendControllers.ts +++ b/src/chains-config/westendControllers.ts @@ -49,6 +49,7 @@ export const westendControllers: ControllerConfig = { 'PalletsStakingValidators', 'PalletsStorage', 'Paras', + 'RuntimeApis', 'RuntimeCode', 'RuntimeMetadata', 'RuntimeSpec', diff --git a/src/controllers/index.ts b/src/controllers/index.ts index c1a12d41a..a9ab52b21 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -73,14 +73,14 @@ import { RcPalletsStakingValidators, RcPalletsStorage, } from './rc/pallets'; -import { RcRuntimeCode, RcRuntimeMetadata, RcRuntimeSpec } from './rc/runtime'; +import { RcRuntimeApis, RcRuntimeCode, RcRuntimeMetadata, RcRuntimeSpec } from './rc/runtime'; import { RcTransactionDryRun, RcTransactionFeeEstimate, RcTransactionMaterial, RcTransactionSubmit, } from './rc/transaction'; -import { RuntimeCode, RuntimeMetadata, RuntimeSpec } from './runtime'; +import { RuntimeApis, RuntimeCode, RuntimeMetadata, RuntimeSpec } from './runtime'; import { TransactionDryRun, TransactionFeeEstimate, @@ -144,6 +144,7 @@ export const controllers = { RcPalletsStakingProgress, RcPalletsStakingValidators, RcPalletsStorage, + RcRuntimeApis, RcRuntimeCode, RcRuntimeMetadata, RcRuntimeSpec, @@ -154,6 +155,7 @@ export const controllers = { RcTransactionFeeEstimate, RcTransactionMaterial, RcTransactionSubmit, + RuntimeApis, RuntimeCode, RuntimeMetadata, RuntimeSpec, diff --git a/src/controllers/rc/runtime/RcRuntimeApisController.ts b/src/controllers/rc/runtime/RcRuntimeApisController.ts new file mode 100644 index 000000000..06f6196f5 --- /dev/null +++ b/src/controllers/rc/runtime/RcRuntimeApisController.ts @@ -0,0 +1,95 @@ +// Copyright 2017-2026 Parity Technologies (UK) Ltd. +// This file is part of Substrate API Sidecar. +// +// Substrate API Sidecar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { RequestHandler } from 'express'; + +import { ApiPromiseRegistry } from '../../../apiRegistry'; +import { RuntimeApisService } from '../../../services'; +import { IRuntimeApiCallBody, IRuntimeApiMethodParam, IRuntimeApiParam } from '../../../types/requests'; +import AbstractController from '../../AbstractController'; + +/** + * Relay chain runtime API discovery and invocation endpoints. + */ +export default class RcRuntimeApisController extends AbstractController { + static controllerName = 'RcRuntimeApis'; + static requiredPallets = []; + + constructor(_api: string) { + const rcApiSpecName = ApiPromiseRegistry.getSpecNameByType('relay')?.values(); + const rcSpecName = rcApiSpecName ? Array.from(rcApiSpecName)[0] : undefined; + if (!rcSpecName) { + throw new Error('Relay chain API spec name is not defined.'); + } + + super(rcSpecName, '/rc/runtime/apis', new RuntimeApisService(rcSpecName)); + this.initRoutes(); + } + + protected initRoutes(): void { + this.safeMountAsyncGetHandlers([ + ['', this.getRuntimeApis], + ['/:apiId', this.getRuntimeApi], + ]); + + this.safeMountAsyncPostHandlers([['/:apiId/:methodId', this.callRuntimeApiMethod]]); + } + + private getRuntimeApis: RequestHandler = async ({ query: { at } }, res): Promise => { + const rcApi = ApiPromiseRegistry.getApiByType('relay')[0]?.api; + + if (!rcApi) { + throw new Error('Relay chain API not found, please use SAS_SUBSTRATE_MULTI_CHAIN_URL env variable'); + } + + const hash = await this.getHashFromAt(at, { api: rcApi }); + const apiAt = at ? await rcApi.at(hash) : rcApi; + + RcRuntimeApisController.sanitizedSend(res, await this.service.fetchRuntimeApis(hash, apiAt)); + }; + + private getRuntimeApi: RequestHandler = async ({ params, query: { at } }, res): Promise => { + const { apiId } = params as IRuntimeApiParam; + const rcApi = ApiPromiseRegistry.getApiByType('relay')[0]?.api; + + if (!rcApi) { + throw new Error('Relay chain API not found, please use SAS_SUBSTRATE_MULTI_CHAIN_URL env variable'); + } + + const hash = await this.getHashFromAt(at, { api: rcApi }); + const apiAt = at ? await rcApi.at(hash) : rcApi; + + RcRuntimeApisController.sanitizedSend(res, await this.service.fetchRuntimeApi(hash, apiId, apiAt)); + }; + + private callRuntimeApiMethod: RequestHandler = async ({ params, body }, res): Promise => { + const { apiId, methodId } = params as IRuntimeApiMethodParam; + const { params: runtimeParams, at } = (body ?? {}) as IRuntimeApiCallBody; + const rcApi = ApiPromiseRegistry.getApiByType('relay')[0]?.api; + + if (!rcApi) { + throw new Error('Relay chain API not found, please use SAS_SUBSTRATE_MULTI_CHAIN_URL env variable'); + } + + const hash = await this.getHashFromAt(at, { api: rcApi }); + const apiAt = at ? await rcApi.at(hash) : rcApi; + + RcRuntimeApisController.sanitizedSend( + res, + await this.service.callRuntimeApi(hash, apiId, methodId, runtimeParams, apiAt), + ); + }; +} diff --git a/src/controllers/rc/runtime/index.ts b/src/controllers/rc/runtime/index.ts index 0f912efb9..5721abb0d 100644 --- a/src/controllers/rc/runtime/index.ts +++ b/src/controllers/rc/runtime/index.ts @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +export { default as RcRuntimeApis } from './RcRuntimeApisController'; export { default as RcRuntimeCode } from './RcRuntimeCodeController'; export { default as RcRuntimeMetadata } from './RcRuntimeMetadataController'; export { default as RcRuntimeSpec } from './RcRuntimeSpecController'; diff --git a/src/controllers/runtime/RuntimeApisController.ts b/src/controllers/runtime/RuntimeApisController.ts new file mode 100644 index 000000000..7c6ebd1b8 --- /dev/null +++ b/src/controllers/runtime/RuntimeApisController.ts @@ -0,0 +1,70 @@ +// Copyright 2017-2026 Parity Technologies (UK) Ltd. +// This file is part of Substrate API Sidecar. +// +// Substrate API Sidecar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { RequestHandler } from 'express'; + +import { RuntimeApisService } from '../../services'; +import { IRuntimeApiCallBody, IRuntimeApiMethodParam, IRuntimeApiParam } from '../../types/requests'; +import AbstractController from '../AbstractController'; + +/** + * Runtime API discovery and invocation endpoints. + */ +export default class RuntimeApisController extends AbstractController { + static controllerName = 'RuntimeApis'; + static requiredPallets = []; + + constructor(api: string) { + super(api, '/runtime/apis', new RuntimeApisService(api)); + this.initRoutes(); + } + + protected initRoutes(): void { + this.safeMountAsyncGetHandlers([ + ['', this.getRuntimeApis], + ['/:apiId', this.getRuntimeApi], + ]); + + this.safeMountAsyncPostHandlers([['/:apiId/:methodId', this.callRuntimeApiMethod]]); + } + + private getRuntimeApis: RequestHandler = async ({ query: { at } }, res): Promise => { + const hash = await this.getHashFromAt(at); + const apiAt = at ? await this.api.at(hash) : this.api; + + RuntimeApisController.sanitizedSend(res, await this.service.fetchRuntimeApis(hash, apiAt)); + }; + + private getRuntimeApi: RequestHandler = async ({ params, query: { at } }, res): Promise => { + const { apiId } = params as IRuntimeApiParam; + const hash = await this.getHashFromAt(at); + const apiAt = at ? await this.api.at(hash) : this.api; + + RuntimeApisController.sanitizedSend(res, await this.service.fetchRuntimeApi(hash, apiId, apiAt)); + }; + + private callRuntimeApiMethod: RequestHandler = async ({ params, body }, res): Promise => { + const { apiId, methodId } = params as IRuntimeApiMethodParam; + const { params: runtimeParams, at } = (body ?? {}) as IRuntimeApiCallBody; + const hash = await this.getHashFromAt(at); + const apiAt = at ? await this.api.at(hash) : this.api; + + RuntimeApisController.sanitizedSend( + res, + await this.service.callRuntimeApi(hash, apiId, methodId, runtimeParams, apiAt), + ); + }; +} diff --git a/src/controllers/runtime/index.ts b/src/controllers/runtime/index.ts index 718b654f8..a38e62a1e 100644 --- a/src/controllers/runtime/index.ts +++ b/src/controllers/runtime/index.ts @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +export { default as RuntimeApis } from './RuntimeApisController'; export { default as RuntimeCode } from './RuntimeCodeController'; export { default as RuntimeMetadata } from './RuntimeMetadataController'; export { default as RuntimeSpec } from './RuntimeSpecController'; diff --git a/src/services/runtime/RuntimeApisService.spec.ts b/src/services/runtime/RuntimeApisService.spec.ts new file mode 100644 index 000000000..adbff58aa --- /dev/null +++ b/src/services/runtime/RuntimeApisService.spec.ts @@ -0,0 +1,238 @@ +// Copyright 2017-2026 Parity Technologies (UK) Ltd. +// This file is part of Substrate API Sidecar. +// +// Substrate API Sidecar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { ApiPromiseRegistry } from '../../apiRegistry'; +import { blockHash789629, defaultMockApi } from '../test-helpers/mock'; +import { RuntimeApisService } from './RuntimeApisService'; + +const runtimeApisService = new RuntimeApisService('mock'); +const queryInfoMock = jest.fn().mockResolvedValue({ + toJSON: () => ({ partialFee: '123' }), +}); + +const runtimeApiAtMock = { + registry: { + metadata: { + apis: { + toArray: () => [ + { + name: { + toString: () => 'TransactionPaymentApi', + }, + docs: { + toArray: () => [ + { + toString: () => 'Transaction payment runtime API', + }, + ], + }, + methods: [ + { + name: { + toString: () => 'query_info', + }, + docs: { + toArray: () => [ + { + toString: () => 'Query transaction fee info', + }, + ], + }, + inputs: [ + { + name: { + toString: () => 'uxt', + }, + type: { + toString: () => '0', + }, + }, + { + name: { + toString: () => 'len', + }, + type: { + toString: () => '1', + }, + }, + { + name: { + toString: () => 'auth_type', + }, + type: { + toString: () => '3', + }, + }, + ], + output: { + toString: () => '2', + }, + }, + ], + }, + ], + }, + }, + lookup: { + getTypeDef: (lookupId: number) => ({ + lookupName: + lookupId === 0 + ? 'Bytes' + : lookupId === 1 + ? 'u32' + : lookupId === 2 + ? 'RuntimeDispatchInfo' + : 'Option', + type: + lookupId === 0 + ? 'Bytes' + : lookupId === 1 + ? 'u32' + : lookupId === 2 + ? 'RuntimeDispatchInfo' + : 'Option', + }), + }, + }, + call: { + transactionPaymentApi: { + queryInfo: queryInfoMock, + }, + }, +}; + +describe('RuntimeApisService', () => { + beforeAll(() => { + jest.spyOn(ApiPromiseRegistry, 'getApi').mockImplementation(() => defaultMockApi); + }); + + beforeEach(() => { + queryInfoMock.mockClear(); + }); + + it('fetchRuntimeApis should return runtime API summaries', async () => { + const result = await runtimeApisService.fetchRuntimeApis(blockHash789629, runtimeApiAtMock as never); + + expect(result.at.hash).toEqual(blockHash789629); + expect(result.at.height).toEqual('789629'); + expect(result.apis.length).toBeGreaterThan(0); + expect(result.apis[0].id).toEqual('transactionPaymentApi'); + }); + + it('fetchRuntimeApi should resolve runtime APIs by name or id', async () => { + const byId = await runtimeApisService.fetchRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + runtimeApiAtMock as never, + ); + const byName = await runtimeApisService.fetchRuntimeApi( + blockHash789629, + 'TransactionPaymentApi', + runtimeApiAtMock as never, + ); + + expect(byId.api.id).toEqual('transactionPaymentApi'); + expect(byName.api.name).toEqual('TransactionPaymentApi'); + }); + + it('callRuntimeApi should invoke a callable runtime API method', async () => { + const result = await runtimeApisService.callRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + 'queryInfo', + { arg0: '0x00', arg1: 1 }, + runtimeApiAtMock as never, + ); + + expect(result.api.id).toEqual('transactionPaymentApi'); + expect(result.method.id).toEqual('queryInfo'); + expect(result.result).toEqual({ partialFee: '123' }); + expect(queryInfoMock).toHaveBeenCalledTimes(1); + expect(queryInfoMock).toHaveBeenCalledWith('0x00', 1, null); + }); + + it('callRuntimeApi should allow omitted Option parameters', async () => { + await runtimeApisService.callRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + 'queryInfo', + { arg0: '0x00', arg1: 1 }, + runtimeApiAtMock as never, + ); + + expect(queryInfoMock).toHaveBeenLastCalledWith('0x00', 1, null); + }); + + it('callRuntimeApi should allow explicit null for Option parameters', async () => { + await runtimeApisService.callRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + 'queryInfo', + { arg0: '0x00', arg1: 1, authType: null }, + runtimeApiAtMock as never, + ); + + expect(queryInfoMock).toHaveBeenLastCalledWith('0x00', 1, null); + }); + + it('callRuntimeApi should reject unknown runtime API names', async () => { + await expect( + runtimeApisService.callRuntimeApi( + blockHash789629, + 'notRealRuntimeApi', + 'anyMethod', + {}, + runtimeApiAtMock as never, + ), + ).rejects.toThrow("Runtime API 'notRealRuntimeApi' not found."); + }); + + it('callRuntimeApi should reject unknown method names', async () => { + await expect( + runtimeApisService.callRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + 'notRealMethod', + {}, + runtimeApiAtMock as never, + ), + ).rejects.toThrow("Runtime API method 'notRealMethod' not found"); + }); + + it('callRuntimeApi should validate missing parameters', async () => { + await expect( + runtimeApisService.callRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + 'queryInfo', + { arg0: '0x00' }, + runtimeApiAtMock as never, + ), + ).rejects.toThrow('Missing parameter'); + }); + + it('callRuntimeApi should validate unknown parameter keys', async () => { + await expect( + runtimeApisService.callRuntimeApi( + blockHash789629, + 'transactionPaymentApi', + 'queryInfo', + { arg0: '0x00', arg1: 1, extra: 'unexpected' }, + runtimeApiAtMock as never, + ), + ).rejects.toThrow('Unknown parameter keys'); + }); +}); diff --git a/src/services/runtime/RuntimeApisService.ts b/src/services/runtime/RuntimeApisService.ts new file mode 100644 index 000000000..02a5b5c34 --- /dev/null +++ b/src/services/runtime/RuntimeApisService.ts @@ -0,0 +1,331 @@ +// Copyright 2017-2026 Parity Technologies (UK) Ltd. +// This file is part of Substrate API Sidecar. +// +// Substrate API Sidecar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { ApiPromise } from '@polkadot/api'; +import { ApiDecoration } from '@polkadot/api/types'; +import type { BlockHash, ItemDeprecationInfoV16 } from '@polkadot/types/interfaces'; +import { stringCamelCase } from '@polkadot/util'; +import { BadRequest } from 'http-errors'; + +import { + IDeprecationInfo, + IRuntimeApi, + IRuntimeApiCall, + IRuntimeApiDescription, + IRuntimeApiMethod, + IRuntimeApis, +} from '../../types/responses'; +import { AbstractService } from '../AbstractService'; + +type RuntimeQueryableApi = ApiPromise | ApiDecoration<'promise'>; +type RegistryRuntimeApiMetadata = ReturnType[number]; +type RegistryRuntimeApiMethodMetadata = RegistryRuntimeApiMetadata['methods'][number]; +type RegistryRuntimeApiMethodInputMetadata = RegistryRuntimeApiMethodMetadata['inputs'][number]; + +export class RuntimeApisService extends AbstractService { + async fetchRuntimeApis(hash: BlockHash, apiAt?: RuntimeQueryableApi): Promise { + const api = apiAt || this.api; + const runtimeApis = this.extractRuntimeApis(api); + return { + at: await this.getAt(hash), + apis: runtimeApis.map((runtimeApi) => ({ + name: runtimeApi.name, + id: runtimeApi.id, + docs: runtimeApi.docs, + methodCount: runtimeApi.methods.length, + methods: runtimeApi.methods.map((method) => ({ + name: method.name, + id: method.id, + })), + })), + }; + } + + async fetchRuntimeApi(hash: BlockHash, apiId: string, apiAt?: RuntimeQueryableApi): Promise { + const api = apiAt || this.api; + const runtimeApis = this.extractRuntimeApis(api); + const runtimeApi = this.resolveRuntimeApi(runtimeApis, apiId); + + return { + at: await this.getAt(hash), + api: runtimeApi, + }; + } + + async callRuntimeApi( + hash: BlockHash, + apiId: string, + methodId: string, + params: unknown, + apiAt?: RuntimeQueryableApi, + ): Promise { + const api = apiAt || this.api; + const runtimeApis = this.extractRuntimeApis(api); + const runtimeApi = this.resolveRuntimeApi(runtimeApis, apiId); + const runtimeMethod = this.resolveRuntimeMethod(runtimeApi, methodId); + const orderedParams = this.orderMethodParams(runtimeMethod, params); + + const callApi = api.call; + const runtimeCallApi = callApi[runtimeApi.id]; + + if (!runtimeCallApi) { + throw new BadRequest( + `Runtime API '${runtimeApi.name}' exists in metadata but is not callable via api.call.${runtimeApi.id}.`, + ); + } + + const runtimeCallMethod = runtimeCallApi[runtimeMethod.id]; + + if (!runtimeCallMethod) { + throw new BadRequest( + `Runtime API method '${runtimeMethod.name}' exists in metadata but is not callable via api.call.${runtimeApi.id}.${runtimeMethod.id}.`, + ); + } + + const result = await runtimeCallMethod(...orderedParams); + + return { + at: await this.getAt(hash), + api: { + name: runtimeApi.name, + id: runtimeApi.id, + }, + method: { + name: runtimeMethod.name, + id: runtimeMethod.id, + }, + result: result.toJSON(), + }; + } + + private extractRuntimeApis(api: RuntimeQueryableApi): IRuntimeApiDescription[] { + const runtimeApis = api.registry.metadata.apis.toArray(); + + if (runtimeApis.length === 0) { + throw new BadRequest('Runtime API metadata is not available. This endpoint requires metadata v15 or higher.'); + } + + return runtimeApis.map((runtimeApi) => this.mapRuntimeApi(api, runtimeApi)); + } + + private mapRuntimeApi(api: RuntimeQueryableApi, runtimeApi: RegistryRuntimeApiMetadata): IRuntimeApiDescription { + const runtimeApiName = runtimeApi.name.toString(); + const runtimeApiId = stringCamelCase(runtimeApiName); + const version = 'version' in runtimeApi ? runtimeApi.version.toNumber() : null; + const deprecationInfo = + 'deprecationInfo' in runtimeApi ? this.mapDeprecationInfo(runtimeApi.deprecationInfo) : null; + + return { + name: runtimeApiName, + id: runtimeApiId, + docs: this.sanitizeMetadataDocs(runtimeApi.docs), + version, + deprecationInfo, + methods: runtimeApi.methods.map((method) => this.mapRuntimeApiMethod(api, method)), + }; + } + + private mapRuntimeApiMethod(api: RuntimeQueryableApi, method: RegistryRuntimeApiMethodMetadata): IRuntimeApiMethod { + const methodName = method.name.toString(); + const methodId = stringCamelCase(methodName); + const outputTypeId = method.output.toString(); + const deprecationInfo = 'deprecationInfo' in method ? this.mapDeprecationInfo(method.deprecationInfo) : null; + + return { + name: methodName, + id: methodId, + docs: this.sanitizeMetadataDocs(method.docs), + inputs: method.inputs.map((input, index) => this.mapRuntimeApiMethodInput(api, input, index)), + output: { + typeId: outputTypeId, + type: this.resolveLookupType(api, outputTypeId), + }, + deprecationInfo, + }; + } + + private mapRuntimeApiMethodInput( + api: RuntimeQueryableApi, + input: RegistryRuntimeApiMethodInputMetadata, + index: number, + ): IRuntimeApiMethod['inputs'][number] { + const inputName = input.name.toString() || `arg${index}`; + const inputTypeId = input.type.toString(); + + return { + name: inputName, + id: stringCamelCase(inputName), + typeId: inputTypeId, + type: this.resolveLookupType(api, inputTypeId), + }; + } + + private resolveRuntimeApi(runtimeApis: IRuntimeApiDescription[], apiId: string): IRuntimeApiDescription { + const normalizedApiId = apiId.toLowerCase(); + const runtimeApi = runtimeApis.find( + (api) => api.id.toLowerCase() === normalizedApiId || api.name.toLowerCase() === normalizedApiId, + ); + + if (!runtimeApi) { + throw new BadRequest(`Runtime API '${apiId}' not found.`); + } + + return runtimeApi; + } + + private resolveRuntimeMethod(runtimeApi: IRuntimeApiDescription, methodId: string): IRuntimeApiMethod { + const normalizedMethodId = methodId.toLowerCase(); + const runtimeMethod = runtimeApi.methods.find( + (method) => method.id.toLowerCase() === normalizedMethodId || method.name.toLowerCase() === normalizedMethodId, + ); + + if (!runtimeMethod) { + throw new BadRequest(`Runtime API method '${methodId}' not found in '${runtimeApi.name}'.`); + } + + return runtimeMethod; + } + + private orderMethodParams(runtimeMethod: IRuntimeApiMethod, params: unknown): unknown[] { + const paramsObj = this.parseParamsObject(params); + const entries = Object.entries(paramsObj); + const providedParams = new Map(); + + for (const [key, value] of entries) { + providedParams.set(this.normalizeParamKey(key), value); + } + + if (!runtimeMethod.inputs.length) { + if (entries.length) { + throw new BadRequest( + `Runtime API method '${runtimeMethod.name}' does not take parameters, but ${entries.length.toString()} were provided.`, + ); + } + + return []; + } + + const consumed = new Set(); + const ordered = runtimeMethod.inputs.map((input, index) => { + const acceptedInputKeys = [input.name, input.id, `arg${index}`]; + const acceptedKeys = new Set(acceptedInputKeys.map((key) => this.normalizeParamKey(key))); + + for (const acceptedKey of acceptedKeys) { + if (providedParams.has(acceptedKey)) { + consumed.add(acceptedKey); + return providedParams.get(acceptedKey); + } + } + + if (this.isOptionalRuntimeMethodInput(input)) { + return null; + } + + throw new BadRequest( + `Missing parameter for input '${input.name}' on '${runtimeMethod.name}'. Accepted keys: ${acceptedInputKeys + .map((key) => `'${key}'`) + .join(', ')}.`, + ); + }); + + const unknownKeys = entries + .map(([key]) => key) + .filter((key) => { + return !consumed.has(this.normalizeParamKey(key)); + }); + + if (unknownKeys.length) { + throw new BadRequest(`Unknown parameter keys for '${runtimeMethod.name}': ${unknownKeys.join(', ')}.`); + } + + return ordered; + } + + private parseParamsObject(params: unknown): Record { + if (params === undefined || params === null) { + return {}; + } + + if (typeof params !== 'object' || Array.isArray(params)) { + throw new BadRequest('Field `params` must be an object keyed by method argument names.'); + } + + return params as Record; + } + + private normalizeParamKey(key: string): string { + return key.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + } + + private isOptionalRuntimeMethodInput(input: IRuntimeApiMethod['inputs'][number]): boolean { + return input.type.startsWith('Option<') || input.type === 'Option'; + } + + private mapDeprecationInfo(deprecationInfo: ItemDeprecationInfoV16): IDeprecationInfo { + if (deprecationInfo.isDeprecated) { + const { note, since } = deprecationInfo.asDeprecated; + return { + type: 'Deprecated', + note: note.toString(), + since: since.isSome ? since.unwrap().toString() : null, + }; + } + + return { + type: deprecationInfo.type, + note: null, + since: null, + }; + } + + private sanitizeMetadataDocs(docs: { toArray: () => Array<{ toString: () => string }> }): string { + const docLines = docs + .toArray() + .map((doc) => doc.toString()) + .filter(Boolean); + + if (!docLines.length) { + return ''; + } + + return docLines.join('\n'); + } + + private resolveLookupType(api: RuntimeQueryableApi, typeId: string): string { + const lookupId = Number(typeId); + + if (!Number.isFinite(lookupId)) { + return typeId; + } + + try { + const typeDef = api.registry.lookup.getTypeDef(lookupId); + + return typeDef.type || 'Unknown'; + } catch { + return typeId; + } + } + + private async getAt(hash: BlockHash) { + const { number } = await this.api.rpc.chain.getHeader(hash); + + return { + hash, + height: number.unwrap().toString(10), + }; + } +} diff --git a/src/services/runtime/index.ts b/src/services/runtime/index.ts index 33e0346db..4c82e7f51 100644 --- a/src/services/runtime/index.ts +++ b/src/services/runtime/index.ts @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +export * from './RuntimeApisService'; export * from './RuntimeCodeService'; export * from './RuntimeMetadataService'; export * from './RuntimeSpecService'; diff --git a/src/types/requests.ts b/src/types/requests.ts index 154361c0a..25591955e 100644 --- a/src/types/requests.ts +++ b/src/types/requests.ts @@ -128,6 +128,19 @@ export interface IContractQueryParam extends Query { storageDepositLimit: string; } +export interface IRuntimeApiCallBody { + params?: Record; + at?: string; +} + +export interface IRuntimeApiParam extends ParamsDictionary { + apiId: string; +} + +export interface IRuntimeApiMethodParam extends IRuntimeApiParam { + methodId: string; +} + export interface IPalletsConstantsParam extends ParamsDictionary { palletId: string; constantItemId: string; diff --git a/src/types/responses/RuntimeApi.ts b/src/types/responses/RuntimeApi.ts new file mode 100644 index 000000000..c6375a0ac --- /dev/null +++ b/src/types/responses/RuntimeApi.ts @@ -0,0 +1,87 @@ +// Copyright 2017-2026 Parity Technologies (UK) Ltd. +// This file is part of Substrate API Sidecar. +// +// Substrate API Sidecar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { IAt } from './At'; + +export interface IDeprecationInfo { + type: 'NotDeprecated' | 'DeprecatedWithoutNote' | 'Deprecated'; + note: string | null; + since: string | null; +} + +interface IRuntimeApiMethodInput { + name: string; + id: string; + typeId: string; + type: string; +} + +interface IRuntimeApiMethodOutput { + typeId: string; + type: string; +} + +export interface IRuntimeApiMethod { + name: string; + id: string; + docs: string; + inputs: IRuntimeApiMethodInput[]; + output: IRuntimeApiMethodOutput; + deprecationInfo: IDeprecationInfo | null; +} + +export interface IRuntimeApiDescription { + name: string; + id: string; + docs: string; + version: number | null; + deprecationInfo: IDeprecationInfo | null; + methods: IRuntimeApiMethod[]; +} + +interface IRuntimeApiSummary { + name: string; + id: string; + docs: string; + methodCount: number; + methods: { + name: string; + id: string; + }[]; +} + +export interface IRuntimeApis { + at: IAt; + apis: IRuntimeApiSummary[]; +} + +export interface IRuntimeApi { + at: IAt; + api: IRuntimeApiDescription; +} + +export interface IRuntimeApiCall { + at: IAt; + api: { + name: string; + id: string; + }; + method: { + name: string; + id: string; + }; + result: unknown; +} diff --git a/src/types/responses/index.ts b/src/types/responses/index.ts index f71c29a7c..823870eda 100644 --- a/src/types/responses/index.ts +++ b/src/types/responses/index.ts @@ -58,6 +58,7 @@ export * from './Paras'; export * from './Payout'; export * from './PoolAssets'; export * from './RcBlockFormat'; +export * from './RuntimeApi'; export * from './RuntimeSpec'; export * from './SanitizedArgs'; export * from './SanitizedCall';