diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 2f8d2023..3ab213ba 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -86,8 +86,6 @@ describe("generated client transport", () => { expectedFrame.set(expectedPayload, str.enc("p:1").length + 1); expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); - expect(transport.truapiVersion).toBe(1); - expect(transport.codecVersion).toBe(1); }); it("uses the transport codec version for generated handshake calls", () => { diff --git a/js/packages/truapi/src/scale.ts b/js/packages/truapi/src/scale.ts index f5670ffa..c9a09a1c 100644 --- a/js/packages/truapi/src/scale.ts +++ b/js/packages/truapi/src/scale.ts @@ -9,12 +9,12 @@ import { Bytes, Enum, Struct, - _void, createCodec, createDecoder, enhanceCodec, - str as scaleStr, + str, u8, + _void, type Codec, } from "scale-ts"; import { @@ -123,49 +123,23 @@ export function TaggedUnion( return Enum(inner) as unknown as Codec>; } -/** - * Wire codec for Rust `CallError`, projected to the public domain error `D`. - * - * Generated TypeScript APIs expose only the domain error union in - * `ResultAsync`. The Rust host still wraps that value in - * `CallError::Domain` on the wire so framework errors can share the response - * channel. Encoding always emits `Domain`; decoding returns the inner domain - * value and throws for framework-level failures that have no public `D` shape. - */ -export function CallError(domain: Codec): Codec { - type WireCallError = - | { tag: "Domain"; value: D } - | { tag: "Denied"; value?: undefined } - | { tag: "Unsupported"; value?: undefined } - | { tag: "MalformedFrame"; value: { reason: string } } - | { tag: "HostFailure"; value: { reason: string } }; +/** Public TS value for Rust's derived `CallError` enum. */ +export type CallErrorValue = + | { tag: "Domain"; value: D } + | { tag: "Denied"; value?: undefined } + | { tag: "Unsupported"; value?: undefined } + | { tag: "MalformedFrame"; value: { reason: string } } + | { tag: "HostFailure"; value: { reason: string } }; - const wire = Enum({ +/** SCALE codec for Rust's derived `CallError` enum. */ +export function CallError(domain: Codec): Codec> { + return TaggedUnion({ Domain: domain, Denied: _void, Unsupported: _void, - MalformedFrame: Struct({ reason: scaleStr }), - HostFailure: Struct({ reason: scaleStr }), - }) as unknown as Codec; - - return enhanceCodec( - wire, - (value: D): WireCallError => ({ tag: "Domain", value }), - (value: WireCallError): D => { - switch (value.tag) { - case "Domain": - return value.value; - case "Denied": - throw new Error("Host denied the request"); - case "Unsupported": - throw new Error("Host does not support this request"); - case "MalformedFrame": - throw new Error(`Malformed request frame: ${value.value.reason}`); - case "HostFailure": - throw new Error(`Host failure: ${value.value.reason}`); - } - }, - ); + MalformedFrame: Struct({ reason: str }), + HostFailure: Struct({ reason: str }), + }) as Codec>; } type TaggedUnionCodecs = { diff --git a/playground/package.json b/playground/package.json index 848f58b8..78fef5f3 100644 --- a/playground/package.json +++ b/playground/package.json @@ -23,7 +23,9 @@ "dependencies": { "@monaco-editor/react": "^4", "@parity/truapi": "link:../js/packages/truapi", - "@polkadot-api/substrate-bindings": "^0.12.0", + "@polkadot-api/metadata-builders": "0.14.2", + "@polkadot-api/substrate-bindings": "0.20.2", + "@polkadot-api/utils": "0.4.0", "monaco-editor": "^0.52", "neverthrow": "^8.2.0", "next": "15.5.18", diff --git a/playground/src/lib/example-helpers.ts b/playground/src/lib/example-helpers.ts index 3cfb5d3c..0ee99018 100644 --- a/playground/src/lib/example-helpers.ts +++ b/playground/src/lib/example-helpers.ts @@ -4,9 +4,26 @@ import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; import { Blake2128Concat, Bytes, + decAnyMetadata, Storage, + unifyMetadata, } from "@polkadot-api/substrate-bindings"; -import type { Client, HexString, StorageResultItem } from "@parity/truapi"; +import { + getDynamicBuilder, + getLookupFn, +} from "@polkadot-api/metadata-builders"; +import { fromHex, toHex } from "@polkadot-api/utils"; +import type { + Client, + HexString, + ProductAccountId, + ProductAccountTxPayload, + RemoteChainHeadFollowItem, + RuntimeSpec, + RuntimeType, + StorageResultItem, + TxPayloadExtension, +} from "@parity/truapi"; export type ChainHeadCtx = { genesisHash: `0x${string}`; @@ -23,6 +40,12 @@ export type AccountIdForDotNsUsername = ( username?: string, ) => Promise>; +export type BuildCreateTransactionPayload = (opts: { + signer: ProductAccountId; + genesisHash: HexString; + callData: HexString; +}) => Promise>; + const usernameOwnerOfStorage = Storage("Resources")("UsernameOwnerOf", [ Bytes(), Blake2128Concat, @@ -176,6 +199,425 @@ export function createAccountIdForDotNsUsername( }; } +export function createBuildCreateTransactionPayload( + truapi: Client, +): BuildCreateTransactionPayload { + return async function buildCreateTransactionPayload(opts) { + const accountResult = await truapi.account.getAccount({ + productAccountId: opts.signer, + }); + if (accountResult.isErr()) { + return err(toError(accountResult.error)); + } + + const built = await buildTransactionContext( + truapi, + opts.genesisHash, + accountResult.value.account.publicKey, + ); + if (built.isErr()) return err(built.error); + + const { metadata, runtime, nonce, genesisHash } = built.value; + const unified = unifyMetadata(decAnyMetadata(metadata)); + const lookupFn = getLookupFn(unified); + const builder = getDynamicBuilder(lookupFn); + const chainState = { + genesisHash: fromHex(genesisHash), + specVersion: runtime.specVersion, + transactionVersion: runtime.transactionVersion ?? 0, + nonce, + }; + + return ok({ + signer: opts.signer, + genesisHash, + callData: opts.callData, + extensions: encodeSignedExtensions( + unified, + lookupFn, + builder, + chainState, + ), + txExtVersion: txExtVersionFromMetadata(unified), + }); + }; +} + +type UnifiedMetadata = ReturnType; +type LookupFn = ReturnType; +type LookupEntry = ReturnType; +type DynamicBuilder = ReturnType; + +type ChainState = { + genesisHash: Uint8Array; + specVersion: number; + transactionVersion: number; + nonce: number; +}; + +type TransactionContext = { + genesisHash: HexString; + metadata: Uint8Array; + nonce: number; + runtime: RuntimeSpec; +}; + +function buildTransactionContext( + truapi: Client, + genesisHash: HexString, + accountPublicKey: HexString, +): Promise> { + return new Promise((resolve) => { + let subscription: ReturnType< + ReturnType["subscribe"] + > | null = null; + const completedOperations = new Map>(); + const operationWaiters = new Map< + string, + (result: Result) => void + >(); + let initialized = false; + let settled = false; + + const settle = (result: Result) => { + if (settled) return; + settled = true; + try { + subscription?.unsubscribe(); + } catch { + /* benign */ + } + resolve(result); + }; + + const finishOperation = ( + operationId: string, + result: Result, + ) => { + const waiter = operationWaiters.get(operationId); + if (waiter) { + operationWaiters.delete(operationId); + waiter(result); + return; + } + completedOperations.set(operationId, result); + }; + + const waitForOperation = ( + operationId: string, + ): Promise> => { + const completed = completedOperations.get(operationId); + if (completed) { + completedOperations.delete(operationId); + return Promise.resolve(completed); + } + return new Promise((operationResolve) => { + operationWaiters.set(operationId, operationResolve); + }); + }; + + const callHead = async ( + hash: HexString, + fn: string, + callParameters: HexString, + ): Promise> => { + if (!subscription) { + return err(new Error("chain head subscription was not initialized")); + } + const result = await truapi.chain.callHead({ + genesisHash, + followSubscriptionId: subscription.subscriptionId, + hash, + function: fn, + callParameters, + }); + if (result.isErr()) return err(toError(result.error)); + if (result.value.operation.tag !== "Started") { + return err(new Error(`chainHead call limit reached for ${fn}`)); + } + return waitForOperation(result.value.operation.value.operationId); + }; + + const handleInitialized = async ( + item: Extract, + ) => { + if (initialized) return; + initialized = true; + const hash = item.value.finalizedBlockHashes[0]; + if (!hash) { + settle( + err(new Error("chainHead initialized without a finalized hash")), + ); + return; + } + const runtime = runtimeSpecFrom(item.value.finalizedBlockRuntime); + if (runtime.isErr()) { + settle(err(runtime.error)); + return; + } + + const [metadata, nonce] = await Promise.all([ + callHead(hash, "Metadata_metadata", "0x"), + callHead(hash, "AccountNonceApi_account_nonce", accountPublicKey), + ]); + if (metadata.isErr()) { + settle(err(metadata.error)); + return; + } + if (nonce.isErr()) { + settle(err(nonce.error)); + return; + } + + const rawMetadata = unwrapOpaqueMetadata(metadata.value); + if (rawMetadata.isErr()) { + settle(err(rawMetadata.error)); + return; + } + + let decodedNonce: number; + try { + decodedNonce = nonceFromRuntimeApiOutput(nonce.value); + } catch (error) { + settle(err(toError(error))); + return; + } + + const followSubscriptionId = subscription?.subscriptionId; + if (followSubscriptionId) { + void truapi.chain.unpinHead({ + genesisHash, + followSubscriptionId, + hashes: [hash], + }); + } + + settle( + ok({ + genesisHash, + metadata: rawMetadata.value, + nonce: decodedNonce, + runtime: runtime.value, + }), + ); + }; + + subscription = truapi.chain + .followHeadSubscribe({ + request: { genesisHash, withRuntime: true }, + }) + .subscribe({ + next: (item) => { + switch (item.tag) { + case "Initialized": + void handleInitialized(item); + return; + case "OperationCallDone": + finishOperation(item.value.operationId, ok(item.value.output)); + return; + case "OperationError": + finishOperation( + item.value.operationId, + err( + new Error(`chainHead operation failed: ${item.value.error}`), + ), + ); + return; + case "OperationInaccessible": + finishOperation( + item.value.operationId, + err(new Error("chainHead operation inaccessible")), + ); + return; + case "Stop": + settle( + err( + new Error( + "chain head subscription stopped before transaction context was built", + ), + ), + ); + return; + } + }, + error: (error) => settle(err(toError(error))), + complete: () => + settle( + err( + new Error( + "chain head subscription completed before transaction context was built", + ), + ), + ), + }); + }); +} + +function runtimeSpecFrom(value?: RuntimeType): Result { + if (!value) return err(new Error("chainHead did not include runtime data")); + if (value.tag === "Invalid") { + return err(new Error(`chainHead runtime invalid: ${value.value.error}`)); + } + if (value.value.transactionVersion === undefined) { + return err(new Error("runtime did not include transactionVersion")); + } + return ok(value.value); +} + +function unwrapOpaqueMetadata(output: HexString): Result { + try { + const raw = Bytes().dec(fromHex(output)); + if ( + raw.length < 5 || + raw[0] !== 0x6d || + raw[1] !== 0x65 || + raw[2] !== 0x74 || + raw[3] !== 0x61 + ) { + return err( + new Error("runtime Metadata_metadata returned invalid metadata"), + ); + } + return ok(raw); + } catch (error) { + return err(toError(error)); + } +} + +function nonceFromRuntimeApiOutput(output: HexString): number { + const bytes = fromHex(output); + if (bytes.length < 4) { + throw new Error("AccountNonceApi_account_nonce returned too few bytes"); + } + return new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ).getUint32(0, true); +} + +function txExtVersionFromMetadata(metadata: UnifiedMetadata): number { + const latestVersion = metadata.extrinsic.version.reduce( + (max, version) => Math.max(max, version), + 0, + ); + return latestVersion === 4 ? 0 : latestVersion; +} + +function encodeSignedExtensions( + metadata: UnifiedMetadata, + lookupFn: LookupFn, + builder: DynamicBuilder, + chainState: ChainState, +): TxPayloadExtension[] { + const exts = metadata.extrinsic.signedExtensions[0] as Array<{ + identifier: string; + type: number; + additionalSigned: number; + }>; + + return exts.map((ext) => { + const values = signedExtensionValues(ext, lookupFn, chainState); + const extra = encodeExtensionField( + builder, + lookupFn, + ext.type, + values.extra, + ); + const additionalSigned = encodeExtensionField( + builder, + lookupFn, + ext.additionalSigned, + values.additionalSigned, + ); + + return { + id: ext.identifier, + extra: toHex(extra) as HexString, + additionalSigned: toHex(additionalSigned) as HexString, + }; + }); +} + +function signedExtensionValues( + ext: { identifier: string; type: number; additionalSigned: number }, + lookupFn: LookupFn, + chainState: ChainState, +): { extra: unknown; additionalSigned: unknown } { + switch (ext.identifier) { + case "CheckNonce": + return { extra: chainState.nonce, additionalSigned: undefined }; + case "CheckSpecVersion": + return { + extra: undefined, + additionalSigned: chainState.specVersion, + }; + case "CheckTxVersion": + return { + extra: undefined, + additionalSigned: chainState.transactionVersion, + }; + case "CheckGenesis": + return { + extra: undefined, + additionalSigned: toHex(chainState.genesisHash), + }; + case "CheckMortality": + return { + extra: { type: "Immortal" }, + additionalSigned: toHex(chainState.genesisHash), + }; + case "VerifyMultiSignature": + return { extra: { type: "Disabled" }, additionalSigned: undefined }; + case "ChargeAssetTxPayment": + return { + extra: { tip: 0, asset_id: undefined }, + additionalSigned: undefined, + }; + case "RestrictOrigins": + return { extra: false, additionalSigned: undefined }; + default: + return { + extra: defaultValueForType(lookupFn(ext.type)), + additionalSigned: defaultValueForType(lookupFn(ext.additionalSigned)), + }; + } +} + +function encodeExtensionField( + builder: DynamicBuilder, + lookupFn: LookupFn, + typeId: number, + value: unknown, +): Uint8Array { + const entry = lookupFn(typeId); + if (!entry || entry.type === "void") return new Uint8Array(0); + const codec = builder.buildDefinition(typeId) as { + enc: (value: unknown) => Uint8Array; + }; + return codec.enc(value); +} + +function defaultValueForType(entry: LookupEntry): unknown { + if (!entry) return undefined; + if (entry.type === "void" || entry.type === "option") return undefined; + if (entry.type === "primitive") { + if (entry.value === "bool") return false; + if (entry.value.startsWith("u") || entry.value.startsWith("i")) return 0; + return undefined; + } + if (entry.type === "compact") return 0; + if (entry.type === "array") return new Uint8Array(entry.len); + if (entry.type === "enum") { + const first = Object.entries(entry.value)[0]; + if (!first) return undefined; + const [name, variant] = first; + if (variant.type === "void") return { type: name }; + return { type: name, value: undefined }; + } + return undefined; +} + function findStorageValue( items: StorageResultItem[], key: HexString, diff --git a/playground/src/lib/example-runner.ts b/playground/src/lib/example-runner.ts index 97cf72df..79f1aee7 100644 --- a/playground/src/lib/example-runner.ts +++ b/playground/src/lib/example-runner.ts @@ -2,8 +2,10 @@ import { transform } from "sucrase"; import type { Subscription, TrUApiClient } from "@parity/truapi"; import { createAccountIdForDotNsUsername, + createBuildCreateTransactionPayload, createWithChainHeadFollow, type AccountIdForDotNsUsername, + type BuildCreateTransactionPayload, type WithChainHeadFollow, } from "./example-helpers"; @@ -39,14 +41,14 @@ function exampleAssert( // Drop any `@parity/truapi` import that does not name value specifiers (e.g. // bare type-only imports left over after sucrase). Named value imports are // rewritten by `TRUAPI_NAMED_IMPORT_RE` below. -const IMPORT_RE = /^\s*import\s+(?!\{)[^;]*?from\s+["']@parity\/truapi["'];?\s*$/gm; +const IMPORT_RE = + /^\s*import\s+(?!\{)[^;]*?from\s+["']@parity\/truapi["'];?\s*$/gm; // `import { PASEO_NEXT_V2_ASSET_HUB, ... } from "@parity/truapi"` // → `const { PASEO_NEXT_V2_ASSET_HUB, ... } = __truapi;` const TRUAPI_NAMED_IMPORT_RE = /^\s*import\s*(\{[^}]*\})\s*from\s+["']@parity\/truapi["'];?\s*$/gm; // `import { from, take, ... } from "rxjs"` → `const { from, take, ... } = __rxjs;` -const RXJS_IMPORT_RE = - /^\s*import\s*(\{[^}]*\})\s*from\s+["']rxjs["'];?\s*$/gm; +const RXJS_IMPORT_RE = /^\s*import\s*(\{[^}]*\})\s*from\s+["']rxjs["'];?\s*$/gm; const EXPORT_RE = /^(\s*)export\s+(async\s+function|function|const|let|var|class)\b/gm; @@ -58,14 +60,16 @@ type ConsoleShim = { warn: (...args: unknown[]) => void; }; -const AsyncFunction = Object.getPrototypeOf( - async function () {}, -).constructor as new (...args: string[]) => ( +const AsyncFunction = Object.getPrototypeOf(async function () {}) + .constructor as new ( + ...args: string[] +) => ( truapi: unknown, __console: ConsoleShim, __rxjs: unknown, withChainHeadFollow: WithChainHeadFollow, accountIdForDotNsUsername: AccountIdForDotNsUsername, + buildCreateTransactionPayload: BuildCreateTransactionPayload, __truapi: unknown, assert: typeof exampleAssert, ) => Promise; @@ -105,6 +109,7 @@ export async function runExample(opts: { rxjs: unknown, withChainHeadFollow: WithChainHeadFollow, accountIdForDotNsUsername: AccountIdForDotNsUsername, + buildCreateTransactionPayload: BuildCreateTransactionPayload, truapiPkg: unknown, assert: typeof exampleAssert, ) => Promise; @@ -115,6 +120,7 @@ export async function runExample(opts: { "__rxjs", "withChainHeadFollow", "accountIdForDotNsUsername", + "buildCreateTransactionPayload", "__truapi", "assert", body, @@ -147,16 +153,22 @@ export async function runExample(opts: { }; const [rxjs, truapiPkg] = await Promise.all([getRxjs(), getTruapiPkg()]); - const withChainHeadFollow = createWithChainHeadFollow(trackingClient as TrUApiClient); + const withChainHeadFollow = createWithChainHeadFollow( + trackingClient as TrUApiClient, + ); const accountIdForDotNsUsername = createAccountIdForDotNsUsername( trackingClient as TrUApiClient, ); + const buildCreateTransactionPayload = createBuildCreateTransactionPayload( + trackingClient as TrUApiClient, + ); const promise = run( trackingClient, consoleShim, rxjs, withChainHeadFollow, accountIdForDotNsUsername, + buildCreateTransactionPayload, truapiPkg, exampleAssert, ); @@ -181,17 +193,17 @@ function createTrackingClient( }); } -function wrapService( - svc: object, - onSub: (sub: Subscription) => void, -): unknown { +function wrapService(svc: object, onSub: (sub: Subscription) => void): unknown { return new Proxy(svc as Record, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); if (typeof value !== "function") return value; return (...args: unknown[]) => { const out = (value as (...a: unknown[]) => unknown).apply(target, args); - if (out && typeof (out as { subscribe?: unknown }).subscribe === "function") { + if ( + out && + typeof (out as { subscribe?: unknown }).subscribe === "function" + ) { return wrapObservable(out as ObservableLike, onSub); } return out; diff --git a/playground/src/lib/monaco-setup.ts b/playground/src/lib/monaco-setup.ts index eb401a25..facddd99 100644 --- a/playground/src/lib/monaco-setup.ts +++ b/playground/src/lib/monaco-setup.ts @@ -97,6 +97,12 @@ export function setupMonaco(m: Monaco): void { ` }): import("rxjs").Observable;`, ` /** Resolve a DotNS username to the owning raw AccountId32 hex string. Defaults to truapi.account.getUserId(). */`, ` function accountIdForDotNsUsername(username?: string): Promise>;`, + ` /** Build a metadata-backed product-account transaction payload for \`truapi.signing.createTransaction\`. */`, + ` function buildCreateTransactionPayload(opts: {`, + ` signer: import("@parity/truapi").ProductAccountId;`, + ` genesisHash: \`0x\${string}\`;`, + ` callData: \`0x\${string}\`;`, + ` }): Promise>;`, ` /**`, ` * Assert a condition, throwing when it does not hold. Examples signal`, ` * failure explicitly with \`assert(...)\`; the diagnosis marks an example`, diff --git a/playground/yarn.lock b/playground/yarn.lock index a485b3d9..01d5a21a 100644 --- a/playground/yarn.lock +++ b/playground/yarn.lock @@ -371,11 +371,6 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.18.tgz#beac6228e60e3ee08ce7a20b7f61b3dc516d4b10" integrity sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg== -"@noble/hashes@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" - integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== - "@noble/hashes@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" @@ -408,10 +403,8 @@ integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== "@parity/truapi@link:../js/packages/truapi": - version "0.3.1" - dependencies: - neverthrow "^8.2.0" - scale-ts "^1.6.1" + version "0.0.0" + uid "" "@playwright/test@^1.49.1": version "1.59.1" @@ -420,20 +413,28 @@ dependencies: playwright "1.59.1" -"@polkadot-api/substrate-bindings@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-bindings/-/substrate-bindings-0.12.0.tgz#2b9cd9ba1b7e29c4a1d0be0575504c02cb435c78" - integrity sha512-cIjDeJRHW6g3z+/55UzpoG4LG1N0HbT4x3NvZsQkYg4eoio9Sw7Pw2aZZX86pWemxc7vQbNw7WSz2Gz+ckdX6Q== +"@polkadot-api/metadata-builders@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@polkadot-api/metadata-builders/-/metadata-builders-0.14.2.tgz#b7081728eb6451ae7cc5d56061b301058b1d4af2" + integrity sha512-nhsFfti0M5tE0LR8++0wHqbP54I/QSFXP/uF5I82MYVSKY0NqIDkIFvr27oLo/ltF+o3vcN44ZJQqvi1k6l9mA== + dependencies: + "@polkadot-api/substrate-bindings" "0.20.2" + "@polkadot-api/utils" "0.4.0" + +"@polkadot-api/substrate-bindings@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@polkadot-api/substrate-bindings/-/substrate-bindings-0.20.2.tgz#d0a74935e1b78583375202fb560b22b2d8001ecf" + integrity sha512-js5UTREoI+FlrPRXMhtKimVWmOqwfNFBnhyshsdloSZHNx/Hulg2RQZNvrVTscyZTf8LyxlGJaH5dsitOUoFKw== dependencies: - "@noble/hashes" "^1.8.0" - "@polkadot-api/utils" "0.1.2" - "@scure/base" "^1.2.5" + "@noble/hashes" "^2.2.0" + "@polkadot-api/utils" "0.4.0" + "@scure/base" "^2.2.0" scale-ts "^1.6.1" -"@polkadot-api/utils@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@polkadot-api/utils/-/utils-0.1.2.tgz#45471371183efaa2fc52f40d84326d84e49c7297" - integrity sha512-yhs5k2a8N1SBJcz7EthZoazzLQUkZxbf+0271Xzu42C5AEM9K9uFLbsB+ojzHEM72O5X8lPtSwGKNmS7WQyDyg== +"@polkadot-api/utils@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@polkadot-api/utils/-/utils-0.4.0.tgz#6ee6476aa40dbdb92e4ded39d2feb9002b5b509a" + integrity sha512-9b/hwRM0UloLWV7SfpNaSD/4k8UQAHoaACAk7Xe+1MlfAm2JtnmPiB1GfGrfTyBlsrJVUIBCZpEmbmxVMaIqBA== "@rollup/rollup-linux-x64-gnu@^4.24.0": version "4.60.4" @@ -450,10 +451,10 @@ resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz" integrity sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag== -"@scure/base@^1.2.5": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" - integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== +"@scure/base@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.2.0.tgz#1311378ed247df6d58f8eb8941921965e97e5747" + integrity sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg== "@swc/helpers@0.5.15": version "0.5.15" diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index 2e8aa459..5cc8c5a9 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -294,6 +294,9 @@ pub fn extract_api(krate: &Crate) -> Result { } for candidate in candidates { + if should_skip_type_candidate(&name, &candidate) { + continue; + } let item = krate .index .get(&candidate.item_id) @@ -393,18 +396,24 @@ fn should_skip_type_name(name: &str) -> bool { | "CancellationToken" | "FrameworkOnlyError" | "Infallible" + | "LatestOf" | "RequestId" | "RuntimeFailure" | "RuntimeFailureKind" ) } +fn should_skip_type_candidate(name: &str, candidate: &ItemCandidate) -> bool { + should_skip_type_name(name) || candidate.path.iter().any(|segment| segment == "latest") +} + fn build_name_context(type_candidates: &BTreeMap>) -> NameContext { let mut ctx = NameContext::default(); for (simple_name, candidates) in type_candidates { - if should_skip_type_name(simple_name) { - continue; - } + let candidates = candidates + .iter() + .filter(|candidate| !should_skip_type_candidate(simple_name, candidate)) + .collect::>(); let has_conflict = candidates.len() > 1; for candidate in candidates { let output_name = if has_conflict { diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index eeec62a1..d833b542 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -1901,6 +1901,12 @@ fn codec_expr_mode( _ => bail!("Unsupported primitive type `{name}` in TypeScript codec generation"), }, TypeRef::Named { name, args } => { + if name == "CallError" && args.len() == 1 { + return Ok(format!( + "S.CallError({})", + codec_expr_mode(&args[0], qualified, ctx, mode)? + )); + } let resolved = resolve_named(name, mode); let target = if qualified { qualify_named(&resolved, mode) @@ -1975,6 +1981,12 @@ fn ts_type_with_named(ty: &TypeRef, qualified: bool, mode: NameMode) -> Result bail!("Unsupported primitive type `{name}` in TypeScript type generation"), }, TypeRef::Named { name, args } => { + if name == "CallError" && args.len() == 1 { + return Ok(format!( + "S.CallErrorValue<{}>", + ts_type_with_named(&args[0], qualified, mode)? + )); + } let resolved = resolve_named(name, mode); let target = if qualified { qualify_named(&resolved, mode) diff --git a/rust/crates/truapi-codegen/src/ts/examples.rs b/rust/crates/truapi-codegen/src/ts/examples.rs index b0cc842f..11b6d151 100644 --- a/rust/crates/truapi-codegen/src/ts/examples.rs +++ b/rust/crates/truapi-codegen/src/ts/examples.rs @@ -45,6 +45,12 @@ declare global { }): Observable; /** Resolve a DotNS username to the owning raw AccountId32 hex string. Defaults to truapi.account.getUserId(). */ function accountIdForDotNsUsername(username?: string): Promise>; + /** Build a metadata-backed product-account transaction payload for `truapi.signing.createTransaction`. */ + function buildCreateTransactionPayload(opts: { + signer: import("@parity/truapi").ProductAccountId; + genesisHash: `0x${string}`; + callData: `0x${string}`; + }): Promise>; /** * Assert a condition, throwing when it does not hold. Examples signal * failure explicitly with `assert(...)`; the playground's diagnosis marks diff --git a/rust/crates/truapi/src/api/mod.rs b/rust/crates/truapi/src/api.rs similarity index 62% rename from rust/crates/truapi/src/api/mod.rs rename to rust/crates/truapi/src/api.rs index 957509e4..3e59e2c5 100644 --- a/rust/crates/truapi/src/api/mod.rs +++ b/rust/crates/truapi/src/api.rs @@ -14,6 +14,8 @@ pub mod resource_allocation; pub mod signing; pub mod statement_store; pub mod system; +#[cfg(debug_assertions)] +pub mod testing; pub mod theme; pub use account::Account; @@ -30,9 +32,12 @@ pub use resource_allocation::ResourceAllocation; pub use signing::Signing; pub use statement_store::StatementStore; pub use system::System; +#[cfg(debug_assertions)] +pub use testing::Testing; pub use theme::Theme; /// The unified TrUAPI contract. +#[cfg(debug_assertions)] pub trait TrUApi: Account + Chain @@ -48,12 +53,59 @@ pub trait TrUApi: + Signing + StatementStore + System + + Testing + Theme + Send + Sync { } +#[cfg(not(debug_assertions))] +pub trait TrUApi: + Account + + Chain + + Chat + + CoinPayment + + Entropy + + LocalStorage + + Notifications + + Payment + + Permissions + + Preimage + + ResourceAllocation + + Signing + + StatementStore + + System + + Theme + + Send + + Sync +{ +} + +#[cfg(debug_assertions)] +impl TrUApi for T where + T: Account + + Chain + + Chat + + CoinPayment + + Entropy + + LocalStorage + + Notifications + + Payment + + Permissions + + Preimage + + ResourceAllocation + + Signing + + StatementStore + + System + + Testing + + Theme + + Send + + Sync +{ +} + +#[cfg(not(debug_assertions))] impl TrUApi for T where T: Account + Chain diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index 7c4e065f..83211328 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -86,10 +86,9 @@ pub trait Account: Send + Sync { /// }, /// ringLocation: { /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, - /// ringRootHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", - /// hints: { palletInstance: 42 }, + /// ringRootHash: "0x...", /// }, - /// context: "0x", + /// context: "0x48656c6c6f", /// }); /// assert(result.isOk(), "createAccountProof failed:", result); /// console.log("account proof created:", result.value); diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 6bc4db1e..699609f1 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -20,18 +20,19 @@ pub trait Signing: Send + Sync { /// Construct a signed transaction for a product account. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; /// - /// const result = await truapi.signing.createTransaction({ + /// const payload = await buildCreateTransactionPayload({ /// signer: { /// dotNsIdentifier: "truapi-playground.dot", /// derivationIndex: 0, /// }, - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, - /// callData: "0x0000", - /// extensions: [], - /// txExtVersion: 0, + /// genesisHash: PASEO_NEXT_V2_INDIVIDUALITY.genesis, + /// callData: "0x000000", /// }); + /// assert(payload.isOk(), "buildCreateTransactionPayload failed:", payload); + /// + /// const result = await truapi.signing.createTransaction(payload.value); /// assert(result.isOk(), "createTransaction failed:", result); /// console.log("transaction created:", result.value); /// ``` @@ -47,18 +48,27 @@ pub trait Signing: Send + Sync { /// Construct a signed transaction for a non-product (legacy) account. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; /// - /// const signerResult = await accountIdForDotNsUsername(); - /// assert(signerResult.isOk(), "accountIdForDotNsUsername failed:", signerResult); - /// console.log("fetched user account:", signerResult.value); + /// const accountsResult = await truapi.account.getLegacyAccounts(); + /// assert(accountsResult.isOk(), "getLegacyAccounts failed:", accountsResult); + /// const legacyAccount = accountsResult.value.accounts[0]; + /// assert(legacyAccount, "no legacy accounts available"); + /// console.log("selected legacy account:", legacyAccount); + /// + /// const payload = await buildCreateTransactionPayload({ + /// signer: { + /// dotNsIdentifier: "truapi-playground.dot", + /// derivationIndex: 0, + /// }, + /// genesisHash: PASEO_NEXT_V2_INDIVIDUALITY.genesis, + /// callData: "0x000000", + /// }); + /// assert(payload.isOk(), "buildCreateTransactionPayload failed:", payload); /// /// const result = await truapi.signing.createTransactionWithLegacyAccount({ - /// signer: signerResult.value, - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, - /// callData: "0x0000", - /// extensions: [], - /// txExtVersion: 0, + /// ...payload.value, + /// signer: legacyAccount.publicKey, /// }); /// assert(result.isOk(), "createTransactionWithLegacyAccount failed:", result); /// console.log("transaction created:", result.value); @@ -78,8 +88,13 @@ pub trait Signing: Send + Sync { /// Sign raw bytes with a non-product account. /// /// ```ts + /// const accountsResult = await truapi.account.getLegacyAccounts(); + /// assert(accountsResult.isOk(), "getLegacyAccounts failed:", accountsResult); + /// const legacyAccount = accountsResult.value.accounts[0]; + /// assert(legacyAccount, "no legacy accounts available"); + /// /// const result = await truapi.signing.signRawWithLegacyAccount({ - /// signer: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + /// signer: legacyAccount.publicKey, /// payload: { /// tag: "Bytes", /// value: { bytes: "0x48656c6c6f" }, @@ -103,8 +118,13 @@ pub trait Signing: Send + Sync { /// ```ts /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; /// + /// const accountsResult = await truapi.account.getLegacyAccounts(); + /// assert(accountsResult.isOk(), "getLegacyAccounts failed:", accountsResult); + /// const legacyAccount = accountsResult.value.accounts[0]; + /// assert(legacyAccount, "no legacy accounts available"); + /// /// const result = await truapi.signing.signPayloadWithLegacyAccount({ - /// signer: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + /// signer: legacyAccount.publicKey, /// payload: { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 5addc9b7..16cfc8cd 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -6,7 +6,8 @@ use crate::versioned::statement_store::{ RemoteStatementStoreCreateProofAuthorizedResponse, RemoteStatementStoreCreateProofError, RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, RemoteStatementStoreSubmitError, RemoteStatementStoreSubmitRequest, - RemoteStatementStoreSubscribeItem, RemoteStatementStoreSubscribeRequest, + RemoteStatementStoreSubscribeError, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, }; use crate::wire; use crate::{CallContext, CallError, Subscription}; @@ -56,8 +57,11 @@ pub trait StatementStore: Send + Sync { &self, _cx: &CallContext, _request: RemoteStatementStoreSubscribeRequest, - ) -> Subscription { - Subscription::empty() + ) -> Result< + Subscription, + CallError, + > { + Err(CallError::unavailable()) } /// Create a proof for a statement. diff --git a/rust/crates/truapi/src/api/testing.rs b/rust/crates/truapi/src/api/testing.rs new file mode 100644 index 00000000..f8d41a2f --- /dev/null +++ b/rust/crates/truapi/src/api/testing.rs @@ -0,0 +1,63 @@ +//! Debug-only API used to verify wire-version and framework-error handling. + +use crate::v01; +use crate::v02; +use crate::versioned::testing::{ + TestingVersionProbeError, TestingVersionProbeRequest, TestingVersionProbeResponse, +}; +use crate::wire; +use crate::{CallContext, CallError}; + +/// Development-only probes for generated client/runtime compatibility. +pub trait Testing: Send + Sync { + /// Echo the request version back to the caller. + /// + /// ```ts + /// const result = await truapi.testing.versionProbe({ + /// message: "hello from V2", + /// marker: 42, + /// }); + /// assert(result.isOk(), "testing version probe failed:", result); + /// console.log("testing version probe:", result.value); + /// ``` + #[wire(request_id = 164)] + async fn version_probe( + &self, + _cx: &CallContext, + request: TestingVersionProbeRequest, + ) -> Result> { + match request { + TestingVersionProbeRequest::V1(inner) => Ok(TestingVersionProbeResponse::V1( + v01::TestingVersionProbeResponse { + received_version: 1, + message: inner.message, + }, + )), + TestingVersionProbeRequest::V2(inner) => Ok(TestingVersionProbeResponse::V2( + v02::TestingVersionProbeResponse { + received_version: 2, + message: inner.message, + marker: inner.marker, + }, + )), + } + } + + /// Echo a framework/domain error on the public response channel. + /// + /// ```ts + /// const result = await truapi.testing.echoError({ + /// error: { tag: "HostFailure", value: { reason: "forced by test" } }, + /// }); + /// assert(result.isErr(), "expected host failure"); + /// console.log("echo error:", result.error); + /// ``` + #[wire(request_id = 166)] + async fn echo_error( + &self, + _cx: &CallContext, + request: v01::EchoErrorRequest, + ) -> Result<(), CallError> { + Err(request.error) + } +} diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index 7637a67b..72be14c0 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -1,30 +1,75 @@ //! TrUAPI trait and type definitions for the host product SDK. //! -//! Concrete wire types live in per-version modules (currently [`v01`]). -//! Versioned envelopes are in [`versioned`]. +//! Concrete wire types live in per-version modules. Versioned envelopes are in +//! [`versioned`]. #![forbid(unsafe_code)] #![allow(async_fn_in_trait)] -use std::convert::Infallible; -use std::pin::Pin; +use core::convert::Infallible; +use core::pin::Pin; +use core::task::{Context, Poll}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::task::{Context, Poll}; use futures::Stream; +use parity_scale_codec::{Decode, Encode}; pub mod api; pub mod v01; +#[cfg(debug_assertions)] +pub mod v02; pub mod versioned; +pub mod latest { + use crate::versioned::{self, Versioned}; + + pub use crate::v01::{ + AccountId, AllocatableResource, GenericError, HostSignPayloadData, NotificationId, + ProductAccountId, RawPayload, RemotePermission, ThemeVariant, + }; + + pub type LatestOf = ::Latest; + + pub type HostAccountGetAliasResponse = + LatestOf; + pub type HostDevicePermissionRequest = + LatestOf; + pub type HostDevicePermissionResponse = + LatestOf; + pub type HostFeatureSupportedRequest = LatestOf; + pub type HostFeatureSupportedResponse = + LatestOf; + pub type HostLocalStorageReadError = + LatestOf; + pub type HostNavigateToError = LatestOf; + pub type HostPushNotificationRequest = + LatestOf; + pub type HostPushNotificationResponse = + LatestOf; + pub type HostRequestResourceAllocationRequest = + LatestOf; + pub type HostSignPayloadRequest = LatestOf; + pub type HostSignPayloadWithLegacyAccountRequest = + LatestOf; + pub type HostSignRawRequest = LatestOf; + pub type HostSignRawWithLegacyAccountRequest = + LatestOf; + pub type LegacyAccountTxPayload = + LatestOf; + pub type PreimageSubmitError = LatestOf; + pub type ProductAccountTxPayload = LatestOf; + pub type RemotePermissionRequest = LatestOf; + pub type RemotePermissionResponse = LatestOf; +} + pub use truapi_macros::wire; /// Per-message id carried from the transport frame. pub type RequestId = String; /// Framework-level outcomes shared by API methods. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum CallError { /// Method-specific failure. Domain(D), diff --git a/rust/crates/truapi/src/v01/mod.rs b/rust/crates/truapi/src/v01.rs similarity index 89% rename from rust/crates/truapi/src/v01/mod.rs rename to rust/crates/truapi/src/v01.rs index 8b34df5a..e3691b0e 100644 --- a/rust/crates/truapi/src/v01/mod.rs +++ b/rust/crates/truapi/src/v01.rs @@ -15,6 +15,8 @@ mod resource_allocation; mod signing; mod statement_store; mod system; +#[cfg(debug_assertions)] +mod testing; mod theme; mod transaction; @@ -33,5 +35,7 @@ pub use resource_allocation::*; pub use signing::*; pub use statement_store::*; pub use system::*; +#[cfg(debug_assertions)] +pub use testing::*; pub use theme::*; pub use transaction::*; diff --git a/rust/crates/truapi/src/v01/testing.rs b/rust/crates/truapi/src/v01/testing.rs new file mode 100644 index 00000000..297553e9 --- /dev/null +++ b/rust/crates/truapi/src/v01/testing.rs @@ -0,0 +1,28 @@ +use parity_scale_codec::{Decode, Encode}; + +use crate::CallError; + +/// V1 request payload for the debug-only Testing API. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct TestingVersionProbeRequest { + pub message: String, +} + +/// Request payload for echoing a framework/domain error through the wire shape. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct EchoErrorRequest { + pub error: CallError, +} + +/// V1 response payload for the debug-only Testing API. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct TestingVersionProbeResponse { + pub received_version: u8, + pub message: String, +} + +/// Domain error for the debug-only Testing API. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum TestingVersionProbeError { + Unknown { reason: String }, +} diff --git a/rust/crates/truapi/src/v02.rs b/rust/crates/truapi/src/v02.rs new file mode 100644 index 00000000..763b7985 --- /dev/null +++ b/rust/crates/truapi/src/v02.rs @@ -0,0 +1,5 @@ +//! TrUAPI Protocol v0.2 type definitions. + +mod testing; + +pub use testing::*; diff --git a/rust/crates/truapi/src/v02/testing.rs b/rust/crates/truapi/src/v02/testing.rs new file mode 100644 index 00000000..cbd918bd --- /dev/null +++ b/rust/crates/truapi/src/v02/testing.rs @@ -0,0 +1,22 @@ +use parity_scale_codec::{Decode, Encode}; + +/// Request payload for the debug-only Testing API. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct TestingVersionProbeRequest { + pub message: String, + pub marker: u32, +} + +/// Response payload for the debug-only Testing API. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct TestingVersionProbeResponse { + pub received_version: u8, + pub message: String, + pub marker: u32, +} + +/// Domain error for the debug-only Testing API. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum TestingVersionProbeError { + Unknown { reason: String }, +} diff --git a/rust/crates/truapi/src/versioned/mod.rs b/rust/crates/truapi/src/versioned.rs similarity index 98% rename from rust/crates/truapi/src/versioned/mod.rs rename to rust/crates/truapi/src/versioned.rs index 9da72067..75f0de53 100644 --- a/rust/crates/truapi/src/versioned/mod.rs +++ b/rust/crates/truapi/src/versioned.rs @@ -44,6 +44,8 @@ pub mod resource_allocation; pub mod signing; pub mod statement_store; pub mod system; +#[cfg(debug_assertions)] +pub mod testing; pub mod theme; #[cfg(test)] diff --git a/rust/crates/truapi/src/versioned/statement_store.rs b/rust/crates/truapi/src/versioned/statement_store.rs index 979885ba..ed31d47e 100644 --- a/rust/crates/truapi/src/versioned/statement_store.rs +++ b/rust/crates/truapi/src/versioned/statement_store.rs @@ -5,6 +5,7 @@ use crate::v01; truapi_macros::versioned_type! { pub enum RemoteStatementStoreSubscribeRequest { V1 => v01::RemoteStatementStoreSubscribeRequest } pub enum RemoteStatementStoreSubscribeItem { V1 => v01::RemoteStatementStoreSubscribeItem } + pub enum RemoteStatementStoreSubscribeError { V1 => v01::GenericError } pub enum RemoteStatementStoreCreateProofRequest { V1 => v01::RemoteStatementStoreCreateProofRequest } pub enum RemoteStatementStoreCreateProofResponse { V1 => v01::RemoteStatementStoreCreateProofResponse } pub enum RemoteStatementStoreCreateProofError { V1 => v01::RemoteStatementStoreCreateProofError } diff --git a/rust/crates/truapi/src/versioned/testing.rs b/rust/crates/truapi/src/versioned/testing.rs new file mode 100644 index 00000000..e5d2cda0 --- /dev/null +++ b/rust/crates/truapi/src/versioned/testing.rs @@ -0,0 +1,18 @@ +//! Versioned wrappers for the debug-only [`Testing`](crate::api::Testing) API. + +use crate::{v01, v02}; + +truapi_macros::versioned_type! { + pub enum TestingVersionProbeRequest { + V1 => v01::TestingVersionProbeRequest, + V2 => v02::TestingVersionProbeRequest, + } + pub enum TestingVersionProbeResponse { + V1 => v01::TestingVersionProbeResponse, + V2 => v02::TestingVersionProbeResponse, + } + pub enum TestingVersionProbeError { + V1 => v01::TestingVersionProbeError, + V2 => v02::TestingVersionProbeError, + } +}