From dcb1c4b598c198964f9a05543c28ef6e343e8387 Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 7 Aug 2026 11:59:31 +0530 Subject: [PATCH 01/16] feat(wire): split the frame discriminant into trait and method bytes --- docs/design/truapi-protocol.md | 208 ++++++++++++++- js/packages/truapi/src/client.test.ts | 114 +++++--- js/packages/truapi/src/client.ts | 123 ++++++--- js/packages/truapi/src/index.ts | 3 +- js/packages/truapi/src/sandbox.test.ts | 17 +- js/packages/truapi/src/transport.ts | 87 +++++-- js/packages/truapi/src/wire-equality.test.ts | 93 +++++-- js/packages/truapi/src/wire-table.test.ts | 48 +++- rust/crates/truapi-codegen/src/main.rs | 8 +- rust/crates/truapi-codegen/src/rust.rs | 216 ++++++++++----- .../truapi-codegen/src/rust/wire_table.rs | 124 ++++++--- rust/crates/truapi-codegen/src/rustdoc.rs | 39 ++- rust/crates/truapi-codegen/src/ts.rs | 245 +++++++++++++----- rust/crates/truapi-macros/src/lib.rs | 63 ++++- rust/crates/truapi-server/src/core.rs | 18 +- rust/crates/truapi-server/src/dispatcher.rs | 114 +++++--- rust/crates/truapi-server/src/frame.rs | 225 ++++++++++------ rust/crates/truapi-server/src/host_core.rs | 3 +- rust/crates/truapi-server/src/native.rs | 18 +- rust/crates/truapi-server/src/subscription.rs | 72 +++-- rust/crates/truapi-server/src/ws_bridge.rs | 6 +- .../truapi-server/tests/golden_frame.rs | 8 +- .../tests/snapshots/golden-account-get.bin | Bin 14 -> 15 bytes .../truapi-server/tests/wire_result_shape.rs | 65 +++-- .../tests/wire_table_ts_parity.rs | 20 +- rust/crates/truapi/README.md | 2 +- rust/crates/truapi/src/api/account.rs | 19 +- rust/crates/truapi/src/api/chain.rs | 29 ++- rust/crates/truapi/src/api/chat.rs | 15 +- rust/crates/truapi/src/api/coin_payment.rs | 21 +- rust/crates/truapi/src/api/entropy.rs | 5 +- rust/crates/truapi/src/api/local_storage.rs | 9 +- rust/crates/truapi/src/api/notifications.rs | 7 +- rust/crates/truapi/src/api/payment.rs | 11 +- rust/crates/truapi/src/api/permissions.rs | 7 +- rust/crates/truapi/src/api/preimage.rs | 7 +- .../truapi/src/api/resource_allocation.rs | 5 +- rust/crates/truapi/src/api/signing.rs | 15 +- rust/crates/truapi/src/api/statement_store.rs | 11 +- rust/crates/truapi/src/api/system.rs | 7 +- rust/crates/truapi/src/api/theme.rs | 5 +- rust/crates/truapi/src/lib.rs | 2 +- scripts/codegen.sh | 4 +- 43 files changed, 1544 insertions(+), 574 deletions(-) diff --git a/docs/design/truapi-protocol.md b/docs/design/truapi-protocol.md index a26b466c0..691945d46 100644 --- a/docs/design/truapi-protocol.md +++ b/docs/design/truapi-protocol.md @@ -49,13 +49,19 @@ struct Message { } ``` -`requestId` ties related messages together (see [Rules](#rules)); `payload` carries the action itself. `Payload` is an enum whose variants are the **actions** — the individual things a Host and Product can say to each other. +`requestId` ties related messages together (see [Rules](#rules)); `payload` carries the action itself. On the wire the envelope is laid out as: + +```text +[requestId: SCALE str][trait: u8][method: u8][payload bytes...] +``` + +The two bytes after the `requestId` are the **`(trait, method)` discriminant pair**. The first byte identifies the API trait (`System`, `Account`, `Chain`, ...); the second identifies the action within that trait. The payload bytes are the SCALE-encoded action value, inlined without a length prefix — the receiver reads to the end of the transport frame. Conceptually, `Payload` is a per-trait enum whose variants are the **actions** — the individual things a Host and Product can say to each other. Actions are not written by hand. They are derived mechanically from the TrUAPI methods, so the high-level method signature and the wire format can never drift apart. One method expands into several actions depending on its shape: a plain call becomes a request/response pair, while a subscription becomes a small lifecycle of start, stop, interrupt, and receive messages. -Each action variant carries an explicit wire-protocol discriminant, its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id`. These ids are assigned per method in the `truapi` crate via the `#[wire(...)]` annotation. They are **append-only and never reused**: once an id ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other. The crate is the source of truth for their values. Discriminant 255 is permanently reserved for protocol errors and cannot be assigned to an API method. +Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `0` (so a handshake request frame always starts `[requestId][0x00][0x00]`). Each action carries an explicit method discriminant within its trait — its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id` — assigned per method via the `#[wire(...)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. -Payloads are versioned independently of the action id, so a single message can evolve without renumbering anything around it. The current version `V1` encodes as discriminant `0`: +Payloads are versioned independently of the discriminant pair, so a single message can evolve without renumbering anything around it. The current version `V1` encodes as discriminant `0`: ```rust enum Versioned { @@ -93,7 +99,7 @@ Actions are derived from the TrUAPI methods using the following algorithm: - Argument: the versioned callback argument `Versioned` - Discriminant: `receive_id` -Put together, a slice of `Payload` looks like this (the payload types are illustrative; see the `truapi` crate for the real ones): +Put together, a slice of one trait's `Payload` actions looks like this (the payload types are illustrative; see the `truapi` crate for the real ones): ```rust enum Payload { @@ -123,7 +129,7 @@ A single byte channel carries every call in both directions at once, so the two Every request expects exactly one response. Each Host or Product MUST send a response message for every request it receives, and the request and its response MUST share the same `requestId` — so the caller can match a reply to the call it made even with many calls in flight. -If a receiver has no handler for an incoming discriminant, it MUST send a protocol-error frame with discriminant 255 and the same `requestId`. The codec-version-1 payload is `V1(UnsupportedMessage { discriminant })`, encoded as the three bytes `[0, 0, unsupported_discriminant]`. The sender maps this method-independent response to its own pending request or subscription and reports a generic unsupported error. +If a receiver has no handler for an incoming `(trait, method)` pair, it MUST send a protocol-error frame addressed to `(255, 255)` with the same `requestId`. The codec-version-2 payload is `V1(UnsupportedMessage { trait_id, method_id })`, encoded as the four bytes `[0, 0, unsupported_trait, unsupported_method]` — one byte cannot name a pair, so the error that describes the envelope grew with it. The sender maps this method-independent response to its own pending request or subscription and reports a generic unsupported error. A receiver MUST NOT answer a protocol-error frame with another protocol error. A protocol-error frame MUST NOT receive another protocol-error response. An unmatched error is ignored, while a malformed protocol-error payload is rejected as a wire violation. These rules prevent error loops without hiding malformed control messages. @@ -159,6 +165,196 @@ Before either side trusts a single byte of payload, they have to agree on how th Handshake calls are bidirectional: both Host and Product can send a handshake request, and both MUST respond to one. An implementation CAN apply a timeout of 10 seconds, after which the connection is marked failed and the call returns a timeout error. The handshake result can be cached. -The handshake request carries the protocol (codec) version as a `u8`. On receiving it, the peer switches its encoding/decoding mode to match; for SCALE codec, the version is `1`. A successful handshake MUST be the first request TrUAPI processes — any other request sent before a successful handshake response MUST fail. +The handshake request carries the protocol (codec) version as a `u8`. On receiving it, the peer switches its encoding/decoding mode to match; for the SCALE codec with the two-byte `(trait, method)` envelope, the version is `2`. (Codec version `1` designates the retired single-byte-discriminant envelope; a peer speaking it fails the handshake.) A successful handshake MUST be the first request TrUAPI processes — any other request sent before a successful handshake response MUST fail. The concrete handshake request, response, and error types are defined in the `truapi` crate. + + +## Appendix: codec-1 → codec-2 discriminant mapping + +Codec version 1 used a single flat `u8` discriminant shared across all traits. Codec version 2 replaces it with the `(trait, method)` pair. This table is the one-time mapping between the two numberings; it exists only to interpret captured codec-1 traffic and old fixtures, and is never extended — new methods only ever get codec-2 pairs. + +Trait id assignment: + +| Trait | Trait id | +| --- | --- | +| `System` | 0 | +| `Account` | 1 | +| `Chain` | 2 | +| `Chat` | 3 | +| `CoinPayment` | 4 | +| `Entropy` | 5 | +| `LocalStorage` | 6 | +| `Notifications` | 7 | +| `Payment` | 8 | +| `Permissions` | 9 | +| `Preimage` | 10 | +| `ResourceAllocation` | 11 | +| `Signing` | 12 | +| `StatementStore` | 13 | +| `Theme` | 14 | + +Per-action mapping (codec-1 flat id → codec-2 `(trait, method)` pair): + +| Action | Codec-1 id | Codec-2 (trait, method) | +| --- | --- | --- | +| `system_handshake_request` | 0 | (0, 0) | +| `system_handshake_response` | 1 | (0, 1) | +| `system_feature_supported_request` | 2 | (0, 2) | +| `system_feature_supported_response` | 3 | (0, 3) | +| `system_navigate_to_request` | 6 | (0, 4) | +| `system_navigate_to_response` | 7 | (0, 5) | +| `account_connection_status_subscribe_start` | 18 | (1, 0) | +| `account_connection_status_subscribe_stop` | 19 | (1, 1) | +| `account_connection_status_subscribe_interrupt` | 20 | (1, 2) | +| `account_connection_status_subscribe_receive` | 21 | (1, 3) | +| `account_get_account_request` | 22 | (1, 4) | +| `account_get_account_response` | 23 | (1, 5) | +| `account_get_account_alias_request` | 24 | (1, 6) | +| `account_get_account_alias_response` | 25 | (1, 7) | +| `account_create_account_proof_request` | 26 | (1, 8) | +| `account_create_account_proof_response` | 27 | (1, 9) | +| `account_get_legacy_accounts_request` | 28 | (1, 10) | +| `account_get_legacy_accounts_response` | 29 | (1, 11) | +| `account_get_user_id_request` | 110 | (1, 12) | +| `account_get_user_id_response` | 111 | (1, 13) | +| `account_request_login_request` | 112 | (1, 14) | +| `account_request_login_response` | 113 | (1, 15) | +| `account_sign_vrf_request` | 164 | (1, 16) | +| `account_sign_vrf_response` | 165 | (1, 17) | +| `chain_follow_head_subscribe_start` | 76 | (2, 0) | +| `chain_follow_head_subscribe_stop` | 77 | (2, 1) | +| `chain_follow_head_subscribe_interrupt` | 78 | (2, 2) | +| `chain_follow_head_subscribe_receive` | 79 | (2, 3) | +| `chain_get_head_header_request` | 80 | (2, 4) | +| `chain_get_head_header_response` | 81 | (2, 5) | +| `chain_get_head_body_request` | 82 | (2, 6) | +| `chain_get_head_body_response` | 83 | (2, 7) | +| `chain_get_head_storage_request` | 84 | (2, 8) | +| `chain_get_head_storage_response` | 85 | (2, 9) | +| `chain_call_head_request` | 86 | (2, 10) | +| `chain_call_head_response` | 87 | (2, 11) | +| `chain_unpin_head_request` | 88 | (2, 12) | +| `chain_unpin_head_response` | 89 | (2, 13) | +| `chain_continue_head_request` | 90 | (2, 14) | +| `chain_continue_head_response` | 91 | (2, 15) | +| `chain_stop_head_operation_request` | 92 | (2, 16) | +| `chain_stop_head_operation_response` | 93 | (2, 17) | +| `chain_get_spec_genesis_hash_request` | 94 | (2, 18) | +| `chain_get_spec_genesis_hash_response` | 95 | (2, 19) | +| `chain_get_spec_chain_name_request` | 96 | (2, 20) | +| `chain_get_spec_chain_name_response` | 97 | (2, 21) | +| `chain_get_spec_properties_request` | 98 | (2, 22) | +| `chain_get_spec_properties_response` | 99 | (2, 23) | +| `chain_broadcast_transaction_request` | 100 | (2, 24) | +| `chain_broadcast_transaction_response` | 101 | (2, 25) | +| `chain_stop_transaction_request` | 102 | (2, 26) | +| `chain_stop_transaction_response` | 103 | (2, 27) | +| `chat_create_room_request` | 38 | (3, 0) | +| `chat_create_room_response` | 39 | (3, 1) | +| `chat_register_bot_request` | 40 | (3, 2) | +| `chat_register_bot_response` | 41 | (3, 3) | +| `chat_list_subscribe_start` | 42 | (3, 4) | +| `chat_list_subscribe_stop` | 43 | (3, 5) | +| `chat_list_subscribe_interrupt` | 44 | (3, 6) | +| `chat_list_subscribe_receive` | 45 | (3, 7) | +| `chat_post_message_request` | 46 | (3, 8) | +| `chat_post_message_response` | 47 | (3, 9) | +| `chat_action_subscribe_start` | 48 | (3, 10) | +| `chat_action_subscribe_stop` | 49 | (3, 11) | +| `chat_action_subscribe_interrupt` | 50 | (3, 12) | +| `chat_action_subscribe_receive` | 51 | (3, 13) | +| `chat_custom_message_render_subscribe_start` | 52 | (3, 14) | +| `chat_custom_message_render_subscribe_stop` | 53 | (3, 15) | +| `chat_custom_message_render_subscribe_interrupt` | 54 | (3, 16) | +| `chat_custom_message_render_subscribe_receive` | 55 | (3, 17) | +| `coin_payment_create_purse_request` | 136 | (4, 0) | +| `coin_payment_create_purse_response` | 137 | (4, 1) | +| `coin_payment_query_purse_request` | 138 | (4, 2) | +| `coin_payment_query_purse_response` | 139 | (4, 3) | +| `coin_payment_rebalance_purse_start` | 140 | (4, 4) | +| `coin_payment_rebalance_purse_stop` | 141 | (4, 5) | +| `coin_payment_rebalance_purse_interrupt` | 142 | (4, 6) | +| `coin_payment_rebalance_purse_receive` | 143 | (4, 7) | +| `coin_payment_delete_purse_start` | 144 | (4, 8) | +| `coin_payment_delete_purse_stop` | 145 | (4, 9) | +| `coin_payment_delete_purse_interrupt` | 146 | (4, 10) | +| `coin_payment_delete_purse_receive` | 147 | (4, 11) | +| `coin_payment_create_receivable_request` | 148 | (4, 12) | +| `coin_payment_create_receivable_response` | 149 | (4, 13) | +| `coin_payment_create_cheque_request` | 150 | (4, 14) | +| `coin_payment_create_cheque_response` | 151 | (4, 15) | +| `coin_payment_deposit_start` | 152 | (4, 16) | +| `coin_payment_deposit_stop` | 153 | (4, 17) | +| `coin_payment_deposit_interrupt` | 154 | (4, 18) | +| `coin_payment_deposit_receive` | 155 | (4, 19) | +| `coin_payment_refund_start` | 156 | (4, 20) | +| `coin_payment_refund_stop` | 157 | (4, 21) | +| `coin_payment_refund_interrupt` | 158 | (4, 22) | +| `coin_payment_refund_receive` | 159 | (4, 23) | +| `coin_payment_listen_for_payment_start` | 160 | (4, 24) | +| `coin_payment_listen_for_payment_stop` | 161 | (4, 25) | +| `coin_payment_listen_for_payment_interrupt` | 162 | (4, 26) | +| `coin_payment_listen_for_payment_receive` | 163 | (4, 27) | +| `entropy_derive_request` | 108 | (5, 0) | +| `entropy_derive_response` | 109 | (5, 1) | +| `local_storage_read_request` | 12 | (6, 0) | +| `local_storage_read_response` | 13 | (6, 1) | +| `local_storage_write_request` | 14 | (6, 2) | +| `local_storage_write_response` | 15 | (6, 3) | +| `local_storage_clear_request` | 16 | (6, 4) | +| `local_storage_clear_response` | 17 | (6, 5) | +| `notifications_send_push_notification_request` | 4 | (7, 0) | +| `notifications_send_push_notification_response` | 5 | (7, 1) | +| `notifications_cancel_push_notification_request` | 134 | (7, 2) | +| `notifications_cancel_push_notification_response` | 135 | (7, 3) | +| `payment_balance_subscribe_start` | 118 | (8, 0) | +| `payment_balance_subscribe_stop` | 119 | (8, 1) | +| `payment_balance_subscribe_interrupt` | 120 | (8, 2) | +| `payment_balance_subscribe_receive` | 121 | (8, 3) | +| `payment_top_up_request` | 122 | (8, 4) | +| `payment_top_up_response` | 123 | (8, 5) | +| `payment_request_request` | 124 | (8, 6) | +| `payment_request_response` | 125 | (8, 7) | +| `payment_status_subscribe_start` | 126 | (8, 8) | +| `payment_status_subscribe_stop` | 127 | (8, 9) | +| `payment_status_subscribe_interrupt` | 128 | (8, 10) | +| `payment_status_subscribe_receive` | 129 | (8, 11) | +| `permissions_request_device_permission_request` | 8 | (9, 0) | +| `permissions_request_device_permission_response` | 9 | (9, 1) | +| `permissions_request_remote_permission_request` | 10 | (9, 2) | +| `permissions_request_remote_permission_response` | 11 | (9, 3) | +| `preimage_lookup_subscribe_start` | 64 | (10, 0) | +| `preimage_lookup_subscribe_stop` | 65 | (10, 1) | +| `preimage_lookup_subscribe_interrupt` | 66 | (10, 2) | +| `preimage_lookup_subscribe_receive` | 67 | (10, 3) | +| `preimage_submit_request` | 68 | (10, 4) | +| `preimage_submit_response` | 69 | (10, 5) | +| `resource_allocation_request_request` | 130 | (11, 0) | +| `resource_allocation_request_response` | 131 | (11, 1) | +| `signing_create_transaction_request` | 30 | (12, 0) | +| `signing_create_transaction_response` | 31 | (12, 1) | +| `signing_create_transaction_with_legacy_account_request` | 32 | (12, 2) | +| `signing_create_transaction_with_legacy_account_response` | 33 | (12, 3) | +| `signing_sign_raw_with_legacy_account_request` | 34 | (12, 4) | +| `signing_sign_raw_with_legacy_account_response` | 35 | (12, 5) | +| `signing_sign_payload_with_legacy_account_request` | 36 | (12, 6) | +| `signing_sign_payload_with_legacy_account_response` | 37 | (12, 7) | +| `signing_sign_raw_request` | 114 | (12, 8) | +| `signing_sign_raw_response` | 115 | (12, 9) | +| `signing_sign_payload_request` | 116 | (12, 10) | +| `signing_sign_payload_response` | 117 | (12, 11) | +| `statement_store_subscribe_start` | 56 | (13, 0) | +| `statement_store_subscribe_stop` | 57 | (13, 1) | +| `statement_store_subscribe_interrupt` | 58 | (13, 2) | +| `statement_store_subscribe_receive` | 59 | (13, 3) | +| `statement_store_create_proof_request` | 60 | (13, 4) | +| `statement_store_create_proof_response` | 61 | (13, 5) | +| `statement_store_submit_request` | 62 | (13, 6) | +| `statement_store_submit_response` | 63 | (13, 7) | +| `statement_store_create_proof_authorized_request` | 132 | (13, 8) | +| `statement_store_create_proof_authorized_response` | 133 | (13, 9) | +| `theme_subscribe_start` | 104 | (14, 0) | +| `theme_subscribe_stop` | 105 | (14, 1) | +| `theme_subscribe_interrupt` | 106 | (14, 2) | +| `theme_subscribe_receive` | 107 | (14, 3) | diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 4861891f8..48d20a48e 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -14,7 +14,12 @@ import type { Codec } from "./scale.js"; import { createClient, SubscriptionError } from "./generated/client.js"; import * as T from "./generated/types.js"; import * as W from "./generated/wire-table.js"; -import { encodeWireMessage, PROTOCOL_ERROR_ID, UnsupportedMessageError } from "./transport.js"; +import { + encodeWireMessage, + PROTOCOL_ERROR_METHOD_ID, + PROTOCOL_ERROR_TRAIT_ID, + UnsupportedMessageError, +} from "./transport.js"; /** Wrap a codec in the `{ V1: [0, codec] }` indexed-tagged-union envelope. */ const versionedV1 = (codec: Codec) => indexedTaggedUnion({ V1: [0, codec] }); @@ -154,14 +159,23 @@ function protocolError(requestId: string, payload: Uint8Array): Uint8Array { return unwrap( encodeWireMessage({ requestId, - payload: { id: PROTOCOL_ERROR_ID, value: payload }, + payload: { + traitId: PROTOCOL_ERROR_TRAIT_ID, + methodId: PROTOCOL_ERROR_METHOD_ID, + value: payload, + }, }), "encode protocol error", ); } -function unsupportedMessage(requestId: string, discriminant: number): Uint8Array { - return protocolError(requestId, new Uint8Array([0, 0, discriminant])); +function unsupportedMessage( + requestId: string, + traitId: number, + methodId: number, +): Uint8Array { + // [0] version index, [0] variant index, then the unsupported pair. + return protocolError(requestId, new Uint8Array([0, 0, traitId, methodId])); } describe("generated client transport", () => { @@ -186,10 +200,11 @@ describe("generated client transport", () => { void client.account.getAccount(request); const expectedPayload = T.VersionedHostAccountGetRequest.enc({ tag: "V1", value: request }); - const expectedFrame = new Uint8Array(str.enc("p:1").length + 1 + expectedPayload.length); + const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 22; - expectedFrame.set(expectedPayload, str.enc("p:1").length + 1); + expectedFrame[str.enc("p:1").length] = 1; // account trait + expectedFrame[str.enc("p:1").length + 1] = 4; // get_account request + expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); }); @@ -203,12 +218,13 @@ describe("generated client transport", () => { const expectedPayload = T.VersionedHostHandshakeRequest.enc({ tag: "V1", - value: { codecVersion: 1 }, + value: { codecVersion: 2 }, }); - const expectedFrame = new Uint8Array(str.enc("p:1").length + 1 + expectedPayload.length); + const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 0; - expectedFrame.set(expectedPayload, str.enc("p:1").length + 1); + expectedFrame[str.enc("p:1").length] = 0; // system trait + expectedFrame[str.enc("p:1").length + 1] = 0; // handshake request + expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); }); @@ -223,7 +239,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: "p:1", payload: { - id: W.SYSTEM_HANDSHAKE.response, + traitId: W.SYSTEM_HANDSHAKE.trait, + + methodId: W.SYSTEM_HANDSHAKE.response, value: handshakeResponsePayload({ success: true, value: undefined }), }, }), @@ -284,7 +302,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: "p:1", payload: { - id: W.ACCOUNT_GET_ACCOUNT.response, + traitId: W.ACCOUNT_GET_ACCOUNT.trait, + + methodId: W.ACCOUNT_GET_ACCOUNT.response, value: accountGetResponsePayload({ success: false, value: { tag: "Domain", value: reason }, @@ -366,13 +386,20 @@ describe("generated client transport", () => { const transport = createTransport(fixture.provider); const errors: Error[] = []; const subscription = transport.subscribeRaw({ - ids: { start: 194, stop: 195, interrupt: 196, receive: 197 }, + ids: { trait: 7, start: 194, stop: 195, interrupt: 196, receive: 197 }, payload: new Uint8Array(), onReceive: () => {}, onClose: (error) => errors.push(error), }); - fixture.receive(unsupportedMessage(subscription.subscriptionId, 195)); - fixture.receive(unsupportedMessage(subscription.subscriptionId, 194)); + // Right trait, wrong method: an error about our stop id is not about our + // start, so it must not end the subscription. + fixture.receive(unsupportedMessage(subscription.subscriptionId, 7, 195)); + // Right METHOD, wrong trait. Under a one-byte discriminant these two + // were indistinguishable; the pair is the whole point, so a trait-8 + // error about method 194 must be ignored here. + fixture.receive(unsupportedMessage(subscription.subscriptionId, 8, 194)); + // Our actual start pair: this one ends it. + fixture.receive(unsupportedMessage(subscription.subscriptionId, 7, 194)); subscription.unsubscribe(); expect(errors).toHaveLength(1); @@ -381,11 +408,13 @@ describe("generated client transport", () => { expect({ name: unsupported.name, message: unsupported.message, - discriminant: unsupported.discriminant, + traitId: unsupported.traitId, + methodId: unsupported.methodId, }).toEqual({ name: "UnsupportedMessageError", - message: "Peer does not support wire message 194", - discriminant: 194, + message: "Peer does not support wire message (7, 194)", + traitId: 7, + methodId: 194, }); expect(fixture.sent).toHaveLength(1); }); @@ -401,6 +430,7 @@ describe("generated client transport", () => { fixture.receive( unsupportedMessage( subscription.subscriptionId, + W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.start, ), ); @@ -409,9 +439,11 @@ describe("generated client transport", () => { expect(errors[0]).toBeInstanceOf(SubscriptionError); expect(errors[0].reason).toBeUndefined(); expect(errors[0].cause).toBeInstanceOf(UnsupportedMessageError); - expect((errors[0].cause as UnsupportedMessageError).discriminant).toBe( + const cause = errors[0].cause as UnsupportedMessageError; + expect([cause.traitId, cause.methodId]).toEqual([ + W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.start, - ); + ]); expect(fixture.sent).toHaveLength(1); }); @@ -567,12 +599,12 @@ describe("generated client transport", () => { const requestPayload = T.VersionedHostHandshakeRequest.enc({ tag: "V1", - value: { codecVersion: 1 }, + value: { codecVersion: 2 }, }); const requestFrame = unwrap( encodeWireMessage({ requestId: "h:1", - payload: { id: W.SYSTEM_HANDSHAKE.request, value: requestPayload }, + payload: { traitId: W.SYSTEM_HANDSHAKE.trait, methodId: W.SYSTEM_HANDSHAKE.request, value: requestPayload }, }), "encode inbound handshake_request", ); @@ -582,7 +614,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: "h:1", payload: { - id: W.SYSTEM_HANDSHAKE.response, + traitId: W.SYSTEM_HANDSHAKE.trait, + + methodId: W.SYSTEM_HANDSHAKE.response, value: handshakeResponsePayload({ success: true, value: undefined }), }, }), @@ -605,7 +639,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, value: T.VersionedHostAccountConnectionStatusSubscribeItem.enc({ tag: "V1", value: "Connected", @@ -792,7 +828,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.interrupt, + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.interrupt, value: _void.enc(undefined), }, }), @@ -824,7 +862,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.PAYMENT_BALANCE_SUBSCRIBE.interrupt, + traitId: W.PAYMENT_BALANCE_SUBSCRIBE.trait, + + methodId: W.PAYMENT_BALANCE_SUBSCRIBE.interrupt, value: versionedV1(CallError(T.VersionedHostPaymentBalanceSubscribeError)).enc({ tag: "V1", value: callError, @@ -861,7 +901,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.COIN_PAYMENT_REBALANCE_PURSE.interrupt, + traitId: W.COIN_PAYMENT_REBALANCE_PURSE.trait, + + methodId: W.COIN_PAYMENT_REBALANCE_PURSE.interrupt, value: versionedV1( CallError(T.VersionedHostCoinPaymentRebalancePurseError), ).enc({ tag: "V1", value: callError }), @@ -894,7 +936,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, value: _void.enc(undefined), }, }), @@ -912,7 +956,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.stop, + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.stop, value: _void.enc(undefined), }, }), @@ -924,7 +970,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, value: T.VersionedHostAccountConnectionStatusSubscribeItem.enc({ tag: "V1", value: "Connected", @@ -955,7 +1003,9 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: sub.subscriptionId, payload: { - id: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.stop, + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.stop, value: _void.enc(undefined), }, }), diff --git a/js/packages/truapi/src/client.ts b/js/packages/truapi/src/client.ts index 66c48e32c..e3c5c6c2f 100644 --- a/js/packages/truapi/src/client.ts +++ b/js/packages/truapi/src/client.ts @@ -3,7 +3,8 @@ import { errAsync, okAsync, ResultAsync } from "neverthrow"; import { decodeWireMessage, encodeWireMessage, - PROTOCOL_ERROR_ID, + PROTOCOL_ERROR_METHOD_ID, + PROTOCOL_ERROR_TRAIT_ID, type HostInitiatedSubscriptionHandler, type ObservableSource, type ProtocolMessage, @@ -33,9 +34,15 @@ import * as W from "./generated/wire-table.js"; export type { Subscription, TrUApiTransport }; -const UNANSWERED_WIRE_IDS = new Set( +const UNANSWERED_WIRE_IDS = new Set( Object.values(W).flatMap((ids) => - "response" in ids ? [ids.response] : [ids.stop, ids.interrupt, ids.receive], + "response" in ids + ? [`${ids.trait}:${ids.response}`] + : [ + `${ids.trait}:${ids.stop}`, + `${ids.trait}:${ids.interrupt}`, + `${ids.trait}:${ids.receive}`, + ], ), ); @@ -154,10 +161,18 @@ function unwrapVersionedWireValue(value: unknown): unknown { return isVersionedWireValue(value) ? value.value : value; } -function decodeUnsupportedMessage(payload: Uint8Array): number { - if (payload.length !== 3) { +/** + * Decode `V1(UnsupportedMessage { trait_id, method_id })`. Codec 2 addresses a + * frame by a pair, so the payload is four bytes: version index, error variant + * index, then the trait and method of the frame the peer could not handle. + */ +function decodeUnsupportedMessage(payload: Uint8Array): { + traitId: number; + methodId: number; +} { + if (payload.length !== 4) { throw new Error( - `Malformed protocol error payload: expected 3 bytes, received ${payload.length}`, + `Malformed protocol error payload: expected 4 bytes, received ${payload.length}`, ); } if (payload[0] !== 0) { @@ -170,7 +185,7 @@ function decodeUnsupportedMessage(payload: Uint8Array): number { `Malformed protocol error payload: unknown error discriminant ${payload[1]}`, ); } - return payload[2]; + return { traitId: payload[2], methodId: payload[3] }; } /** @@ -213,7 +228,9 @@ export function createTransport( handler?: (request: unknown) => ObservableSource; instances: Map; }; - const hostRoutes = new Map(); + // Keyed by the full (trait, method) start pair: a bare start id would + // collide the moment two traits both number a subscription the same. + const hostRoutes = new Map(); /** * Normalize arbitrary thrown values into `Error` instances. @@ -267,31 +284,47 @@ export function createTransport( } const { requestId, payload } = decoded.value; - if (payload.id === PROTOCOL_ERROR_ID) { - let discriminant: number; + if ( + payload.traitId === PROTOCOL_ERROR_TRAIT_ID && + payload.methodId === PROTOCOL_ERROR_METHOD_ID + ) { + let unsupported: { traitId: number; methodId: number }; try { - discriminant = decodeUnsupportedMessage(payload.value); + unsupported = decodeUnsupportedMessage(payload.value); } catch (error) { closeWithError(error); return; } + // Match on the whole pair: a bare method id would alias across traits and + // could resolve the wrong pending call. const request = pending.get(requestId); - if (request?.ids.request === discriminant) { + if ( + request?.ids.trait === unsupported.traitId && + request?.ids.request === unsupported.methodId + ) { pending.delete(requestId); request.resolveUnsupported(); return; } const subscription = subscriptions.get(requestId); - if (subscription?.ids.start === discriminant) { + if ( + subscription?.ids.trait === unsupported.traitId && + subscription?.ids.start === unsupported.methodId + ) { subscriptions.delete(requestId); - subscription.onClose?.(new UnsupportedMessageError(discriminant)); + subscription.onClose?.( + new UnsupportedMessageError(unsupported.traitId, unsupported.methodId), + ); } return; } - if (payload.id === W.SYSTEM_HANDSHAKE.request) { + if ( + payload.traitId === W.SYSTEM_HANDSHAKE.trait && + payload.methodId === W.SYSTEM_HANDSHAKE.request + ) { // Auto-respond to inbound `host_handshake_request` frames. // // Legacy hosts shipping `@novasamatech/host-api@0.6.x` (e.g. dotli) @@ -321,7 +354,8 @@ export function createTransport( send({ requestId, payload: { - id: W.SYSTEM_HANDSHAKE.response, + traitId: W.SYSTEM_HANDSHAKE.trait, + methodId: W.SYSTEM_HANDSHAKE.response, value: response, }, }); @@ -331,13 +365,17 @@ export function createTransport( return; } - const hostRoute = hostRoutes.get(payload.id); + const hostRoute = hostRoutes.get(`${payload.traitId}:${payload.methodId}`); if (hostRoute) { startHostSubscription(hostRoute, requestId, payload.value); return; } for (const candidate of hostRoutes.values()) { - if (payload.id !== candidate.ids.stop) continue; + if ( + payload.traitId !== candidate.ids.trait || + payload.methodId !== candidate.ids.stop + ) + continue; const bufferedIndex = candidate.buffered.findIndex( (start) => start.requestId === requestId, ); @@ -351,7 +389,13 @@ export function createTransport( } const p = pending.get(requestId); - if (p && payload.id === p.ids.response) { + if (p) { + if ( + payload.traitId !== p.ids.trait || + payload.methodId !== p.ids.response + ) { + return; + } pending.delete(requestId); try { p.resolve(payload.value); @@ -363,7 +407,10 @@ export function createTransport( const subscription = subscriptions.get(requestId); if (subscription) { - if (payload.id === subscription.ids.receive) { + if ( + payload.traitId === subscription.ids.trait && + payload.methodId === subscription.ids.receive + ) { try { subscription.onReceive(payload.value); } catch (error) { @@ -374,15 +421,17 @@ export function createTransport( subscriptions.delete(requestId); subscription.onClose?.(toError(error)); } - return; - } else if (payload.id === subscription.ids.interrupt) { + } else if ( + payload.traitId === subscription.ids.trait && + payload.methodId === subscription.ids.interrupt + ) { subscriptions.delete(requestId); subscription.onInterrupt?.(payload.value); return; } } - if (UNANSWERED_WIRE_IDS.has(payload.id)) { + if (UNANSWERED_WIRE_IDS.has(`${payload.traitId}:${payload.methodId}`)) { return; } @@ -390,8 +439,14 @@ export function createTransport( send({ requestId, payload: { - id: PROTOCOL_ERROR_ID, - value: new Uint8Array([0, 0, payload.id]), + traitId: PROTOCOL_ERROR_TRAIT_ID, + methodId: PROTOCOL_ERROR_METHOD_ID, + value: new Uint8Array([ + 0, + 0, + payload.traitId, + payload.methodId, + ]), }, }); } catch { @@ -539,7 +594,8 @@ export function createTransport( send({ requestId, payload: { - id: ids.request, + traitId: ids.trait, + methodId: ids.request, value: payload, }, }); @@ -580,7 +636,8 @@ export function createTransport( send({ requestId, payload: { - id: ids.start, + traitId: ids.trait, + methodId: ids.start, value: payload, }, }); @@ -600,7 +657,8 @@ export function createTransport( send({ requestId, payload: { - id: ids.stop, + traitId: ids.trait, + methodId: ids.stop, value: _void.enc(undefined), }, }); @@ -617,8 +675,11 @@ export function createTransport( interruptPayload, bufferCapacity, }: RegisterHostInitiatedSubscriptionParams) { - if (hostRoutes.has(ids.start)) { - throw new Error(`host-initiated subscription ${ids.start} is already registered`); + const routeKey = `${ids.trait}:${ids.start}`; + if (hostRoutes.has(routeKey)) { + throw new Error( + `host-initiated subscription (${ids.trait}, ${ids.start}) is already registered`, + ); } const route: HostRoute = { ids, @@ -629,7 +690,7 @@ export function createTransport( buffered: [], instances: new Map(), }; - hostRoutes.set(ids.start, route); + hostRoutes.set(routeKey, route); return { setHandler(handler: HostInitiatedSubscriptionHandler) { const installed = handler as ( diff --git a/js/packages/truapi/src/index.ts b/js/packages/truapi/src/index.ts index 726ddd851..4321399f5 100644 --- a/js/packages/truapi/src/index.ts +++ b/js/packages/truapi/src/index.ts @@ -16,7 +16,8 @@ export type { } from "./transport.js"; export type { CreateTransportOptions } from "./client.js"; export { - PROTOCOL_ERROR_ID, + PROTOCOL_ERROR_METHOD_ID, + PROTOCOL_ERROR_TRAIT_ID, SubscriptionError, UnsupportedMessageError, createIframeProvider, diff --git a/js/packages/truapi/src/sandbox.test.ts b/js/packages/truapi/src/sandbox.test.ts index 291505cd4..284ca5430 100644 --- a/js/packages/truapi/src/sandbox.test.ts +++ b/js/packages/truapi/src/sandbox.test.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; -import { encodeWireMessage, PROTOCOL_ERROR_ID } from "./transport.js"; +import { + encodeWireMessage, + PROTOCOL_ERROR_METHOD_ID, + PROTOCOL_ERROR_TRAIT_ID, +} from "./transport.js"; let importCounter = 0; @@ -221,7 +225,7 @@ describe("sandbox iframe MessagePort handshake", () => { const probe = encodeWireMessage({ requestId: "legacy-probe", - payload: { id: 254, value: new Uint8Array() }, + payload: { traitId: 254, methodId: 253, value: new Uint8Array() }, }); expect(probe.isOk()).toBe(true); if (probe.isErr()) throw probe.error; @@ -255,7 +259,7 @@ describe("sandbox iframe MessagePort handshake", () => { const probe = encodeWireMessage({ requestId: "legacy-probe", - payload: { id: 254, value: new Uint8Array() }, + payload: { traitId: 254, methodId: 253, value: new Uint8Array() }, }); expect(probe.isOk()).toBe(true); if (probe.isErr()) throw probe.error; @@ -269,8 +273,11 @@ describe("sandbox iframe MessagePort handshake", () => { const unsupported = encodeWireMessage({ requestId: "legacy-probe", payload: { - id: PROTOCOL_ERROR_ID, - value: new Uint8Array([0, 0, 254]), + traitId: PROTOCOL_ERROR_TRAIT_ID, + methodId: PROTOCOL_ERROR_METHOD_ID, + // [0] version index, [0] variant index, then the pair that was + // not understood, echoed in arrival order. + value: new Uint8Array([0, 0, 254, 253]), }, }); expect(unsupported.isOk()).toBe(true); diff --git a/js/packages/truapi/src/transport.ts b/js/packages/truapi/src/transport.ts index 292d3e8c2..b271f5518 100644 --- a/js/packages/truapi/src/transport.ts +++ b/js/packages/truapi/src/transport.ts @@ -3,18 +3,28 @@ import { err, ok, type Result, type ResultAsync } from "neverthrow"; import { str, u8, type CallErrorValue, type ResultPayload } from "./scale.js"; -/** Wire discriminant reserved for method-independent protocol errors. **/ -export const PROTOCOL_ERROR_ID = 255 as const; +/** + * Wire trait discriminant reserved for method-independent protocol errors. No + * API trait may declare it, so no method is ever addressed here. + **/ +export const PROTOCOL_ERROR_TRAIT_ID = 255 as const; + +/** Wire method discriminant reserved for method-independent protocol errors. **/ +export const PROTOCOL_ERROR_METHOD_ID = 255 as const; /** The peer rejected an outbound frame because it does not support its API. **/ export class UnsupportedMessageError extends Error { - /** Wire discriminant of the unsupported outbound frame. **/ - readonly discriminant: number; + /** Trait discriminant of the unsupported outbound frame. **/ + readonly traitId: number; - constructor(discriminant: number) { - super(`Peer does not support wire message ${discriminant}`); + /** Method discriminant of the unsupported outbound frame. **/ + readonly methodId: number; + + constructor(traitId: number, methodId: number) { + super(`Peer does not support wire message (${traitId}, ${methodId})`); this.name = "UnsupportedMessageError"; - this.discriminant = discriminant; + this.traitId = traitId; + this.methodId = methodId; } } @@ -143,12 +153,17 @@ export interface ObservableSource { **/ export interface RequestFrameIds { /** - * Wire discriminant for the outbound request frame. + * Wire trait discriminant carried by both frames. + **/ + trait: number; + + /** + * Wire method discriminant for the outbound request frame. **/ request: number; /** - * Wire discriminant for the inbound response frame. + * Wire method discriminant for the inbound response frame. **/ response: number; } @@ -158,22 +173,27 @@ export interface RequestFrameIds { **/ export interface SubscriptionFrameIds { /** - * Wire discriminant for the outbound start frame. + * Wire trait discriminant carried by all four frames. + **/ + trait: number; + + /** + * Wire method discriminant for the outbound start frame. **/ start: number; /** - * Wire discriminant for the outbound stop frame. + * Wire method discriminant for the outbound stop frame. **/ stop: number; /** - * Wire discriminant for the inbound interrupt frame. + * Wire method discriminant for the inbound interrupt frame. **/ interrupt: number; /** - * Wire discriminant for the inbound receive frame. + * Wire method discriminant for the inbound receive frame. **/ receive: number; } @@ -307,9 +327,14 @@ export interface TrUApiTransport { **/ export interface Payload { /** - * Wire-table numeric discriminant. + * Wire-table trait discriminant: first byte of the `(trait, method)` pair. **/ - id: number; + traitId: number; + + /** + * Wire-table method discriminant within the trait: second byte of the pair. + **/ + methodId: number; /** * SCALE-encoded payload body. @@ -379,12 +404,20 @@ export interface WebSocketWireProvider extends WireProvider { export function encodeWireMessage( message: ProtocolMessage, ): Result { - const id = message.payload.id; - if (!Number.isInteger(id) || id < 0 || id > 255) { - return err(new Error(`Invalid wire discriminant: ${id}`)); + const { traitId, methodId } = message.payload; + if (!Number.isInteger(traitId) || traitId < 0 || traitId > 255) { + return err(new Error(`Invalid wire trait discriminant: ${traitId}`)); + } + if (!Number.isInteger(methodId) || methodId < 0 || methodId > 255) { + return err(new Error(`Invalid wire method discriminant: ${methodId}`)); } return ok( - concatBytes(str.enc(message.requestId), u8.enc(id), message.payload.value), + concatBytes( + str.enc(message.requestId), + u8.enc(traitId), + u8.enc(methodId), + message.payload.value, + ), ); } @@ -406,15 +439,23 @@ export function decodeWireMessage( const requestId = str.dec(cursor.subarray(0, requestIdEnd)); cursor = cursor.subarray(requestIdEnd); if (cursor.length < 1) { - return err(new Error("Wire frame too short: missing discriminant byte")); + return err( + new Error("Wire frame too short: missing trait discriminant byte"), + ); + } + if (cursor.length < 2) { + return err( + new Error("Wire frame too short: missing method discriminant byte"), + ); } - const id = cursor[0]; - const value = cursor.subarray(1); + const traitId = cursor[0]; + const methodId = cursor[1]; + const value = cursor.subarray(2); // Hand the value bytes back as a fresh slice so callers may safely retain // it even if the source buffer is reused by the transport. const valueCopy = new Uint8Array(value.length); valueCopy.set(value); - return ok({ requestId, payload: { id, value: valueCopy } }); + return ok({ requestId, payload: { traitId, methodId, value: valueCopy } }); } /** diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts index af656c48f..5aa37a050 100644 --- a/js/packages/truapi/src/wire-equality.test.ts +++ b/js/packages/truapi/src/wire-equality.test.ts @@ -18,12 +18,13 @@ function toHex(u: Uint8Array): string { .join(""); } -function expectedWire(tagId: number, valueBytes: Uint8Array): Uint8Array { +function expectedWire(traitId: number, methodId: number, valueBytes: Uint8Array): Uint8Array { const reqId = str.enc("p:1"); - const out = new Uint8Array(reqId.length + 1 + valueBytes.length); + const out = new Uint8Array(reqId.length + 2 + valueBytes.length); out.set(reqId, 0); - out[reqId.length] = tagId; - out.set(valueBytes, reqId.length + 1); + out[reqId.length] = traitId; + out[reqId.length + 1] = methodId; + out.set(valueBytes, reqId.length + 2); return out; } @@ -38,19 +39,36 @@ function unwrap(result: Result, message: string): T { } describe("encodeWireMessage / decodeWireMessage wire equality", () => { - it("encodes handshake_request (discriminant 0) to match the Rust reference", () => { - const inner = new Uint8Array([0x00, 0x01]); // V1 variant + codec_version=1 + it("pins the handshake frame end-to-end: requestId + 0x00 0x00 + payload", () => { + // Trait 0 = system, method 0 = handshake request. This locks the + // system trait to discriminant zero: the handshake is the first frame + // either side sends, so its envelope must never drift. + expect(W.SYSTEM_HANDSHAKE.trait).toBe(0); + expect(W.SYSTEM_HANDSHAKE.request).toBe(0); + + const inner = new Uint8Array([0x00, 0x02]); // V1 variant + codec_version=2 const encoded = unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: W.SYSTEM_HANDSHAKE.request, value: inner }, + payload: { + traitId: W.SYSTEM_HANDSHAKE.trait, + methodId: W.SYSTEM_HANDSHAKE.request, + value: inner, + }, }), "encode handshake_request", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(0, inner))); + // [0c 70 3a 31] "p:1" + [00] system trait + [00] handshake request + payload. + expect(toHex(encoded)).toBe("0c703a3100000002"); + expect(toHex(encoded)).toBe(toHex(expectedWire(0, 0, inner))); + + const decoded = unwrap(decodeWireMessage(encoded), "decode handshake_request"); + expect(decoded.payload.traitId).toBe(0); + expect(decoded.payload.methodId).toBe(0); + expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); - it("encodes account_get_request (discriminant 22) to match the golden fixture", () => { + it("encodes account_get_request (pair (1, 4)) to match the golden fixture", () => { // payload = V1(("foo", 0u32)); same vector as the Rust golden fixture. const inner = new Uint8Array([ 0x00, // V1 variant @@ -63,12 +81,16 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { const encoded = unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: W.ACCOUNT_GET_ACCOUNT.request, value: inner }, + payload: { + traitId: W.ACCOUNT_GET_ACCOUNT.trait, + methodId: W.ACCOUNT_GET_ACCOUNT.request, + value: inner, + }, }), "encode account_get_request", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(22, inner))); - expect(toHex(encoded)).toBe("0c703a3116000c666f6f00000000"); + expect(toHex(encoded)).toBe(toHex(expectedWire(1, 4, inner))); + expect(toHex(encoded)).toBe("0c703a310104000c666f6f00000000"); }); it("round-trips a local_storage_read frame through encode + decode", () => { @@ -76,30 +98,54 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { const encoded = unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: W.LOCAL_STORAGE_READ.request, value: inner }, + payload: { + traitId: W.LOCAL_STORAGE_READ.trait, + methodId: W.LOCAL_STORAGE_READ.request, + value: inner, + }, }), "encode local_storage_read_request", ); const decoded = unwrap(decodeWireMessage(encoded), "decode local_storage_read_request"); expect(decoded.requestId).toBe("p:1"); - expect(decoded.payload.id).toBe(W.LOCAL_STORAGE_READ.request); + expect(decoded.payload.traitId).toBe(W.LOCAL_STORAGE_READ.trait); + expect(decoded.payload.methodId).toBe(W.LOCAL_STORAGE_READ.request); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); - it("rejects an invalid outbound discriminant", () => { + it("rejects an invalid outbound trait discriminant", () => { const result = encodeWireMessage({ requestId: "p:1", - payload: { id: 256, value: new Uint8Array() }, + payload: { traitId: 256, methodId: 0, value: new Uint8Array() }, }); expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr().message).toMatch(/Invalid wire discriminant/); + expect(result._unsafeUnwrapErr().message).toMatch(/Invalid wire trait discriminant/); }); - it("rejects a truncated frame with no discriminant byte", () => { + it("rejects an invalid outbound method discriminant", () => { + const result = encodeWireMessage({ + requestId: "p:1", + payload: { traitId: 0, methodId: 256, value: new Uint8Array() }, + }); + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr().message).toMatch(/Invalid wire method discriminant/); + }); + + it("rejects a truncated frame with no trait byte", () => { const truncated = str.enc("p:1"); // just the requestId, nothing after. const result = decodeWireMessage(truncated); expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr().message).toMatch(/missing discriminant byte/); + expect(result._unsafeUnwrapErr().message).toMatch(/missing trait discriminant byte/); + }); + + it("rejects a truncated frame with a trait byte but no method byte", () => { + const reqId = str.enc("p:1"); + const truncated = new Uint8Array(reqId.length + 1); + truncated.set(reqId, 0); + truncated[reqId.length] = 0x00; + const result = decodeWireMessage(truncated); + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr().message).toMatch(/missing method discriminant byte/); }); it("round-trips a 32 KiB requestId via the mode-2 compact-len prefix", () => { @@ -110,7 +156,11 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { const encoded = unwrap( encodeWireMessage({ requestId: longId, - payload: { id: W.ACCOUNT_GET_ACCOUNT.request, value: inner }, + payload: { + traitId: W.ACCOUNT_GET_ACCOUNT.trait, + methodId: W.ACCOUNT_GET_ACCOUNT.request, + value: inner, + }, }), "encode long-id account_get_request", ); @@ -119,7 +169,8 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { expect(encoded[0] & 0b11).toBe(0b10); const decoded = unwrap(decodeWireMessage(encoded), "decode long-id account_get_request"); expect(decoded.requestId).toBe(longId); - expect(decoded.payload.id).toBe(W.ACCOUNT_GET_ACCOUNT.request); + expect(decoded.payload.traitId).toBe(W.ACCOUNT_GET_ACCOUNT.trait); + expect(decoded.payload.methodId).toBe(W.ACCOUNT_GET_ACCOUNT.request); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); }); diff --git a/js/packages/truapi/src/wire-table.test.ts b/js/packages/truapi/src/wire-table.test.ts index 579d3bd9b..f1561c87c 100644 --- a/js/packages/truapi/src/wire-table.test.ts +++ b/js/packages/truapi/src/wire-table.test.ts @@ -1,8 +1,9 @@ // Programmatic wire-equality loop. // // `wire-equality.test.ts` exercises a handful of hand-picked frames. This file -// iterates every generated numeric frame id and asserts the codec round-trips a -// sentinel payload and produces the expected byte layout for each. +// iterates every generated (trait, method) frame pair and asserts the codec +// round-trips a sentinel payload and produces the expected byte layout for +// each. import type { Result } from "neverthrow"; import { describe, expect, it } from "bun:test"; @@ -17,12 +18,18 @@ function toHex(u: Uint8Array): string { .join(""); } -function expectedWire(reqId: string, tagId: number, valueBytes: Uint8Array): Uint8Array { +function expectedWire( + reqId: string, + traitId: number, + methodId: number, + valueBytes: Uint8Array, +): Uint8Array { const idBytes = str.enc(reqId); - const out = new Uint8Array(idBytes.length + 1 + valueBytes.length); + const out = new Uint8Array(idBytes.length + 2 + valueBytes.length); out.set(idBytes, 0); - out[idBytes.length] = tagId; - out.set(valueBytes, idBytes.length + 1); + out[idBytes.length] = traitId; + out[idBytes.length + 1] = methodId; + out.set(valueBytes, idBytes.length + 2); return out; } @@ -37,7 +44,10 @@ function unwrap(result: Result, message: string): T { } const frames = Object.entries(W as Record>).flatMap( - ([method, ids]) => Object.entries(ids).map(([kind, id]) => ({ method, kind, id })), + ([method, ids]) => { + const { trait: traitId, ...kinds } = ids; + return Object.entries(kinds).map(([kind, id]) => ({ method, kind, traitId, id })); + }, ); describe("generated wire-table round-trip", () => { @@ -45,21 +55,31 @@ describe("generated wire-table round-trip", () => { expect(frames.length).toBeGreaterThan(0); }); - // Per-id sentinel payload so any cross-talk between ids surfaces as a + it("gives every constant a trait discriminant", () => { + for (const [method, ids] of Object.entries(W as Record>)) { + expect(Number.isInteger(ids.trait), `${method} is missing a trait id`).toBe(true); + } + }); + + // Per-pair sentinel payload so any cross-talk between pairs surfaces as a // concrete byte mismatch rather than a silent equality. - it.each(frames)("round-trips $method.$kind (id $id)", ({ id }) => { - const sentinel = new Uint8Array([id, 0xa5, ~id & 0xff, 0x5a]); - const requestId = `r:${id}`; + it.each(frames)("round-trips $method.$kind (pair ($traitId, $id))", ({ traitId, id }) => { + const sentinel = new Uint8Array([traitId, id, 0xa5, ~id & 0xff, 0x5a]); + const requestId = `r:${traitId}:${id}`; const encoded = unwrap( - encodeWireMessage({ requestId, payload: { id, value: sentinel } }), + encodeWireMessage({ + requestId, + payload: { traitId, methodId: id, value: sentinel }, + }), "encode", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(requestId, id, sentinel))); + expect(toHex(encoded)).toBe(toHex(expectedWire(requestId, traitId, id, sentinel))); const decoded = unwrap(decodeWireMessage(encoded), "decode"); expect(decoded.requestId).toBe(requestId); - expect(decoded.payload.id).toBe(id); + expect(decoded.payload.traitId).toBe(traitId); + expect(decoded.payload.methodId).toBe(id); expect(toHex(decoded.payload.value)).toBe(toHex(sentinel)); }); }); diff --git a/rust/crates/truapi-codegen/src/main.rs b/rust/crates/truapi-codegen/src/main.rs index 66a803751..b3d846263 100644 --- a/rust/crates/truapi-codegen/src/main.rs +++ b/rust/crates/truapi-codegen/src/main.rs @@ -12,7 +12,11 @@ mod rust; mod rustdoc; mod ts; -const RESERVED_PROTOCOL_ERROR_ID: u8 = u8::MAX; +/// Trait discriminant reserved for method-independent protocol errors. Codec 2 +/// addresses frames by `(trait, method)`, so the reservation moved from a single +/// id to a whole trait: that is the only level at which it can be enforced, +/// since a method reaches the protocol-error address only through its trait. +const RESERVED_PROTOCOL_ERROR_TRAIT_ID: u8 = u8::MAX; #[derive(Parser)] #[command( @@ -41,7 +45,7 @@ struct Cli { client_version: Option, /// Wire codec version for generated handshake calls. - #[arg(long, default_value_t = 1)] + #[arg(long, default_value_t = 2)] codec_version: u8, /// Output directory for generated playground metadata (optional). diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index 8368a82f7..f34026614 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -270,6 +270,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), + wire_trait_id: Some(0), methods: vec![make_subscription_method("connection_status_subscribe", 18)], docs: None, }], @@ -301,12 +302,14 @@ mod tests { TraitDef { name: "StatementStore".to_string(), module_path: Vec::new(), + wire_trait_id: Some(1), methods: vec![make_request_method("submit", 62)], docs: None, }, TraitDef { name: "Preimage".to_string(), module_path: Vec::new(), + wire_trait_id: Some(2), methods: vec![make_request_method("submit", 68)], docs: None, }, @@ -353,12 +356,14 @@ mod tests { TraitDef { name: "Foo".to_string(), module_path: Vec::new(), + wire_trait_id: Some(3), methods: vec![make_request_method("bar_baz", 10)], docs: None, }, TraitDef { name: "FooBar".to_string(), module_path: Vec::new(), + wire_trait_id: Some(4), methods: vec![make_request_method("baz", 12)], docs: None, }, @@ -389,6 +394,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![make_request_method("request_device_permission", 8)], docs: None, }], @@ -415,6 +421,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![ make_request_method("alpha", 10), make_request_method("beta", 10), @@ -427,76 +434,152 @@ mod tests { let err = generate_wire_table(&api).expect_err("duplicate ids must error"); let msg = format!("{err}"); assert!( - msg.contains("wire id 10 reused"), + msg.contains("wire id (5, 10) reused"), "unexpected error message: {msg}", ); } - /// Discriminant 255 is reserved for protocol-level errors, so no API - /// method may claim it for any request, response, or subscription frame. + /// Method ids are scoped per trait: two traits may both use method id 0, + /// and the emitted consts carry each trait's discriminant. #[test] - fn wire_table_reserves_protocol_error_id() { - let mut explicit_request = make_request_method("explicit_request", 255); - explicit_request.wire.response_id = Some(1); - - let inferred_response = make_request_method("inferred_response", 254); - - let mut explicit_response = make_request_method("explicit_response", 1); - explicit_response.wire.response_id = Some(255); - - let mut explicit_start = make_subscription_method("explicit_start", 255); - explicit_start.wire.stop_id = Some(1); - explicit_start.wire.interrupt_id = Some(2); - explicit_start.wire.receive_id = Some(3); - - let mut explicit_stop = make_subscription_method("explicit_stop", 1); - explicit_stop.wire.stop_id = Some(255); - explicit_stop.wire.interrupt_id = Some(2); - explicit_stop.wire.receive_id = Some(3); - - let mut explicit_interrupt = make_subscription_method("explicit_interrupt", 1); - explicit_interrupt.wire.stop_id = Some(2); - explicit_interrupt.wire.interrupt_id = Some(255); - explicit_interrupt.wire.receive_id = Some(3); - - let mut explicit_receive = make_subscription_method("explicit_receive", 1); - explicit_receive.wire.stop_id = Some(2); - explicit_receive.wire.interrupt_id = Some(3); - explicit_receive.wire.receive_id = Some(255); - - let inferred_receive = make_subscription_method("inferred_receive", 252); - - for method in [ - explicit_request, - inferred_response, - explicit_response, - explicit_start, - explicit_stop, - explicit_interrupt, - explicit_receive, - inferred_receive, - ] { - let method_name = method.name.clone(); - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Example".to_string(), + fn wire_table_allows_same_method_id_in_different_traits() { + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "StatementStore".to_string(), module_path: Vec::new(), - methods: vec![method], + wire_trait_id: Some(13), + methods: vec![make_request_method("submit", 0)], docs: None, - }], - public_trait_order: vec!["Example".to_string()], - types: Vec::new(), - }; + }, + TraitDef { + name: "Preimage".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(10), + methods: vec![make_request_method("submit", 0)], + docs: None, + }, + ], + public_trait_order: vec!["StatementStore".to_string(), "Preimage".to_string()], + types: vec![], + }; - let error = generate_wire_table(&api) - .expect_err(&format!("{method_name} must not allocate wire id 255")); - let message = error.to_string(); - assert!( - message.contains("wire id 255 reused") - && message.contains("reserved for protocol errors"), - "unexpected error for {method_name}: {message}", - ); - } + let table = generate_wire_table(&api).expect("wire_table"); + assert!( + table.contains("trait_id: 13,"), + "missing trait id 13:\n{table}" + ); + assert!( + table.contains("trait_id: 10,"), + "missing trait id 10:\n{table}" + ); + } + + /// Two traits must not share a wire trait id. + #[test] + fn wire_table_rejects_duplicate_trait_ids() { + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "StatementStore".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(4), + methods: vec![make_request_method("submit", 0)], + docs: None, + }, + TraitDef { + name: "Preimage".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(4), + methods: vec![make_request_method("submit", 0)], + docs: None, + }, + ], + public_trait_order: vec!["StatementStore".to_string(), "Preimage".to_string()], + types: vec![], + }; + + let err = generate_wire_table(&api).expect_err("duplicate trait ids must error"); + let msg = format!("{err}"); + assert!( + msg.contains("wire trait id 4 reused"), + "unexpected error message: {msg}", + ); + } + + /// A trait missing `#[wire_trait(id = N)]` must fail emission. + #[test] + fn wire_table_missing_trait_id_errors() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + wire_trait_id: None, + methods: vec![make_request_method("request_device_permission", 8)], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + + let err = generate_wire_table(&api).expect_err("missing trait id must error"); + let msg = format!("{err}"); + assert!( + msg.contains("missing #[wire_trait(id = N)]"), + "unexpected error message: {msg}", + ); + } + + /// Trait 255 is reserved for protocol errors, so no API trait may declare + /// it. Codec 2 addresses a frame by `(trait, method)`, which moves the + /// reservation from a single id to a whole trait: a method id of 255 is now + /// a perfectly ordinary address, and the only way to reach the reserved + /// `(255, 255)` is through a trait that owns 255. This replaces main's + /// method-level test, which asserted the eight method-id positions could + /// not be 255 - true under one byte, wrong under two. + #[test] + fn wire_table_rejects_the_reserved_protocol_error_trait_id() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Example".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(crate::RESERVED_PROTOCOL_ERROR_TRAIT_ID), + methods: vec![make_request_method("submit", 0)], + docs: None, + }], + public_trait_order: vec!["Example".to_string()], + types: vec![], + }; + + let err = generate_wire_table(&api).expect_err("trait id 255 must be refused"); + let msg = err.to_string(); + assert!( + msg.contains("wire trait id 255 reused") + && msg.contains("reserved for protocol errors"), + "unexpected error message: {msg}", + ); + } + + /// The other half of the reservation: it must not have grown. A method id of + /// 255 inside an ordinary trait is a legal address under a two-byte + /// envelope, and refusing it would silently cost every trait its last slot. + #[test] + fn wire_table_allows_method_id_255_outside_the_reserved_trait() { + let mut method = make_request_method("explicit_request", 255); + method.wire.response_id = Some(1); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Example".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(7), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Example".to_string()], + types: vec![], + }; + + generate_wire_table(&api).expect("(7, 255) is an ordinary address"); } /// Pin `wire_const_name`'s `convert_case::Case::UpperSnake` behavior: @@ -540,6 +623,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![method], docs: None, }], @@ -563,6 +647,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), + wire_trait_id: Some(0), methods: vec![method], docs: None, }], @@ -587,6 +672,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![method], docs: None, }], @@ -610,6 +696,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), + wire_trait_id: Some(0), methods: vec![method], docs: None, }], @@ -641,6 +728,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![method], docs: None, }], @@ -674,6 +762,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![method], docs: None, }], @@ -708,6 +797,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![method], docs: None, }], @@ -737,6 +827,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), + wire_trait_id: Some(5), methods: vec![method], docs: None, }], @@ -776,6 +867,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), + wire_trait_id: Some(0), methods: vec![method], docs: None, }], diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs index da47e24d4..5e830ac39 100644 --- a/rust/crates/truapi-codegen/src/rust/wire_table.rs +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -1,12 +1,14 @@ -//! Emits `wire_table.rs`: the (id, tag) lookup table the server uses to -//! pair incoming wire frames with their request, response, or +//! Emits `wire_table.rs`: the (trait, method) discriminant lookup table the +//! server uses to pair incoming wire frames with their request, response, or //! subscription role. //! -//! Per-method `#[wire(...)]` annotations decide id assignment: +//! A trait-level `#[wire_trait(id = N)]` annotation assigns the trait +//! discriminant; per-method `#[wire(...)]` annotations decide method-id +//! assignment within the trait: //! - request methods reserve `(request_id, response_id)`. //! - subscription methods reserve `(start_id, stop_id, interrupt_id, receive_id)`. //! -//! Missing annotations and collisions both hard-fail codegen. +//! Missing annotations and collisions (per trait) both hard-fail codegen. use std::collections::BTreeMap; use std::fmt::Write; @@ -17,16 +19,18 @@ use indoc::{formatdoc, writedoc}; use crate::rustdoc::*; use super::{const_name, wire_method_name}; -use crate::RESERVED_PROTOCOL_ERROR_ID; +use crate::RESERVED_PROTOCOL_ERROR_TRAIT_ID; #[derive(Debug, Clone, Copy)] struct WireEntry { + trait_id: u8, request_id: u8, response_id: u8, } #[derive(Debug, Clone, Copy)] struct SubEntry { + trait_id: u8, start_id: u8, stop_id: u8, interrupt_id: u8, @@ -42,15 +46,33 @@ enum MethodEntry { /// Emit the contents of `wire_table.rs`. pub fn generate_wire_table(api: &ApiDefinition) -> Result { let mut method_entries: Vec<(String, MethodEntry)> = Vec::new(); - let mut seen = BTreeMap::from([( - RESERVED_PROTOCOL_ERROR_ID, + let mut seen: BTreeMap<(u8, u8), String> = BTreeMap::new(); + // Seed the reserved trait as already taken, so a trait declaring 255 + // collides here instead of silently claiming the address protocol errors + // travel on. Reserving the trait rather than the single pair (255, 255) is + // what makes this reachable: a method can only land on that pair through a + // trait that owns 255, and nothing else constrains a declared trait id. + let mut seen_traits: BTreeMap = BTreeMap::from([( + RESERVED_PROTOCOL_ERROR_TRAIT_ID, "reserved for protocol errors".to_string(), )]); let mut seen_methods: BTreeMap = BTreeMap::new(); for trait_def in &api.traits { + // Method-less traits (e.g. the `TrUApi` umbrella trait) own no wire + // frames and need no trait discriminant. + if trait_def.methods.is_empty() { + continue; + } + let trait_id = trait_wire_id(trait_def)?; + if let Some(existing) = seen_traits.insert(trait_id, trait_def.name.clone()) { + bail!( + "wire trait id {trait_id} reused: `{existing}` and `{}` collide", + trait_def.name + ); + } for method in &trait_def.methods { - let entry = method_entry(trait_def, method)?; + let entry = method_entry(trait_def, trait_id, method)?; let wire_method = wire_method_name(&trait_def.name, &method.name); if let Some(existing) = seen_methods.insert( wire_method.clone(), @@ -68,14 +90,31 @@ pub fn generate_wire_table(api: &ApiDefinition) -> Result { } method_entries.sort_by_key(|(_, entry)| match entry { - MethodEntry::Request(WireEntry { request_id, .. }) => *request_id, - MethodEntry::Subscription(SubEntry { start_id, .. }) => *start_id, + MethodEntry::Request(WireEntry { + trait_id, + request_id, + .. + }) => (*trait_id, *request_id), + MethodEntry::Subscription(SubEntry { + trait_id, start_id, .. + }) => (*trait_id, *start_id), }); render(&method_entries) } -fn method_entry(trait_def: &TraitDef, method: &MethodDef) -> Result { +/// The trait's wire discriminant. Every API trait must carry a +/// `#[wire_trait(id = N)]` annotation. +fn trait_wire_id(trait_def: &TraitDef) -> Result { + trait_def.wire_trait_id.ok_or_else(|| { + anyhow::anyhow!( + "trait `{}` is missing #[wire_trait(id = N)] annotation", + trait_def.name + ) + }) +} + +fn method_entry(trait_def: &TraitDef, trait_id: u8, method: &MethodDef) -> Result { let wire = &method.wire; match method.kind { MethodKind::Request => { @@ -99,6 +138,7 @@ fn method_entry(trait_def: &TraitDef, method: &MethodDef) -> Result })?; let response_id = infer_id(wire.response_id, request_id, 1, &method.name)?; Ok(MethodEntry::Request(WireEntry { + trait_id, request_id, response_id, })) @@ -122,6 +162,7 @@ fn method_entry(trait_def: &TraitDef, method: &MethodDef) -> Result let interrupt_id = infer_id(wire.interrupt_id, start_id, 2, &method.name)?; let receive_id = infer_id(wire.receive_id, start_id, 3, &method.name)?; Ok(MethodEntry::Subscription(SubEntry { + trait_id, start_id, stop_id, interrupt_id, @@ -141,33 +182,35 @@ fn infer_id(explicit: Option, anchor: u8, offset: u8, method_name: &str) -> } fn insert_entry( - seen: &mut BTreeMap, + seen: &mut BTreeMap<(u8, u8), String>, method_name: &str, entry: MethodEntry, ) -> Result<()> { - let pairs: Vec<(u8, String)> = match entry { + let pairs: Vec<(u8, u8, String)> = match entry { MethodEntry::Request(WireEntry { + trait_id, request_id, response_id, }) => vec![ - (request_id, format!("{method_name}_request")), - (response_id, format!("{method_name}_response")), + (trait_id, request_id, format!("{method_name}_request")), + (trait_id, response_id, format!("{method_name}_response")), ], MethodEntry::Subscription(SubEntry { + trait_id, start_id, stop_id, interrupt_id, receive_id, }) => vec![ - (start_id, format!("{method_name}_start")), - (stop_id, format!("{method_name}_stop")), - (interrupt_id, format!("{method_name}_interrupt")), - (receive_id, format!("{method_name}_receive")), + (trait_id, start_id, format!("{method_name}_start")), + (trait_id, stop_id, format!("{method_name}_stop")), + (trait_id, interrupt_id, format!("{method_name}_interrupt")), + (trait_id, receive_id, format!("{method_name}_receive")), ], }; - for (id, tag) in pairs { - if let Some(existing) = seen.insert(id, tag.clone()) { - bail!("wire id {id} reused: `{existing}` and `{tag}` collide"); + for (trait_id, id, tag) in pairs { + if let Some(existing) = seen.insert((trait_id, id), tag.clone()) { + bail!("wire id ({trait_id}, {id}) reused: `{existing}` and `{tag}` collide"); } } Ok(()) @@ -182,31 +225,37 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { //! //! Auto-generated by truapi-codegen. Do not edit. //! - //! Each method reserves either two ids (request/response) or four - //! (start/stop/interrupt/receive). The ids for each method are exposed - //! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the - //! generated dispatcher both reference those consts so the numbers live - //! in exactly one place. The table is sorted by request/start id. + //! Every frame carries a `(trait, method)` discriminant pair. Each + //! method reserves either two method ids (request/response) or four + //! (start/stop/interrupt/receive) within its trait. The ids for each + //! method are exposed as a named const (`PREIMAGE_SUBMIT`, ...); + //! [`WIRE_TABLE`] and the generated dispatcher both reference those + //! consts so the numbers live in exactly one place. The table is + //! sorted by (trait id, request/start id). /// Request method wire discriminants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RequestFrameIds {{ - /// Discriminant for the request frame. + /// Trait discriminant carried by both frames. + pub trait_id: u8, + /// Method discriminant for the request frame. pub request_id: u8, - /// Discriminant for the response frame. + /// Method discriminant for the response frame. pub response_id: u8, }} /// Subscription method wire discriminants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SubscriptionFrameIds {{ - /// Discriminant for the start frame. + /// Trait discriminant carried by all four frames. + pub trait_id: u8, + /// Method discriminant for the start frame. pub start_id: u8, - /// Discriminant for the stop frame. + /// Method discriminant for the stop frame. pub stop_id: u8, - /// Discriminant for the interrupt frame (server-initiated termination). + /// Method discriminant for the interrupt frame (server-initiated termination). pub interrupt_id: u8, - /// Discriminant for each receive frame (a streamed item). + /// Method discriminant for each receive frame (a streamed item). pub receive_id: u8, }} @@ -234,18 +283,21 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { let konst = const_name(name); let block = match entry { MethodEntry::Request(WireEntry { + trait_id, request_id, response_id, }) => formatdoc! { r#" /// Wire discriminants for `{name}`. pub const {konst}: RequestFrameIds = RequestFrameIds {{ + trait_id: {trait_id}, request_id: {request_id}, response_id: {response_id}, }}; "# }, MethodEntry::Subscription(SubEntry { + trait_id, start_id, stop_id, interrupt_id, @@ -254,6 +306,7 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { r#" /// Wire discriminants for `{name}`. pub const {konst}: SubscriptionFrameIds = SubscriptionFrameIds {{ + trait_id: {trait_id}, start_id: {start_id}, stop_id: {stop_id}, interrupt_id: {interrupt_id}, @@ -270,8 +323,9 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { writedoc!( out, r#" - /// The full wire table. Ordering is part of the wire protocol; - /// only ever append. Removed methods leave their slot empty. + /// The full wire table. Trait ids and per-trait method ordering are + /// part of the wire protocol; only ever append within a trait. + /// Removed methods leave their slot empty. pub const WIRE_TABLE: &[WireEntry] = &[ "# ) diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index f84eaff20..59efcbe8b 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -68,6 +68,9 @@ pub struct TraitDef { /// Module path leading to the trait, excluding the trait name itself /// (e.g. `["truapi", "api", "account"]`). pub module_path: Vec, + /// Wire-protocol trait discriminant from the `#[wire_trait(id = N)]` + /// attribute: the first byte of the `(trait, method)` pair on the wire. + pub wire_trait_id: Option, /// Methods declared on the trait, in declaration order. pub methods: Vec, /// Rustdoc comment on the trait. Service markers are retained for codegen. @@ -644,6 +647,7 @@ fn extract_trait( Ok(TraitDef { name, module_path, + wire_trait_id: item.docs.as_deref().and_then(extract_wire_trait_id), methods, docs: item.docs.clone(), }) @@ -809,6 +813,26 @@ fn extract_marker_value<'a>(docs: &'a str, marker: &str) -> Option<&'a str> { }) } +/// Extracts the `@wire_trait_id=N` marker from a trait's doc comment block. +/// Annotated traits carry the marker via the `#[wire_trait(id = N)]` +/// proc-macro, which appends a hidden doc string so it propagates through +/// rustdoc JSON. +fn extract_wire_trait_id(docs: &str) -> Option { + for line in docs.lines() { + let line = line.trim_start(); + let Some(value) = line.strip_prefix("@wire_trait_id=") else { + continue; + }; + let end = value + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(value.len()); + if let Ok(id) = value[..end].parse::() { + return Some(id); + } + } + None +} + /// Extracts `@wire__id=N` markers from a doc comment block. Annotated /// methods carry these markers via the `#[wire(...)]` proc-macro, which appends /// hidden doc strings so they propagate through rustdoc JSON. @@ -1484,7 +1508,8 @@ mod tests { #[test] fn clean_docs_strips_wire_markers() { - let docs = "Trait summary.\n\n@wire_request_id=7\n@service_required_execution=Chat\n"; + let docs = "Trait summary.\n\n@wire_request_id=7\n@wire_trait_id=3\n\ + @service_required_execution=Chat\n"; assert_eq!(clean_docs(Some(docs)).as_deref(), Some("Trait summary.")); } @@ -1494,6 +1519,7 @@ mod tests { let trait_def = TraitDef { name: "Chat".into(), module_path: Vec::new(), + wire_trait_id: None, methods: Vec::new(), docs: Some("Chat operations.\n\n@service_required_execution=Chat".into()), }; @@ -1502,6 +1528,17 @@ mod tests { assert_eq!(trait_def.public_docs().as_deref(), Some("Chat operations.")); } + #[test] + fn extract_wire_trait_id_reads_marker() { + assert_eq!( + extract_wire_trait_id("Trait summary.\n\n@wire_trait_id=14\n"), + Some(14) + ); + assert_eq!(extract_wire_trait_id("Trait summary."), None); + // Out-of-range values are ignored rather than truncated. + assert_eq!(extract_wire_trait_id("@wire_trait_id=300"), None); + } + #[test] fn parse_accepts_tested_format_version() { let json = format!(r#"{{ "format_version": {MIN_FORMAT_VERSION}, "index": {{}} }}"#); diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index 41d542e7f..33820fd62 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -9,7 +9,7 @@ use anyhow::{Result, bail}; use convert_case::{Case, Casing}; use indoc::{formatdoc, writedoc}; -use crate::RESERVED_PROTOCOL_ERROR_ID; +use crate::RESERVED_PROTOCOL_ERROR_TRAIT_ID; use crate::rustdoc::*; mod examples; @@ -584,28 +584,48 @@ fn method_wire_sort_id(method: &MethodDef) -> u8 { fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result { let wrappers = collect_versioned_wrappers(api); - let mut seen = BTreeMap::from([( - RESERVED_PROTOCOL_ERROR_ID, + let mut seen: BTreeMap<(u8, u8), String> = BTreeMap::new(); + // Mirrors the Rust emitter's seeding in `rust/wire_table.rs`: trait 255 is + // reserved for protocol errors and must be refused identically by both, or + // the two languages would disagree about which addresses are legal. + let mut seen_traits: BTreeMap = BTreeMap::from([( + RESERVED_PROTOCOL_ERROR_TRAIT_ID, "reserved for protocol errors".to_string(), )]); - let mut constants: Vec<(String, ExpandedWireIds)> = Vec::new(); + let mut constants: Vec<(String, u8, ExpandedWireIds)> = Vec::new(); for trait_def in &api.traits { + // Method-less traits (e.g. the `TrUApi` umbrella trait) own no wire + // frames and need no trait discriminant. + if trait_def.methods.is_empty() { + continue; + } + let trait_id = trait_wire_id(trait_def)?; + if let Some(existing) = seen_traits.insert(trait_id, trait_def.name.clone()) { + bail!( + "wire trait id {trait_id} reused: `{existing}` and `{}` collide", + trait_def.name + ); + } for method in &trait_def.methods { let wire_ids = wire_ids_for_method(trait_def, method)?; for (id, tag) in wire_ids.entries(&method.name) { - if let Some(existing) = seen.insert(id, tag.clone()) { - bail!("wire id {id} reused: `{existing}` and `{tag}` collide"); + if let Some(existing) = seen.insert((trait_id, id), tag.clone()) { + bail!("wire id ({trait_id}, {id}) reused: `{existing}` and `{tag}` collide"); } } if !method_is_included(trait_def, method, &wrappers, target_version)? { continue; } - constants.push((wire_const_name(&trait_def.name, &method.name), wire_ids)); + constants.push(( + wire_const_name(&trait_def.name, &method.name), + trait_id, + wire_ids, + )); } } - constants.sort_by_key(|(_, ids)| ids.sort_id()); + constants.sort_by_key(|(_, trait_id, ids)| (*trait_id, ids.sort_id())); let mut out = String::new(); writedoc!( @@ -615,12 +635,13 @@ fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result Result Result Result Result { + trait_def.wire_trait_id.ok_or_else(|| { + anyhow::anyhow!( + "trait `{}` is missing #[wire_trait(id = N)] annotation", + trait_def.name + ) + }) +} + fn method_is_included( trait_def: &TraitDef, method: &MethodDef, @@ -2537,12 +2571,14 @@ mod tests { let json_rpc = TraitDef { name: "JsonRpc".to_string(), module_path: Vec::new(), + wire_trait_id: Some(6), methods: Vec::new(), docs: None, }; let system = TraitDef { name: "System".to_string(), module_path: Vec::new(), + wire_trait_id: Some(7), methods: Vec::new(), docs: None, }; @@ -2581,6 +2617,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(8), methods, docs: None, }], @@ -2809,6 +2846,7 @@ mod tests { .expect("generate wire table"); assert!(source.contains("export const EXAMPLE_STREAM = {")); + assert!(source.contains(" trait: 8,")); assert!(source.contains(" start: 2,")); assert!(source.contains(" receive: 5,")); assert!(source.contains("export const EXAMPLE_LATER = {")); @@ -2834,70 +2872,49 @@ mod tests { ) .expect_err("duplicate ids must error"); - assert!(err.to_string().contains("wire id 3 reused")); + assert!(err.to_string().contains("wire id (8, 3) reused")); } - /// Discriminant 255 is reserved for protocol-level errors, so no API - /// method may claim it for any request, response, or subscription frame. + /// Trait 255 is reserved for protocol errors, so no API trait may declare + /// it. Kept byte-for-byte in step with the Rust emitter's + /// `wire_table_rejects_the_reserved_protocol_error_trait_id`: if the two + /// languages disagreed about which addresses are legal, one of them would + /// emit a table the other rejects. #[test] - fn generate_wire_table_reserves_protocol_error_id() { - let mut explicit_request = request_method("explicit_request", Some(255)); - explicit_request.wire.response_id = Some(1); - - let inferred_response = request_method("inferred_response", Some(254)); - - let mut explicit_response = request_method("explicit_response", Some(1)); - explicit_response.wire.response_id = Some(255); - - let mut explicit_start = subscription_method("explicit_start", Some(255)); - explicit_start.wire.stop_id = Some(1); - explicit_start.wire.interrupt_id = Some(2); - explicit_start.wire.receive_id = Some(3); - - let mut explicit_stop = subscription_method("explicit_stop", Some(1)); - explicit_stop.wire.stop_id = Some(255); - explicit_stop.wire.interrupt_id = Some(2); - explicit_stop.wire.receive_id = Some(3); - - let mut explicit_interrupt = subscription_method("explicit_interrupt", Some(1)); - explicit_interrupt.wire.stop_id = Some(2); - explicit_interrupt.wire.interrupt_id = Some(255); - explicit_interrupt.wire.receive_id = Some(3); - - let mut explicit_receive = subscription_method("explicit_receive", Some(1)); - explicit_receive.wire.stop_id = Some(2); - explicit_receive.wire.interrupt_id = Some(3); - explicit_receive.wire.receive_id = Some(255); - - let inferred_receive = subscription_method("inferred_receive", Some(252)); - - for method in [ - explicit_request, - inferred_response, - explicit_response, - explicit_start, - explicit_stop, - explicit_interrupt, - explicit_receive, - inferred_receive, - ] { - let method_name = method.name.clone(); - let error = generate_wire_table(&api(vec![method]), 2) - .expect_err(&format!("{method_name} must not allocate wire id 255")); - let message = error.to_string(); - assert!( - message.contains("wire id 255 reused") - && message.contains("reserved for protocol errors"), - "unexpected error for {method_name}: {message}", - ); - } + fn generate_wire_table_rejects_the_reserved_protocol_error_trait_id() { + let mut api = api(vec![request_method("submit", Some(0))]); + api.traits[0].wire_trait_id = Some(RESERVED_PROTOCOL_ERROR_TRAIT_ID); + + let error = + generate_wire_table(&api, 2).expect_err("trait id 255 must be refused"); + let message = error.to_string(); + assert!( + message.contains("wire trait id 255 reused") + && message.contains("reserved for protocol errors"), + "unexpected error: {message}", + ); + } + + /// The reservation must not have grown while moving up a level: under a + /// two-byte envelope `(8, 255)` is an ordinary address, and refusing it + /// would quietly cost every trait its last method slot. + #[test] + fn generate_wire_table_allows_method_id_255_outside_the_reserved_trait() { + let mut method = request_method("explicit_request", Some(255)); + method.wire.response_id = Some(1); + + generate_wire_table(&api(vec![method]), 2).expect("(8, 255) is an ordinary address"); } + /// Version filtering must not become an escape hatch: a trait that declares + /// the reserved id is refused even when every one of its methods is excluded + /// from the target version. The trait id is validated before any method is + /// considered, which is what makes that hold. #[test] - fn generate_wire_table_reserves_protocol_error_id_for_filtered_method() { + fn generate_wire_table_rejects_the_reserved_trait_id_for_filtered_methods() { let mut future = request_method_with_wrappers( "future", - Some(RESERVED_PROTOCOL_ERROR_ID), + Some(0), "FutureRequest", "FutureResponse", "FutureError", @@ -2907,6 +2924,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(RESERVED_PROTOCOL_ERROR_TRAIT_ID), methods: vec![future], docs: None, }], @@ -2919,10 +2937,11 @@ mod tests { }; let error = generate_wire_table(&api, 1) - .expect_err("filtered methods must not allocate wire id 255"); + .expect_err("a filtered-out method must not unlock the reserved trait id"); assert!( - error.to_string().contains("wire id 255 reused") - && error.to_string().contains("reserved for protocol errors") + error.to_string().contains("wire trait id 255 reused") + && error.to_string().contains("reserved for protocol errors"), + "unexpected error: {error}", ); } @@ -2981,12 +3000,92 @@ mod tests { assert!(err.to_string().contains("wire id overflow")); } + /// Method ids are scoped per trait: two traits may both use method id 0. + #[test] + fn generate_wire_table_allows_same_method_id_in_different_traits() { + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "Alpha".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(0), + methods: vec![request_method("first", Some(0))], + docs: None, + }, + TraitDef { + name: "Beta".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(1), + methods: vec![request_method("second", Some(0))], + docs: None, + }, + ], + public_trait_order: Vec::new(), + types: Vec::new(), + }; + + let source = generate_wire_table(&api, 2).expect("generate wire table"); + + assert!(source.contains("export const ALPHA_FIRST = {")); + assert!(source.contains("export const BETA_SECOND = {")); + assert!(source.contains(" trait: 0,")); + assert!(source.contains(" trait: 1,")); + } + + /// Two traits must not share a wire trait id. + #[test] + fn generate_wire_table_rejects_duplicate_trait_ids() { + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "Alpha".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(3), + methods: vec![request_method("first", Some(0))], + docs: None, + }, + TraitDef { + name: "Beta".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(3), + methods: vec![request_method("second", Some(0))], + docs: None, + }, + ], + public_trait_order: Vec::new(), + types: Vec::new(), + }; + + let err = generate_wire_table(&api, 2).expect_err("duplicate trait ids must error"); + assert!(err.to_string().contains("wire trait id 3 reused")); + } + + /// A trait without `#[wire_trait(id = N)]` must fail emission. + #[test] + fn generate_wire_table_rejects_missing_trait_id() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Alpha".to_string(), + module_path: Vec::new(), + wire_trait_id: None, + methods: vec![request_method("first", Some(0))], + docs: None, + }], + public_trait_order: Vec::new(), + types: Vec::new(), + }; + + let err = generate_wire_table(&api, 2).expect_err("missing trait id must error"); + assert!(err.to_string().contains("missing #[wire_trait(id = N)]")); + } + #[test] fn generate_wire_table_filters_methods_by_target_version() { let api = ApiDefinition { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(8), methods: vec![ request_method_with_wrappers( "legacy", @@ -3033,6 +3132,7 @@ mod tests { TraitDef { name: "Legacy".to_string(), module_path: Vec::new(), + wire_trait_id: Some(9), methods: vec![request_method_with_wrappers( "legacy_call", Some(2), @@ -3045,6 +3145,7 @@ mod tests { TraitDef { name: "FutureOnly".to_string(), module_path: Vec::new(), + wire_trait_id: Some(10), methods: vec![request_method_with_wrappers( "future_call", Some(4), @@ -3082,6 +3183,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(8), methods: vec![MethodDef { name: "example_call".to_string(), kind: MethodKind::Request, @@ -3131,6 +3233,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(8), methods: vec![ MethodDef { name: "legacy_call".to_string(), @@ -3198,6 +3301,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(8), methods: vec![MethodDef { name: "example_call".to_string(), kind: MethodKind::Request, @@ -3244,6 +3348,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), + wire_trait_id: Some(8), methods: vec![MethodDef { name: "example_call".to_string(), kind: MethodKind::Request, diff --git a/rust/crates/truapi-macros/src/lib.rs b/rust/crates/truapi-macros/src/lib.rs index 8e27efc4d..fc5a591e9 100644 --- a/rust/crates/truapi-macros/src/lib.rs +++ b/rust/crates/truapi-macros/src/lib.rs @@ -4,10 +4,12 @@ //! envelopes: the `Vn` enums (with SCALE codec indices) plus their //! `Versioned`/`IntoLatest`/`FromLatest` impls from `truapi::versioned`. //! -//! The `wire` attribute marks a trait method with -//! its wire-protocol discriminant ids. The ids appear on the wire as the u8 discriminant in the -//! `Struct { request_id: str, payload: Enum() }` envelope; method -//! ordering becomes part of the wire protocol. +//! The `wire` attribute marks a trait method with its wire-protocol +//! discriminant ids, and the `wire_trait` attribute marks an API trait with +//! its trait discriminant. Together they form the two-byte +//! `(trait, method)` discriminant pair in the +//! `Struct { request_id: str, payload: (trait, method, bytes) }` envelope; +//! trait and method ordering become part of the wire protocol. //! //! At compile time the macro validates that every id literal is a `u8`. It emits //! a hidden doc line so the value survives into rustdoc JSON, where @@ -176,6 +178,59 @@ pub fn wire(args: TokenStream, item: TokenStream) -> TokenStream { .into() } +/// Arguments to `#[wire_trait(id = N)]`. +struct WireTraitArgs { + id: u8, +} + +impl Parse for WireTraitArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let key: Ident = input.parse()?; + if key != "id" { + return Err(syn::Error::new(key.span(), "expected `id = N`")); + } + input.parse::()?; + let lit: LitInt = input.parse()?; + let id = lit.base10_parse().map_err(|err| { + syn::Error::new(lit.span(), format!("wire trait id must fit in a u8: {err}")) + })?; + if !input.is_empty() { + return Err(input.error("expected a single `id = N` argument")); + } + Ok(Self { id }) + } +} + +/// Mark a TrUAPI service trait with its wire-protocol trait discriminant. +/// +/// ```ignore +/// #[wire_trait(id = 0)] +/// pub trait System: Send + Sync { ... } +/// ``` +/// +/// The trait id is the first byte of the `(trait, method)` discriminant pair +/// every frame of the trait's methods carries on the wire. Expands to the +/// original trait plus a hidden `@wire_trait_id=N` doc tag that +/// `truapi-codegen` extracts from rustdoc JSON. +#[proc_macro_attribute] +pub fn wire_trait(args: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as WireTraitArgs); + let tag = format!("@wire_trait_id={}", args.id); + + match syn::parse::(item) { + Ok(mut item_trait) => { + item_trait.attrs.push(syn::parse_quote!(#[doc = #tag])); + quote!(#item_trait).into() + } + Err(_) => syn::Error::new( + proc_macro2::Span::call_site(), + "#[wire_trait] can only be applied to traits", + ) + .to_compile_error() + .into(), + } +} + fn wire_tags(args: &WireArgs) -> Vec { let mut tags = [ ("request_id", args.request_id), diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index 7c2e1c3df..5f8eeeabe 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -195,7 +195,8 @@ mod tests { let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; @@ -204,7 +205,8 @@ mod tests { .expect("dispatcher should emit a response"); let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); assert_eq!(response.request_id, "p:1"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // Wire payload is `Result`-shaped: // [Ok disc=0x00][V1 variant 0x00][supported=1] assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); @@ -219,7 +221,8 @@ mod tests { let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request_bytes, }, }; @@ -228,7 +231,8 @@ mod tests { .expect("dispatcher should emit a response"); let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); assert_eq!(response.request_id, "p:1"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); response.payload.value } @@ -345,7 +349,8 @@ mod tests { let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: sub_ids.start_id, + trait_id: sub_ids.trait_id, + method_id: sub_ids.start_id, value: Vec::new(), }, }; @@ -366,7 +371,8 @@ mod tests { let sent = transport.sent.lock().unwrap().clone(); assert!(!sent.is_empty(), "expected at least one _receive frame"); let first = &sent[0]; - assert_eq!(first.payload.id, sub_ids.receive_id); + assert_eq!(first.payload.trait_id, sub_ids.trait_id); + assert_eq!(first.payload.method_id, sub_ids.receive_id); // V1(Disconnected): V1 variant 0x00, Disconnected discriminant 0x00. assert_eq!(first.payload.value, vec![0x00, 0x00]); } diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index a7258059f..46812c6a8 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -1,10 +1,10 @@ //! Request dispatcher. //! //! Routes incoming frames to the appropriate trait method based on the -//! numeric wire discriminant. The handler set is registered by the -//! auto-generated [`crate::generated::dispatcher::register`] function; this -//! module provides the framework that owns the registration tables and the -//! routing logic. +//! numeric `(trait, method)` wire discriminant pair. The handler set is +//! registered by the auto-generated +//! [`crate::generated::dispatcher::register`] function; this module provides +//! the framework that owns the registration tables and the routing logic. use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -14,7 +14,8 @@ use parity_scale_codec::Encode; use tracing::instrument; use crate::frame::{ - PROTOCOL_ERROR_ID, Payload, ProtocolErrorV1, ProtocolMessage, VersionedProtocolError, + PROTOCOL_ERROR_KEY, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, ProtocolErrorV1, + ProtocolMessage, VersionedProtocolError, }; use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds}; use crate::subscription::{Spawner, SubscriptionManager, SubscriptionStream}; @@ -51,11 +52,11 @@ pub struct SubscriptionEntry { } /// Routes incoming protocol messages to registered handlers, keyed on the -/// numeric wire discriminant. +/// numeric `(trait, method)` wire discriminant pair. pub struct Dispatcher { - by_request: HashMap, - by_start: HashMap, - stop_ids: HashSet, + by_request: HashMap<(u8, u8), RequestEntry>, + by_start: HashMap<(u8, u8), SubscriptionEntry>, + stop_ids: HashSet<(u8, u8)>, subscriptions: SubscriptionManager, /// Trusted executable kind bound to this connection; `None` leaves the /// surface unrestricted for direct dispatcher embeddings. @@ -90,10 +91,11 @@ impl Dispatcher { self.execution.is_none_or(|actual| actual == required) } - /// Register a request-response handler, keyed on `ids.request_id`. Returns - /// the previously registered entry if any; callers (the generated - /// `dispatcher::register`) should treat `Some` as a programming error - /// since each request id must own exactly one handler. + /// Register a request-response handler, keyed on + /// `(ids.trait_id, ids.request_id)`. Returns the previously registered + /// entry if any; callers (the generated `dispatcher::register`) should + /// treat `Some` as a programming error since each discriminant pair must + /// own exactly one handler. pub fn on_request(&mut self, ids: RequestFrameIds, handler: F) -> Option where F: Fn(String, Vec) -> BoxFuture<'static, Result, Vec>> @@ -102,7 +104,7 @@ impl Dispatcher { + 'static, { self.by_request.insert( - ids.request_id, + (ids.trait_id, ids.request_id), RequestEntry { ids, handler: Arc::new(handler), @@ -110,9 +112,10 @@ impl Dispatcher { ) } - /// Register a subscription handler, keyed on `ids.start_id`, and record - /// `ids.stop_id` so a matching `_stop` frame tears the subscription down. - /// Returns the previously registered entry if any. + /// Register a subscription handler, keyed on + /// `(ids.trait_id, ids.start_id)`, and record the stop pair so a matching + /// `_stop` frame tears the subscription down. Returns the previously + /// registered entry if any. pub fn on_subscription( &mut self, ids: SubscriptionFrameIds, @@ -124,9 +127,9 @@ impl Dispatcher { + Sync + 'static, { - self.stop_ids.insert(ids.stop_id); + self.stop_ids.insert((ids.trait_id, ids.stop_id)); self.by_start.insert( - ids.start_id, + (ids.trait_id, ids.start_id), SubscriptionEntry { ids, handler: Arc::new(handler), @@ -135,16 +138,21 @@ impl Dispatcher { } /// Process an incoming protocol message, sending any responses or - /// subscription frames through `transport`. + /// subscription frames through `transport`. A `(trait, method)` pair with + /// no registered handler is answered with a correlated protocol error + /// rather than dropped, so a peer learns its frame went unhandled instead + /// of waiting on a reply that never comes. #[instrument(skip_all, fields(runtime.method = "dispatcher.dispatch"))] pub async fn dispatch(&self, message: ProtocolMessage, transport: Arc) { - let id = message.payload.id; + let key = (message.payload.trait_id, message.payload.method_id); - if id == PROTOCOL_ERROR_ID { + // Never answer a protocol error with a protocol error: two peers that + // disagree would otherwise trade frames forever. + if key == PROTOCOL_ERROR_KEY { return; } - if let Some(entry) = self.by_request.get(&id) { + if let Some(entry) = self.by_request.get(&key) { let request_id = message.request_id.clone(); let value = (entry.handler)(request_id, message.payload.value) .await @@ -152,11 +160,12 @@ impl Dispatcher { transport.send(ProtocolMessage { request_id: message.request_id, payload: Payload { - id: entry.ids.response_id, + trait_id: entry.ids.trait_id, + method_id: entry.ids.response_id, value, }, }); - } else if let Some(entry) = self.by_start.get(&id) { + } else if let Some(entry) = self.by_start.get(&key) { // Reserve the slot before awaiting the handler so a `_stop` // arriving while the handler resolves cancels the pending // subscription instead of racing the registration. @@ -167,6 +176,7 @@ impl Dispatcher { Ok(stream) => { self.subscriptions.activate( token, + entry.ids.trait_id, entry.ids.receive_id, entry.ids.interrupt_id, stream, @@ -178,21 +188,30 @@ impl Dispatcher { transport.send(ProtocolMessage { request_id: message.request_id, payload: Payload { - id: entry.ids.interrupt_id, + trait_id: entry.ids.trait_id, + method_id: entry.ids.interrupt_id, value: err_bytes, }, }); } } - } else if self.stop_ids.contains(&id) { + } else if self.stop_ids.contains(&key) { self.subscriptions.handle_stop(&message.request_id); } else { + // Response / receive / interrupt frames are handled by the client + // side and are never registered here, so they land in this arm too: + // answering them is what tells a mismatched peer its frame was not + // understood. No log - a peer speaking a wire we do not know could + // otherwise flood the host's logs one frame at a time. + let (trait_id, method_id) = key; transport.send(ProtocolMessage { request_id: message.request_id, payload: Payload { - id: PROTOCOL_ERROR_ID, + trait_id: PROTOCOL_ERROR_TRAIT_ID, + method_id: PROTOCOL_ERROR_METHOD_ID, value: VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { - discriminant: id, + trait_id, + method_id, }) .encode(), }, @@ -245,10 +264,14 @@ mod tests { } } - fn make_frame(id: u8, value: Vec) -> ProtocolMessage { + fn make_frame(trait_id: u8, method_id: u8, value: Vec) -> ProtocolMessage { ProtocolMessage { request_id: "p:1".into(), - payload: Payload { id, value }, + payload: Payload { + trait_id, + method_id, + value, + }, } } @@ -257,17 +280,21 @@ mod tests { let dispatcher = Dispatcher::new(test_spawner()); let transport = Arc::new(RecordingTransport::default()); let transport_dyn: Arc = transport.clone(); - let frame = make_frame(250, Vec::new()); + let frame = make_frame(250, 251, Vec::new()); futures::executor::block_on(dispatcher.dispatch(frame, transport_dyn)); + // 250 != 251 on purpose: the reply must echo the pair in the order it + // arrived, and equal values would let a transposition pass. assert_eq!( transport.sent(), vec![ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: PROTOCOL_ERROR_ID, + trait_id: PROTOCOL_ERROR_TRAIT_ID, + method_id: PROTOCOL_ERROR_METHOD_ID, value: VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { - discriminant: 250 - },) + trait_id: 250, + method_id: 251, + }) .encode(), }, }] @@ -279,9 +306,13 @@ mod tests { let dispatcher = Dispatcher::new(test_spawner()); let transport = Arc::new(RecordingTransport::default()); let frame = make_frame( - PROTOCOL_ERROR_ID, - VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { discriminant: 250 }) - .encode(), + PROTOCOL_ERROR_TRAIT_ID, + PROTOCOL_ERROR_METHOD_ID, + VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { + trait_id: 250, + method_id: 251, + }) + .encode(), ); futures::executor::block_on(dispatcher.dispatch(frame, transport.clone())); assert_eq!(transport.sent(), Vec::::new()); @@ -293,6 +324,7 @@ mod tests { fn dispatch_request_handler_error_emits_response_payload() { let mut dispatcher = Dispatcher::new(test_spawner()); let ids = RequestFrameIds { + trait_id: 7, request_id: 200, response_id: 201, }; @@ -300,11 +332,12 @@ mod tests { Box::pin(async move { Err(vec![9, 8, 7]) }) }); let transport = Arc::new(RecordingTransport::default()); - let frame = make_frame(200, Vec::new()); + let frame = make_frame(7, 200, Vec::new()); futures::executor::block_on(dispatcher.dispatch(frame, transport.clone())); let sent = transport.sent(); assert_eq!(sent.len(), 1, "exactly one response expected"); - assert_eq!(sent[0].payload.id, 201); + assert_eq!(sent[0].payload.trait_id, 7); + assert_eq!(sent[0].payload.method_id, 201); assert_eq!(sent[0].payload.value, vec![9, 8, 7]); } @@ -315,6 +348,7 @@ mod tests { fn register_request_twice_returns_previous_handler() { let mut dispatcher = Dispatcher::new(test_spawner()); let ids = RequestFrameIds { + trait_id: 7, request_id: 200, response_id: 201, }; diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index f119fcc5b..6269e60c9 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -4,15 +4,16 @@ //! and a `payload`. On the wire the envelope is: //! //! ```text -//! [requestId: SCALE str][discriminant: u8][payload bytes...] +//! [requestId: SCALE str][trait: u8][method: u8][payload bytes...] //! ``` //! -//! The discriminant maps to a method/kind slot via the auto-generated -//! [`crate::generated::wire_table::WIRE_TABLE`]. Method ordering is part of -//! the wire protocol; only ever append to the table. The payload bytes are -//! the SCALE-encoded inner value, inlined without a length prefix. +//! The `(trait, method)` discriminant pair maps to a method/kind slot via the +//! auto-generated [`crate::generated::wire_table::WIRE_TABLE`]. Trait ids and +//! per-trait method ordering are part of the wire protocol; only ever append +//! within a trait. The payload bytes are the SCALE-encoded inner value, +//! inlined without a length prefix. //! -//! In-memory we keep the numeric id directly so dispatch does not need to +//! In-memory we keep the numeric pair directly so dispatch does not need to //! reconstruct string action tags on every frame. use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, Output}; @@ -21,7 +22,7 @@ use truapi::versioned::{FromLatest, IntoLatest, Versioned}; use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds, WIRE_TABLE, WireKind}; -/// Top-level wire message. Encoded as `[requestId][discriminant][bytes]`. +/// Top-level wire message. Encoded as `[requestId][trait][method][bytes]`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProtocolMessage { /// Per-message identifier carried by both halves of a request/response. @@ -30,10 +31,19 @@ pub struct ProtocolMessage { pub payload: Payload, } -/// Reserved discriminant for method-independent protocol errors. -pub const PROTOCOL_ERROR_ID: u8 = 255; +/// Reserved trait discriminant for method-independent protocol errors. No API +/// trait may declare it (the codegen rejects `#[wire_trait(id = 255)]`), so no +/// method can ever be addressed here. +pub const PROTOCOL_ERROR_TRAIT_ID: u8 = 255; -/// Versioned payload carried by [`PROTOCOL_ERROR_ID`] frames. +/// Reserved method discriminant for method-independent protocol errors, within +/// [`PROTOCOL_ERROR_TRAIT_ID`]. +pub const PROTOCOL_ERROR_METHOD_ID: u8 = 255; + +/// The reserved `(trait, method)` address protocol errors travel on. +pub const PROTOCOL_ERROR_KEY: (u8, u8) = (PROTOCOL_ERROR_TRAIT_ID, PROTOCOL_ERROR_METHOD_ID); + +/// Versioned payload carried by [`PROTOCOL_ERROR_KEY`] frames. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum VersionedProtocolError { /// Initial protocol error shape. @@ -44,11 +54,15 @@ pub enum VersionedProtocolError { /// Protocol errors supported by codec version 1. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum ProtocolErrorV1 { - /// The receiver does not support the incoming message discriminant. + /// The receiver does not support the incoming message address. Codec 2 + /// addresses a frame by `(trait, method)`, so one byte can no longer name + /// it: a bare method id is ambiguous across traits. #[codec(index = 0)] UnsupportedMessage { - /// Unsupported wire discriminant from the incoming frame. - discriminant: u8, + /// Trait discriminant of the unsupported incoming frame. + trait_id: u8, + /// Method discriminant of the unsupported incoming frame. + method_id: u8, }, } @@ -130,7 +144,8 @@ pub fn encode_versioned_interrupt_payload(value: T, version: u8) -> V impl Encode for ProtocolMessage { fn encode_to(&self, dest: &mut T) { self.request_id.encode_to(dest); - self.payload.id.encode_to(dest); + self.payload.trait_id.encode_to(dest); + self.payload.method_id.encode_to(dest); // Payload bytes are inlined; the receiver reads "until end of frame" // because each transport frame is one ProtocolMessage. This matches // the public versioned enum transport shape (variant payload encoded @@ -145,35 +160,45 @@ impl Encode for ProtocolMessage { impl Decode for ProtocolMessage { fn decode(input: &mut I) -> Result { let request_id = String::decode(input)?; - let id = u8::decode(input)?; - // Unknown ids are accepted here; routing is deferred to dispatch. + let trait_id = u8::decode(input) + .map_err(|_| CodecError::from("frame is missing the trait discriminant byte"))?; + let method_id = u8::decode(input) + .map_err(|_| CodecError::from("frame is missing the method discriminant byte"))?; + // Unknown (trait, method) pairs are accepted here; routing is deferred + // to dispatch, which reports frames with no registered handler. let remaining = input .remaining_len()? .ok_or_else(|| CodecError::from("frame input must report remaining length"))?; let mut value = vec![0u8; remaining]; input.read(&mut value)?; - if id == PROTOCOL_ERROR_ID { + if (trait_id, method_id) == PROTOCOL_ERROR_KEY { decode_protocol_error_payload(&value)?; } Ok(ProtocolMessage { request_id, - payload: Payload { id, value }, + payload: Payload { + trait_id, + method_id, + value, + }, }) } } -/// Tagged payload. The `id` is the wire discriminant from -/// [`crate::generated::wire_table::WIRE_TABLE`], identifying the frame's method -/// and kind (request/response/start/stop/interrupt/receive). +/// Tagged payload. The `(trait_id, method_id)` pair is the wire discriminant +/// from [`crate::generated::wire_table::WIRE_TABLE`], identifying the frame's +/// trait, method, and kind (request/response/start/stop/interrupt/receive). /// /// Note: `Payload` does not derive `Encode`/`Decode` directly; the wire /// representation lives on [`ProtocolMessage`]. `Payload` is kept as a plain -/// data type for in-memory dispatch (key on `id`, value bytes already +/// data type for in-memory dispatch (key on the pair, value bytes already /// SCALE-encoded by the call site). #[derive(Debug, Clone, PartialEq, Eq)] pub struct Payload { - /// Wire discriminant identifying the frame's method and kind. - pub id: u8, + /// Trait discriminant: first byte of the wire pair. + pub trait_id: u8, + /// Method discriminant within the trait: second byte of the wire pair. + pub method_id: u8, /// SCALE-encoded inner value bytes. pub value: Vec, } @@ -248,80 +273,98 @@ mod tests { V1(T), } - fn build(id: u8, value: Vec) -> ProtocolMessage { + fn build(trait_id: u8, method_id: u8, value: Vec) -> ProtocolMessage { ProtocolMessage { request_id: "p:1".to_string(), - payload: Payload { id, value }, + payload: Payload { + trait_id, + method_id, + value, + }, } } - fn expected_wire(id: u8, value: &[u8]) -> Vec { + fn expected_wire(trait_id: u8, method_id: u8, value: &[u8]) -> Vec { let mut out = Vec::new(); "p:1".to_string().encode_to(&mut out); - out.push(id); + out.push(trait_id); + out.push(method_id); out.extend_from_slice(value); out } #[test] - fn handshake_request_encodes_with_discriminant_zero() { - // SCALE-encoded HostHandshakeRequest::V1(1u8) = [0u8 variant][1u8 codec_version] - let inner: Vec = vec![0x00, 0x01]; - let msg = build(0, inner.clone()); - assert_eq!(msg.encode(), expected_wire(0, &inner)); + fn handshake_request_encodes_with_discriminant_pair_zero_zero() { + // SCALE-encoded HostHandshakeRequest::V1(2u8) = [0u8 variant][2u8 codec_version] + let inner: Vec = vec![0x00, 0x02]; + let msg = build(0, 0, inner.clone()); + assert_eq!(msg.encode(), expected_wire(0, 0, &inner)); } #[test] - fn get_account_request_encodes_with_discriminant_22() { + fn get_account_request_encodes_with_discriminant_pair() { let mut inner = vec![0x00]; // V1 variant "foo".to_string().encode_to(&mut inner); 0u32.encode_to(&mut inner); - let msg = build(22, inner.clone()); - assert_eq!(msg.encode(), expected_wire(22, &inner)); + // account trait = 1, get_account request = 4. + let msg = build(1, 4, inner.clone()); + assert_eq!(msg.encode(), expected_wire(1, 4, &inner)); } #[test] - fn round_trip_preserves_id_and_value() { + fn round_trip_preserves_ids_and_value() { let inner: Vec = vec![0x00, 0x42, 0xab, 0xcd]; - let msg = build(12, inner.clone()); + let msg = build(6, 0, inner.clone()); let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); assert_eq!(decoded, msg); } - /// An unknown discriminant is no longer rejected at decode; routing is - /// deferred to dispatch. + /// An unknown discriminant pair is not rejected at decode; routing is + /// deferred to dispatch (which reports frames with no registered handler). #[test] - fn unknown_discriminant_decodes_ok() { + fn unknown_discriminant_pair_decodes_ok() { let mut bytes = Vec::new(); "p:1".to_string().encode_to(&mut bytes); - bytes.push(250); // far outside the populated range + bytes.push(250); // far outside the populated trait range + bytes.push(123); bytes.extend_from_slice(&[0xaa, 0xbb]); - let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("unknown id must decode"); - assert_eq!(decoded.payload.id, 250); + let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("unknown pair must decode"); + assert_eq!(decoded.payload.trait_id, 250); + assert_eq!(decoded.payload.method_id, 123); assert_eq!(decoded.payload.value, vec![0xaa, 0xbb]); } #[test] fn protocol_error_payload_has_stable_versioned_shape() { - let error = - VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { discriminant: 250 }); + let error = VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { + trait_id: 250, + method_id: 251, + }); let encoded = error.encode(); let decoded = VersionedProtocolError::decode(&mut &encoded[..]).expect("decode"); - assert_eq!((encoded, decoded), (vec![0, 0, 250], error)); + // [0] versioned index, [0] variant index, [250] trait, [251] method. + // The pair grew this payload from 3 bytes to 4; trait and method differ + // here on purpose, so transposing the two fields cannot pass. + assert_eq!((encoded, decoded), (vec![0, 0, 250, 251], error)); } #[test] fn malformed_protocol_error_payloads_fail_frame_decoding() { + // Re-derived for the 2-byte address: a valid payload is now 4 bytes, so + // `[0, 0, 250, 0]` - the old trailing-byte case - decodes cleanly as the + // pair (250, 0) and would silently stop testing anything. for payload in [ - vec![0, 0], - vec![0, 0, 250, 0], - vec![1, 0, 250], - vec![0, 1, 250], + vec![0, 0], // no address at all + vec![0, 0, 250], // trait present, method truncated + vec![0, 0, 250, 251, 0], // one trailing byte past a full pair + vec![1, 0, 250, 251], // unknown VersionedProtocolError index + vec![0, 1, 250, 251], // unknown ProtocolErrorV1 variant index ] { let message = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: PROTOCOL_ERROR_ID, + trait_id: PROTOCOL_ERROR_TRAIT_ID, + method_id: PROTOCOL_ERROR_METHOD_ID, value: payload, }, }; @@ -333,25 +376,28 @@ mod tests { /// regression where `Decode` mishandles a frame whose payload is empty for /// `_stop` / `_interrupt` (no inner data) but non-empty for `_start` / /// `_receive`. The ids are the `account_connection_status_subscribe` - /// quartet (18..=21). + /// quartet (trait 1, methods 0..=3). #[test] fn subscription_phases_round_trip_through_codec() { let cases: &[(u8, Vec)] = &[ - (18, vec![0x00, 0xaa]), // start - (19, Vec::new()), // stop - (20, Vec::new()), // interrupt - (21, vec![0x01, 0x02, 0x03, 0x04]), // receive + (0, vec![0x00, 0xaa]), // start + (1, Vec::new()), // stop + (2, Vec::new()), // interrupt + (3, vec![0x01, 0x02, 0x03, 0x04]), // receive ]; - for (id, value) in cases { - let msg = build(*id, value.clone()); + for (method_id, value) in cases { + let msg = build(1, *method_id, value.clone()); let bytes = msg.encode(); assert_eq!( bytes, - expected_wire(*id, value), - "encode mismatch for id {id}" + expected_wire(1, *method_id, value), + "encode mismatch for method id {method_id}" ); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); - assert_eq!(decoded, msg, "round-trip mismatch for id {id}"); + assert_eq!( + decoded, msg, + "round-trip mismatch for method id {method_id}" + ); } } @@ -360,18 +406,21 @@ mod tests { #[test] fn id_helpers_resolve_known_methods() { let handshake = request_ids("system_handshake").expect("known request method"); + assert_eq!(handshake.trait_id, 0); assert_eq!(handshake.request_id, 0); assert_eq!(handshake.response_id, 1); let get_account = request_ids("account_get_account").expect("known request method"); - assert_eq!(get_account.request_id, 22); + assert_eq!(get_account.trait_id, 1); + assert_eq!(get_account.request_id, 4); let sub = subscription_ids("account_connection_status_subscribe").expect("known subscription"); - assert_eq!(sub.start_id, 18); - assert_eq!(sub.stop_id, 19); - assert_eq!(sub.interrupt_id, 20); - assert_eq!(sub.receive_id, 21); + assert_eq!(sub.trait_id, 1); + assert_eq!(sub.start_id, 0); + assert_eq!(sub.stop_id, 1); + assert_eq!(sub.interrupt_id, 2); + assert_eq!(sub.receive_id, 3); // A request method is not a subscription and vice versa. assert!(subscription_ids("system_handshake").is_none()); @@ -383,11 +432,11 @@ mod tests { /// handle `remaining_len == 0` without erroring or reading past EOF. #[test] fn empty_payload_round_trips() { - // local_storage_clear_response = 17. - let msg = build(17, Vec::new()); + // local_storage_clear_response = (6, 5). + let msg = build(6, 5, Vec::new()); let bytes = msg.encode(); - // [SCALE compact-len 0x0c][p][:][1][u8 17] = 4 + 1 = 5 bytes total - assert_eq!(bytes.len(), 5); + // [SCALE compact-len 0x0c][p][:][1][u8 6][u8 5] = 4 + 2 = 6 bytes total + assert_eq!(bytes.len(), 6); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); assert_eq!(decoded, msg); } @@ -400,7 +449,8 @@ mod tests { let msg = ProtocolMessage { request_id: long_id, payload: Payload { - id: 22, + trait_id: 1, + method_id: 4, value: vec![0x00, 0xab, 0xcd], }, }; @@ -408,15 +458,30 @@ mod tests { assert_eq!(decoded, msg); } - /// Truncated frames must surface a `CodecError`, not panic. + /// Truncated frames must surface a `CodecError`, not panic, and the + /// trait-byte and method-byte truncations report distinct errors. #[test] fn truncated_frames_error_cleanly() { // Empty buffer. assert!(ProtocolMessage::decode(&mut &[][..]).is_err()); - // Just the requestId, no discriminant byte. + // Just the requestId, no trait byte. let mut only_request_id = Vec::new(); "p:1".to_string().encode_to(&mut only_request_id); - assert!(ProtocolMessage::decode(&mut &only_request_id[..]).is_err()); + let err = ProtocolMessage::decode(&mut &only_request_id[..]) + .expect_err("missing trait byte must error"); + assert!( + format!("{err}").contains("trait discriminant"), + "unexpected error: {err}" + ); + // RequestId plus the trait byte, no method byte. + let mut missing_method = only_request_id.clone(); + missing_method.push(0); + let err = ProtocolMessage::decode(&mut &missing_method[..]) + .expect_err("missing method byte must error"); + assert!( + format!("{err}").contains("method discriminant"), + "unexpected error: {err}" + ); // RequestId header claims length=200 but the buffer is far shorter. let truncated_str_header = [200u8 << 2, 0x61, 0x62, 0x63]; assert!(ProtocolMessage::decode(&mut &truncated_str_header[..]).is_err()); @@ -430,12 +495,13 @@ mod tests { let msg = ProtocolMessage { request_id: String::new(), payload: Payload { - id: 22, + trait_id: 1, + method_id: 4, value: vec![0x00, 0x01, 0x02], }, }; let bytes = msg.encode(); - // [SCALE compact-len 0 = 0x00][discriminant][payload] + // [SCALE compact-len 0 = 0x00][trait][method][payload] assert_eq!(bytes[0], 0x00); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); assert_eq!(decoded, msg); @@ -447,7 +513,8 @@ mod tests { let msg = ProtocolMessage { request_id: "héllo-世界-🦀".to_string(), payload: Payload { - id: 22, + trait_id: 1, + method_id: 4, value: vec![0x00, 0x01], }, }; @@ -460,7 +527,7 @@ mod tests { #[test] fn large_payload_round_trips() { let big = vec![0xa5u8; 100 * 1024]; - let msg = build(22, big); + let msg = build(1, 4, big); let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); assert_eq!(decoded, msg); } diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index 7291ecad7..3988971c4 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -1520,7 +1520,8 @@ mod tests { let frame = ProtocolMessage { request_id: "theme:1".to_string(), payload: Payload { - id: ids.start_id, + trait_id: ids.trait_id, + method_id: ids.start_id, value: Vec::new(), }, }; diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index ede443c2c..960e5bbb7 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -3329,7 +3329,8 @@ mod tests { let permission_frame = ProtocolMessage { request_id: "p:permission".into(), payload: Payload { - id: permission_ids.request_id, + trait_id: permission_ids.trait_id, + method_id: permission_ids.request_id, value: HostDevicePermissionRequest::V1( v01::HostDevicePermissionRequest::Camera, ) @@ -3355,7 +3356,8 @@ mod tests { let feature_frame = ProtocolMessage { request_id: "p:feature".into(), payload: Payload { - id: feature_ids.request_id, + trait_id: feature_ids.trait_id, + method_id: feature_ids.request_id, value: HostFeatureSupportedRequest::V1( v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], @@ -3410,10 +3412,18 @@ mod tests { }); assert_eq!(feature_response.request_id, "p:feature"); - assert_eq!(feature_response.payload.id, feature_ids.response_id); + assert_eq!(feature_response.payload.trait_id, feature_ids.trait_id); + assert_eq!(feature_response.payload.method_id, feature_ids.response_id); assert_eq!(permission_response.request_id, "p:permission"); - assert_eq!(permission_response.payload.id, permission_ids.response_id); + assert_eq!( + permission_response.payload.trait_id, + permission_ids.trait_id + ); + assert_eq!( + permission_response.payload.method_id, + permission_ids.response_id + ); // [Ok 0x00][V1 0x00][granted=1] assert_eq!(permission_response.payload.value, vec![0x00, 0x00, 0x01]); diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index ab60fd0e5..0657a7572 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -20,7 +20,8 @@ use parity_scale_codec::{Decode, DecodeLimit, Encode}; use truapi::v01; use crate::frame::{ - IdFactory, PROTOCOL_ERROR_ID, Payload, ProtocolErrorV1, ProtocolMessage, + IdFactory, PROTOCOL_ERROR_KEY, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, + ProtocolErrorV1, ProtocolMessage, VersionedProtocolError, decode_protocol_error_payload, }; use crate::generated::wire_table::SubscriptionFrameIds; @@ -155,6 +156,7 @@ impl SubscriptionManager { pub fn activate( &self, token: ReservationToken, + trait_id: u8, receive_id: u8, interrupt_id: u8, mut stream: SubscriptionStream, @@ -212,7 +214,8 @@ impl SubscriptionManager { stream_transport.send(ProtocolMessage { request_id: rid.clone(), payload: Payload { - id: receive_id, + trait_id, + method_id: receive_id, value, }, }) @@ -221,7 +224,8 @@ impl SubscriptionManager { stream_transport.send(ProtocolMessage { request_id: rid.clone(), payload: Payload { - id: interrupt_id, + trait_id, + method_id: interrupt_id, value, }, }); @@ -252,7 +256,8 @@ impl SubscriptionManager { transport.send(ProtocolMessage { request_id, payload: Payload { - id: interrupt_id, + trait_id, + method_id: interrupt_id, value: Vec::new(), }, }); @@ -267,13 +272,14 @@ impl SubscriptionManager { pub fn register( &self, request_id: String, + trait_id: u8, receive_id: u8, interrupt_id: u8, stream: SubscriptionStream, transport: Arc, ) { let token = self.reserve(request_id); - self.activate(token, receive_id, interrupt_id, stream, transport); + self.activate(token, trait_id, receive_id, interrupt_id, stream, transport); } /// Handle a `_stop` frame from the product side. Cancels a live @@ -419,24 +425,28 @@ impl HostInitiatedSubscriptionManager { .lock() .expect("host subscription state mutex poisoned"); let slot = state.active.get(&message.request_id)?; - if message.payload.id == slot.ids.receive_id { + let key = (message.payload.trait_id, message.payload.method_id); + if key == (slot.ids.trait_id, slot.ids.receive_id) { let sender = slot.sender.clone(); drop(state); let _ = sender.unbounded_send(HostInitiatedFrame::Item(message.payload.value)); - } else if message.payload.id == slot.ids.interrupt_id { + } else if key == (slot.ids.trait_id, slot.ids.interrupt_id) { // Deliver the terminal before dropping the sender, so the stream // reports a declining product rather than a silent end. let sender = slot.sender.clone(); let _ = sender.unbounded_send(HostInitiatedFrame::Interrupt); state.active.remove(&message.request_id); - } else if message.payload.id == PROTOCOL_ERROR_ID { + } else if key == PROTOCOL_ERROR_KEY { let Ok(VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { - discriminant, + trait_id, + method_id, })) = decode_protocol_error_payload(&message.payload.value) else { return None; }; - if discriminant != slot.ids.start_id { + // Only OUR start frame going unsupported ends this render; an error + // about any other pair belongs to a different subscription. + if (trait_id, method_id) != (slot.ids.trait_id, slot.ids.start_id) { return None; } let sender = slot.sender.clone(); @@ -820,9 +830,11 @@ mod tests { manager.handle_message(ProtocolMessage { request_id: "h:1".into(), payload: Payload { - id: PROTOCOL_ERROR_ID, + trait_id: PROTOCOL_ERROR_TRAIT_ID, + method_id: PROTOCOL_ERROR_METHOD_ID, value: VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { - discriminant: host_ids().start_id, + trait_id: host_ids().trait_id, + method_id: host_ids().start_id, }) .encode(), }, @@ -847,7 +859,8 @@ mod tests { for value in [ VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { - discriminant: host_ids().stop_id, + trait_id: host_ids().trait_id, + method_id: host_ids().stop_id, }) .encode(), vec![0, 0], @@ -855,7 +868,8 @@ mod tests { manager.handle_message(ProtocolMessage { request_id: "h:1".into(), payload: Payload { - id: PROTOCOL_ERROR_ID, + trait_id: PROTOCOL_ERROR_TRAIT_ID, + method_id: PROTOCOL_ERROR_METHOD_ID, value, }, }); @@ -971,7 +985,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let slow_stream: SubscriptionStream = Box::pin(stream::pending()); - manager.register("p:1".to_string(), 99, 98, slow_stream, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 98, slow_stream, transport_dyn); manager.handle_stop("p:1"); // Give the worker thread a beat to observe the cancel. std::thread::sleep(std::time::Duration::from_millis(50)); @@ -989,15 +1003,17 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let items = dummy_stream(vec![vec![0xaa], vec![0xbb]]); - manager.register("p:1".to_string(), 99, 98, items, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 98, items, transport_dyn); let observed = transport_typed.wait_for(3, std::time::Duration::from_secs(2)); assert_eq!(observed, 3, "expected 2 receive frames + 1 interrupt"); let frames = transport_typed.sent(); - assert_eq!(frames[0].payload.id, 99); + assert_eq!(frames[0].payload.trait_id, 7); + assert_eq!(frames[0].payload.method_id, 99); assert_eq!(frames[0].payload.value, vec![0xaa]); - assert_eq!(frames[1].payload.id, 99); + assert_eq!(frames[1].payload.method_id, 99); assert_eq!(frames[1].payload.value, vec![0xbb]); - assert_eq!(frames[2].payload.id, 98); + assert_eq!(frames[2].payload.trait_id, 7); + assert_eq!(frames[2].payload.method_id, 98); assert_eq!(frames[2].payload.value, Vec::::new()); } @@ -1010,7 +1026,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let slow_stream: SubscriptionStream = Box::pin(stream::pending()); - manager.register("p:1".to_string(), 99, 98, slow_stream, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 98, slow_stream, transport_dyn); manager.handle_stop("p:1"); // Second call must not panic and must not emit any frame. manager.handle_stop("p:1"); @@ -1037,7 +1053,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(spawner); let items = dummy_stream(vec![vec![0xcc]]); - manager.register("p:1".to_string(), 99, 98, items, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 98, items, transport_dyn); // Wait for the worker future to drain to completion so we know // the spawner closure ran on this path. @@ -1060,7 +1076,7 @@ mod tests { let token = manager.reserve("p:1".to_string()); manager.handle_stop("p:1"); let items = dummy_stream(vec![vec![0x01], vec![0x02]]); - manager.activate(token, 99, 98, items, transport_dyn); + manager.activate(token, 7, 99, 98, items, transport_dyn); std::thread::sleep(std::time::Duration::from_millis(50)); assert!( transport_typed.sent().is_empty(), @@ -1081,11 +1097,11 @@ mod tests { // First subscription never yields; the second reservation for the // same id must stop it. let pending: SubscriptionStream = Box::pin(stream::pending()); - manager.register("p:1".to_string(), 99, 98, pending, transport_dyn.clone()); + manager.register("p:1".to_string(), 7, 99, 98, pending, transport_dyn.clone()); // Second subscription yields one item then ends. let items = dummy_stream(vec![vec![0xaa]]); - manager.register("p:1".to_string(), 99, 98, items, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 98, items, transport_dyn); // Exactly the second stream's frames appear: one receive + one // completion interrupt. The first (pending) stream contributes none. @@ -1095,9 +1111,11 @@ mod tests { "expected the second stream's receive + interrupt only" ); let frames = transport_typed.sent(); - assert_eq!(frames[0].payload.id, 99); + assert_eq!(frames[0].payload.trait_id, 7); + assert_eq!(frames[0].payload.method_id, 99); assert_eq!(frames[0].payload.value, vec![0xaa]); - assert_eq!(frames[1].payload.id, 98); + assert_eq!(frames[1].payload.trait_id, 7); + assert_eq!(frames[1].payload.method_id, 98); manager.handle_stop("p:1"); std::thread::sleep(std::time::Duration::from_millis(50)); @@ -1118,7 +1136,7 @@ mod tests { dropped: dropped.clone(), }); - manager.register("p:1".to_string(), 99, 98, stream, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 98, stream, transport_dyn); manager.cancel_all(); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); diff --git a/rust/crates/truapi-server/src/ws_bridge.rs b/rust/crates/truapi-server/src/ws_bridge.rs index 7cc279d3f..5bd2a1bfe 100644 --- a/rust/crates/truapi-server/src/ws_bridge.rs +++ b/rust/crates/truapi-server/src/ws_bridge.rs @@ -632,7 +632,8 @@ mod tests { let request_frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: HostFeatureSupportedRequest::V1( v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], @@ -658,7 +659,8 @@ mod tests { let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); assert_eq!(response.request_id, "p:1"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // Wire payload is `Result`-shaped: // [Ok disc=0x00][V1 variant 0x00][supported=1] assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs index 94a75050d..f6aa3f62c 100644 --- a/rust/crates/truapi-server/tests/golden_frame.rs +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -9,9 +9,10 @@ //! payload = account_get_account_request, //! inner = HostAccountGetRequest::V1(("foo", 0u32)) //! -//! On the wire (14 bytes): +//! On the wire (15 bytes): //! [0c 70 3a 31] requestId = compact-len(3) + "p:1" -//! [16] discriminant 22 = account_get_account_request +//! [01] trait discriminant 1 = account +//! [04] method discriminant 4 = get_account request //! [00] versioned wrapper variant V1 //! [0c 66 6f 6f] "foo" //! [00 00 00 00] u32 = 0 @@ -38,7 +39,8 @@ fn golden_account_get_frame_decodes_to_expected_message() { let expected = ProtocolMessage { request_id: "p:1".to_string(), payload: Payload { - id: wire_table::ACCOUNT_GET_ACCOUNT.request_id, + trait_id: wire_table::ACCOUNT_GET_ACCOUNT.trait_id, + method_id: wire_table::ACCOUNT_GET_ACCOUNT.request_id, value: expected_inner, }, }; diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin index c66be11b9bf19e8c751b7faa4996bf36cd7e90b4..d14ecd70a7c811793b3ce3ca6e1624e479aacd6a 100644 GIT binary patch literal 15 Ucmd-nurg$1Vc<#2&u0Jv02D(4Jpcdz literal 14 Tcmd-nurd^5;7QBRX8-~K6a)fJ diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index 5cfb62cbe..bee335622 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -27,7 +27,8 @@ use truapi::{CallError, v01}; use truapi_server::core::TrUApiCore; use truapi_server::frame::{ - PROTOCOL_ERROR_ID, Payload, ProtocolErrorV1, ProtocolMessage, VersionedProtocolError, + PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, ProtocolErrorV1, ProtocolMessage, + VersionedProtocolError, request_ids, subscription_ids, }; @@ -53,13 +54,15 @@ fn feature_supported_ok_response_uses_ok_discriminant() { let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; let response = dispatch(&core, frame); assert_eq!(response.request_id, "p:1"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // Wire payload: [V1 disc=0x00][Ok disc=0x00][encoded response body]. let mut expected = vec![0x00u8, 0x00u8]; @@ -138,13 +141,15 @@ fn local_storage_read_err_response_uses_err_discriminant() { let frame = ProtocolMessage { request_id: "p:2".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; let response = dispatch(&core, frame); assert_eq!(response.request_id, "p:2"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // Wire payload: // [V1 disc=0x00][Err disc=0x01][CallError::Domain][V1 error][encoded error body]. @@ -193,13 +198,15 @@ fn assert_request_returns_domain_error( ProtocolMessage { request_id: request_id.into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value, }, }, ); assert_eq!(response.request_id, request_id); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); assert_eq!(response.payload.value, versioned_result_err_payload(error)); } @@ -218,7 +225,8 @@ fn assert_subscription_start_interrupts_error( ProtocolMessage { request_id: request_id.into(), payload: Payload { - id: ids.start_id, + trait_id: ids.trait_id, + method_id: ids.start_id, value, }, }, @@ -228,7 +236,8 @@ fn assert_subscription_start_interrupts_error( let sent = transport.sent.lock().unwrap(); assert_eq!(sent.len(), 1); assert_eq!(sent[0].request_id, request_id); - assert_eq!(sent[0].payload.id, ids.interrupt_id); + assert_eq!(sent[0].payload.trait_id, ids.trait_id); + assert_eq!(sent[0].payload.method_id, ids.interrupt_id); assert_eq!( sent[0].payload.value, versioned_interrupt_err_payload(error) @@ -264,13 +273,15 @@ fn foreign_account_proof_returns_not_allowlisted_without_confirmation() { ProtocolMessage { request_id: "p:account-proof".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }, ); assert_eq!(response.request_id, "p:account-proof"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // RFC-0024 forbids a prompt fallback for bearer proofs made with a foreign key. let expected = versioned_result_err_payload(account::HostAccountCreateProofError::V1( v01::HostAccountCreateProofError::NotAllowlisted, @@ -378,7 +389,8 @@ fn malformed_result_subscription_start_interrupts_with_malformed_frame() { ProtocolMessage { request_id: "p:malformed-sub".into(), payload: Payload { - id: ids.start_id, + trait_id: ids.trait_id, + method_id: ids.start_id, value: vec![0xff], }, }, @@ -388,7 +400,8 @@ fn malformed_result_subscription_start_interrupts_with_malformed_frame() { let sent = transport.sent.lock().unwrap(); assert_eq!(sent.len(), 1); assert_eq!(sent[0].request_id, "p:malformed-sub"); - assert_eq!(sent[0].payload.id, ids.interrupt_id); + assert_eq!(sent[0].payload.trait_id, ids.trait_id); + assert_eq!(sent[0].payload.method_id, ids.interrupt_id); assert_eq!(sent[0].payload.value.first(), Some(&0x00)); let mut payload = &sent[0].payload.value[1..]; @@ -436,7 +449,8 @@ fn unknown_wire_discriminant_returns_correlated_protocol_error() { let request = ProtocolMessage { request_id: "p:unknown".into(), payload: Payload { - id: 250, + trait_id: 250, + method_id: 249, value: vec![0, 0, 0, 0], }, }; @@ -448,9 +462,13 @@ fn unknown_wire_discriminant_returns_correlated_protocol_error() { ProtocolMessage { request_id: "p:unknown".into(), payload: Payload { - id: PROTOCOL_ERROR_ID, + trait_id: PROTOCOL_ERROR_TRAIT_ID, + method_id: PROTOCOL_ERROR_METHOD_ID, value: VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { - discriminant: 250, + // echoed in arrival order; 250 != 249 so a transposed + // pair cannot pass this assertion + trait_id: 250, + method_id: 249, }) .encode(), }, @@ -476,7 +494,8 @@ fn subscription_start_receive_stop_through_wire_boundary() { let start = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.start_id, + trait_id: ids.trait_id, + method_id: ids.start_id, value: Vec::new(), }, }; @@ -488,14 +507,22 @@ fn subscription_start_receive_stop_through_wire_boundary() { assert!(Instant::now() < deadline, "no initial _receive frame"); std::thread::sleep(Duration::from_millis(10)); } - assert_eq!(transport.sent.lock().unwrap()[0].payload.id, ids.receive_id); + assert_eq!( + transport.sent.lock().unwrap()[0].payload.trait_id, + ids.trait_id + ); + assert_eq!( + transport.sent.lock().unwrap()[0].payload.method_id, + ids.receive_id + ); // Stop the subscription, then push a session change. A live subscription // would emit a Connected `_receive`; a stopped one must stay silent. let stop = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.stop_id, + trait_id: ids.trait_id, + method_id: ids.stop_id, value: Vec::new(), }, }; diff --git a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs index e349139c9..624dcde64 100644 --- a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs +++ b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs @@ -1,6 +1,6 @@ //! Cross-language parity check: the Rust `WIRE_TABLE` and the TS -//! `wire-table.ts` must list the exact same `(method, request_id, response_id)` -//! tuples in the same order. A drift here means a product built against one +//! `wire-table.ts` must list the exact same +//! `(method, trait_id, request_id, response_id)` tuples in the same order. A drift here means a product built against one //! side will fail to decode frames produced by the other. //! //! Both files are auto-generated text artifacts of `truapi-codegen`; the @@ -20,6 +20,7 @@ const RUST_TABLE: &str = include_str!("../src/generated/wire_table.rs"); #[derive(Debug, PartialEq, Eq)] struct Row { method: String, + trait_id: u8, request_or_start: u8, response_or_receive: u8, /// Subscription `_stop` / `_interrupt` ids; `None` for request methods. @@ -59,6 +60,7 @@ fn parse_rust(src: &str) -> Vec { continue; } let method = rest[..colon].trim().to_ascii_lowercase(); + let mut trait_id = None; let mut request_or_start = None; let mut response_or_receive = None; let mut stop = None; @@ -68,6 +70,9 @@ fn parse_rust(src: &str) -> Vec { if t.starts_with("};") { break; } + if let Some(rest) = t.strip_prefix("trait_id: ") { + trait_id = Some(parse_id(rest, &method)); + } if let Some(rest) = t .strip_prefix("request_id: ") .or_else(|| t.strip_prefix("start_id: ")) @@ -89,7 +94,9 @@ fn parse_rust(src: &str) -> Vec { } if let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) { out.push(Row { - method, + method: method.clone(), + trait_id: trait_id + .unwrap_or_else(|| panic!("missing trait_id for `{method}` in Rust table")), request_or_start: rs, response_or_receive: rr, stop, @@ -116,6 +123,7 @@ fn parse_ts(src: &str) -> Vec { continue; }; let method = rest[..name_end].to_ascii_lowercase(); + let mut trait_id = None; let mut request_or_start = None; let mut response_or_receive = None; let mut stop = None; @@ -126,6 +134,9 @@ fn parse_ts(src: &str) -> Vec { if t.starts_with("start:") || t.contains("SubscriptionFrameIds") { is_subscription = true; } + if let Some(rest) = t.strip_prefix("trait: ") { + trait_id = Some(parse_id(rest, &method)); + } if let Some(rest) = t .strip_prefix("request: ") .or_else(|| t.strip_prefix("start: ")) @@ -147,6 +158,9 @@ fn parse_ts(src: &str) -> Vec { if t.starts_with("} as const") || t == "}" { if let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) { out.push(Row { + trait_id: trait_id.unwrap_or_else(|| { + panic!("missing trait id for `{method}` in TS table") + }), method, request_or_start: rs, response_or_receive: rr, diff --git a/rust/crates/truapi/README.md b/rust/crates/truapi/README.md index e47929328..5edb6a7cd 100644 --- a/rust/crates/truapi/README.md +++ b/rust/crates/truapi/README.md @@ -23,7 +23,7 @@ The crate has two layers: 1. **Protocol types** under `v01`. 2. **Unified host contract** under `api`, where each method takes a `CallContext`, a versioned request type, and returns a versioned response with `CallError` or a `Subscription`. -Wire ids are part of the public protocol after F1: existing ids are append-only. Do not renumber or reuse them. The generated Rust dispatcher and the generated TypeScript wire table must stay byte-compatible with deployed products. +Wire ids are part of the public protocol. Every frame carries a two-byte `(trait, method)` discriminant pair: the trait id comes from the trait-level `#[wire_trait(id = N)]` annotation and the method id from the method-level `#[wire(...)]` annotation. Trait ids and existing method ids are append-only **per trait**: never renumber or reuse an id within a trait, and never reassign a trait id. New methods take the next free method ids in their own trait without affecting any other trait. The generated Rust dispatcher and the generated TypeScript wire table must stay byte-compatible with deployed products. ## Key modules diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index f7ee2b758..09f235a19 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -14,10 +14,11 @@ use crate::versioned::account::{ HostGetUserIdRequest, HostGetUserIdResponse, HostRequestLoginError, HostRequestLoginRequest, HostRequestLoginResponse, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// Account lookup, aliasing, and proof generation. +#[wire_trait(id = 1)] #[crate::async_trait] pub trait Account: Send + Sync { /// Subscribe to account connection status changes. @@ -30,7 +31,7 @@ pub trait Account: Send + Sync { /// ); /// console.log("connection status:", status); /// ``` - #[wire(start_id = 18)] + #[wire(start_id = 0)] async fn connection_status_subscribe( &self, _cx: &CallContext, @@ -62,7 +63,7 @@ pub trait Account: Send + Sync { /// assert(otherProduct.isOk(), "cross-product getAccount was denied or failed:", otherProduct); /// console.log("other product account retrieved after approval:", otherProduct.value); /// ``` - #[wire(request_id = 22)] + #[wire(request_id = 4)] async fn get_account( &self, _cx: &CallContext, @@ -106,7 +107,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "getAccountAlias failed:", result); /// console.log("account alias:", result.value); /// ``` - #[wire(request_id = 24)] + #[wire(request_id = 6)] async fn get_account_alias( &self, _cx: &CallContext, @@ -151,7 +152,7 @@ pub trait Account: Send + Sync { /// ); /// console.log("foreign account proof refused without prompting"); /// ``` - #[wire(request_id = 26)] + #[wire(request_id = 8)] async fn create_account_proof( &self, _cx: &CallContext, @@ -185,7 +186,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "signVrf failed:", result); /// console.log("vrf signature:", result.value); /// ``` - #[wire(request_id = 164)] + #[wire(request_id = 16)] async fn sign_vrf( &self, _cx: &CallContext, @@ -282,7 +283,7 @@ pub trait Account: Send + Sync { /// assert(result.value.accounts.length === 0, "unexpected legacy accounts:", result.value); /// console.log("legacy accounts:", result.value.accounts); /// ``` - #[wire(request_id = 28)] + #[wire(request_id = 10)] async fn get_legacy_accounts( &self, _cx: &CallContext, @@ -298,7 +299,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "getUserId failed:", result); /// console.log("user id:", result.value); /// ``` - #[wire(request_id = 110)] + #[wire(request_id = 12)] async fn get_user_id( &self, _cx: &CallContext, @@ -319,7 +320,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "requestLogin failed:", result); /// console.log("login completed:", result.value); /// ``` - #[wire(request_id = 112)] + #[wire(request_id = 14)] async fn request_login( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index f08dbf531..ab2fc54d2 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -19,10 +19,11 @@ use crate::versioned::chain::{ RemoteChainTransactionStopError, RemoteChainTransactionStopRequest, RemoteChainTransactionStopResponse, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// Chain interaction methods. +#[wire_trait(id = 2)] #[crate::async_trait] pub trait Chain: Send + Sync { /// Follow the chain head and receive block events. @@ -45,7 +46,7 @@ pub trait Chain: Send + Sync { /// ); /// console.log("head follow event:", item); /// ``` - #[wire(start_id = 76)] + #[wire(start_id = 0)] async fn follow_head_subscribe( &self, _cx: &CallContext, @@ -74,7 +75,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getHeadHeader failed:", result); /// console.log("block header:", result.value); /// ``` - #[wire(request_id = 80)] + #[wire(request_id = 4)] async fn get_head_header( &self, _cx: &CallContext, @@ -103,7 +104,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getHeadBody failed:", result); /// console.log("block body:", result.value); /// ``` - #[wire(request_id = 82)] + #[wire(request_id = 6)] async fn get_head_body( &self, _cx: &CallContext, @@ -137,7 +138,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getHeadStorage failed:", result); /// console.log("storage value:", result.value); /// ``` - #[wire(request_id = 84)] + #[wire(request_id = 8)] async fn get_head_storage( &self, _cx: &CallContext, @@ -173,7 +174,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "callHead failed:", result); /// console.log("runtime call result:", result.value); /// ``` - #[wire(request_id = 86)] + #[wire(request_id = 10)] async fn call_head( &self, _cx: &CallContext, @@ -206,7 +207,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "unpinHead failed:", result); /// console.log("blocks unpinned"); /// ``` - #[wire(request_id = 88)] + #[wire(request_id = 12)] async fn unpin_head( &self, _cx: &CallContext, @@ -239,7 +240,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "continueHead failed:", result); /// console.log("operation continued"); /// ``` - #[wire(request_id = 90)] + #[wire(request_id = 14)] async fn continue_head( &self, _cx: &CallContext, @@ -272,7 +273,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "stopHeadOperation failed:", result); /// console.log("operation stopped"); /// ``` - #[wire(request_id = 92)] + #[wire(request_id = 16)] async fn stop_head_operation( &self, _cx: &CallContext, @@ -294,7 +295,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getSpecGenesisHash failed:", result); /// console.log("genesis hash:", result.value); /// ``` - #[wire(request_id = 94)] + #[wire(request_id = 18)] async fn get_spec_genesis_hash( &self, _cx: &CallContext, @@ -316,7 +317,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getSpecChainName failed:", result); /// console.log("chain name:", result.value); /// ``` - #[wire(request_id = 96)] + #[wire(request_id = 20)] async fn get_spec_chain_name( &self, _cx: &CallContext, @@ -337,7 +338,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getSpecProperties failed:", result); /// console.log("chain properties:", result.value); /// ``` - #[wire(request_id = 98)] + #[wire(request_id = 22)] async fn get_spec_properties( &self, _cx: &CallContext, @@ -359,7 +360,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "broadcastTransaction failed:", result); /// console.log("transaction broadcast:", result.value); /// ``` - #[wire(request_id = 100)] + #[wire(request_id = 24)] async fn broadcast_transaction( &self, _cx: &CallContext, @@ -394,7 +395,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "stopTransaction failed:", result); /// console.log("transaction broadcast stopped"); /// ``` - #[wire(request_id = 102)] + #[wire(request_id = 26)] async fn stop_transaction( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/chat.rs b/rust/crates/truapi/src/api/chat.rs index 3af67cef2..74e50157e 100644 --- a/rust/crates/truapi/src/api/chat.rs +++ b/rust/crates/truapi/src/api/chat.rs @@ -7,10 +7,11 @@ use crate::versioned::chat::{ HostChatRegisterBotRequest, HostChatRegisterBotResponse, ProductChatCustomMessageRenderItem, ProductChatCustomMessageRenderRequest, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// Chat room, bot, and message APIs. +#[wire_trait(id = 3)] #[crate::service(required_execution = Worker)] #[crate::async_trait] pub trait Chat: Send + Sync { @@ -25,7 +26,7 @@ pub trait Chat: Send + Sync { /// assert(result.isOk(), "createRoom failed:", result); /// console.log("room created:", result.value); /// ``` - #[wire(request_id = 38)] + #[wire(request_id = 0)] async fn create_room( &self, _cx: &CallContext, @@ -45,7 +46,7 @@ pub trait Chat: Send + Sync { /// assert(result.isOk(), "registerBot failed:", result); /// console.log("bot registered:", result.value); /// ``` - #[wire(request_id = 40)] + #[wire(request_id = 2)] async fn register_bot( &self, _cx: &CallContext, @@ -64,7 +65,7 @@ pub trait Chat: Send + Sync { /// ); /// console.log("room list received:", item); /// ``` - #[wire(start_id = 42)] + #[wire(start_id = 4)] async fn list_subscribe(&self, _cx: &CallContext) -> Subscription { Subscription::empty() } @@ -91,7 +92,7 @@ pub trait Chat: Send + Sync { /// assert(result.isOk(), "postMessage failed:", result); /// console.log("message posted:", result.value); /// ``` - #[wire(request_id = 46)] + #[wire(request_id = 8)] async fn post_message( &self, _cx: &CallContext, @@ -110,7 +111,7 @@ pub trait Chat: Send + Sync { /// ); /// console.log("action received:", item); /// ``` - #[wire(start_id = 48)] + #[wire(start_id = 10)] async fn action_subscribe( &self, _cx: &CallContext, @@ -126,7 +127,7 @@ pub trait Chat: Send + Sync { /// return of({ tag: "String", value: { text: `${messageType}: ${payload}` } }); /// }); /// ``` - #[wire(host_initiated, start_id = 52)] + #[wire(host_initiated, start_id = 14)] fn custom_message_render( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/coin_payment.rs b/rust/crates/truapi/src/api/coin_payment.rs index 5839b8e3c..e33592c67 100644 --- a/rust/crates/truapi/src/api/coin_payment.rs +++ b/rust/crates/truapi/src/api/coin_payment.rs @@ -14,14 +14,15 @@ use crate::versioned::coin_payment::{ HostCoinPaymentRebalancePurseRequest, HostCoinPaymentRefundError, HostCoinPaymentRefundItem, HostCoinPaymentRefundRequest, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// CoinPayment operations. /// /// RFC 0017 describes `Resolvable` values for long-running operations. /// TrUAPI represents those as subscriptions whose items are the RFC status /// updates. +#[wire_trait(id = 4)] #[crate::async_trait] pub trait CoinPayment: Send + Sync { /// Create a new firewalled CoinPayment purse. @@ -33,7 +34,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "createPurse failed:", result); /// console.log("purse created:", result.value.purse); /// ``` - #[wire(request_id = 136)] + #[wire(request_id = 0)] async fn create_purse( &self, _cx: &CallContext, @@ -50,7 +51,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "queryPurse failed:", result); /// console.log("purse info:", result.value.info); /// ``` - #[wire(request_id = 138)] + #[wire(request_id = 2)] async fn query_purse( &self, _cx: &CallContext, @@ -73,7 +74,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("rebalance status:", status); /// ``` - #[wire(start_id = 140)] + #[wire(start_id = 4)] async fn rebalance_purse( &self, _cx: &CallContext, @@ -99,7 +100,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("delete status:", status); /// ``` - #[wire(start_id = 144)] + #[wire(start_id = 8)] async fn delete_purse( &self, _cx: &CallContext, @@ -118,7 +119,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "createReceivable failed:", result); /// console.log("receivable created:", result.value.receivable); /// ``` - #[wire(request_id = 148)] + #[wire(request_id = 12)] async fn create_receivable( &self, _cx: &CallContext, @@ -141,7 +142,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "createCheque failed:", result); /// console.log("cheque created:", result.value.cheque); /// ``` - #[wire(request_id = 150)] + #[wire(request_id = 14)] async fn create_cheque( &self, _cx: &CallContext, @@ -168,7 +169,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("deposit status:", status); /// ``` - #[wire(start_id = 152)] + #[wire(start_id = 16)] async fn deposit( &self, _cx: &CallContext, @@ -195,7 +196,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("refund status:", status); /// ``` - #[wire(start_id = 156)] + #[wire(start_id = 20)] async fn refund( &self, _cx: &CallContext, @@ -222,7 +223,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("payment received:", item); /// ``` - #[wire(start_id = 160)] + #[wire(start_id = 24)] async fn listen_for_payment( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/entropy.rs b/rust/crates/truapi/src/api/entropy.rs index 32f510b9b..cbad2ca66 100644 --- a/rust/crates/truapi/src/api/entropy.rs +++ b/rust/crates/truapi/src/api/entropy.rs @@ -3,10 +3,11 @@ use crate::versioned::entropy::{ HostDeriveEntropyError, HostDeriveEntropyRequest, HostDeriveEntropyResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// Deterministic entropy derivation. +#[wire_trait(id = 5)] #[crate::async_trait] pub trait Entropy: Send + Sync { /// Derive deterministic entropy. @@ -18,7 +19,7 @@ pub trait Entropy: Send + Sync { /// assert(result.isOk(), "derive failed:", result); /// console.log("entropy derived:", result.value); /// ``` - #[wire(request_id = 108)] + #[wire(request_id = 0)] async fn derive( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/local_storage.rs b/rust/crates/truapi/src/api/local_storage.rs index ec0bc6343..1f1dc92a4 100644 --- a/rust/crates/truapi/src/api/local_storage.rs +++ b/rust/crates/truapi/src/api/local_storage.rs @@ -5,10 +5,11 @@ use crate::versioned::local_storage::{ HostLocalStorageReadError, HostLocalStorageReadRequest, HostLocalStorageReadResponse, HostLocalStorageWriteError, HostLocalStorageWriteRequest, HostLocalStorageWriteResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// Local key/value storage scoped to the calling product. +#[wire_trait(id = 6)] #[crate::async_trait] pub trait LocalStorage: Send + Sync { /// Read a value by key. @@ -18,7 +19,7 @@ pub trait LocalStorage: Send + Sync { /// assert(result.isOk(), "read failed:", result); /// console.log("storage value read:", result.value.value); /// ``` - #[wire(request_id = 12)] + #[wire(request_id = 0)] async fn read( &self, cx: &CallContext, @@ -35,7 +36,7 @@ pub trait LocalStorage: Send + Sync { /// assert(result.isOk(), "write failed:", result); /// console.log("storage write succeeded"); /// ``` - #[wire(request_id = 14)] + #[wire(request_id = 2)] async fn write( &self, cx: &CallContext, @@ -49,7 +50,7 @@ pub trait LocalStorage: Send + Sync { /// assert(result.isOk(), "clear failed:", result); /// console.log("storage clear succeeded"); /// ``` - #[wire(request_id = 16)] + #[wire(request_id = 4)] async fn clear( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/notifications.rs b/rust/crates/truapi/src/api/notifications.rs index 559938a84..3701afeb3 100644 --- a/rust/crates/truapi/src/api/notifications.rs +++ b/rust/crates/truapi/src/api/notifications.rs @@ -5,10 +5,11 @@ use crate::versioned::notifications::{ HostPushNotificationCancelResponse, HostPushNotificationError, HostPushNotificationRequest, HostPushNotificationResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// Notification methods for locally-rendered push notifications. +#[wire_trait(id = 7)] #[crate::async_trait] pub trait Notifications: Send + Sync { /// Send a push notification to the user. @@ -28,7 +29,7 @@ pub trait Notifications: Send + Sync { /// assert(result.isOk(), "sendPushNotification failed:", result); /// console.log("notification sent:", result.value); /// ``` - #[wire(request_id = 4)] + #[wire(request_id = 0)] async fn send_push_notification( &self, cx: &CallContext, @@ -49,7 +50,7 @@ pub trait Notifications: Send + Sync { /// assert(result.isOk(), "cancelPushNotification failed:", result); /// console.log("notification cancelled"); /// ``` - #[wire(request_id = 134)] + #[wire(request_id = 2)] async fn cancel_push_notification( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/payment.rs b/rust/crates/truapi/src/api/payment.rs index eab781c5f..31b957c47 100644 --- a/rust/crates/truapi/src/api/payment.rs +++ b/rust/crates/truapi/src/api/payment.rs @@ -7,10 +7,11 @@ use crate::versioned::payment::{ HostPaymentStatusSubscribeRequest, HostPaymentTopUpError, HostPaymentTopUpRequest, HostPaymentTopUpResponse, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// Payment request and balance/status subscription methods. +#[wire_trait(id = 8)] #[crate::async_trait] pub trait Payment: Send + Sync { /// Subscribe to payment balance updates. @@ -23,7 +24,7 @@ pub trait Payment: Send + Sync { /// ); /// console.log("balance received:", balance); /// ``` - #[wire(start_id = 118)] + #[wire(start_id = 0)] async fn balance_subscribe( &self, _cx: &CallContext, @@ -53,7 +54,7 @@ pub trait Payment: Send + Sync { /// assert(result.isOk(), "request failed:", result); /// console.log("payment requested:", result.value); /// ``` - #[wire(request_id = 124)] + #[wire(request_id = 6)] async fn request( &self, _cx: &CallContext, @@ -90,7 +91,7 @@ pub trait Payment: Send + Sync { /// ); /// console.log("payment status received:", status); /// ``` - #[wire(start_id = 126)] + #[wire(start_id = 8)] async fn status_subscribe( &self, _cx: &CallContext, @@ -112,7 +113,7 @@ pub trait Payment: Send + Sync { /// assert(result.isOk(), "topUp failed:", result); /// console.log("balance topped up"); /// ``` - #[wire(request_id = 122)] + #[wire(request_id = 4)] async fn top_up( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/permissions.rs b/rust/crates/truapi/src/api/permissions.rs index a190984d9..d03c64101 100644 --- a/rust/crates/truapi/src/api/permissions.rs +++ b/rust/crates/truapi/src/api/permissions.rs @@ -4,10 +4,11 @@ use crate::versioned::permissions::{ HostDevicePermissionError, HostDevicePermissionRequest, HostDevicePermissionResponse, RemotePermissionError, RemotePermissionRequest, RemotePermissionResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// Permission request methods. +#[wire_trait(id = 9)] #[crate::async_trait] pub trait Permissions: Send + Sync { /// Request a device-capability permission from the user. @@ -17,7 +18,7 @@ pub trait Permissions: Send + Sync { /// assert(result.isOk(), "requestDevicePermission failed:", result); /// console.log("device permission result:", result.value); /// ``` - #[wire(request_id = 8)] + #[wire(request_id = 0)] async fn request_device_permission( &self, cx: &CallContext, @@ -33,7 +34,7 @@ pub trait Permissions: Send + Sync { /// assert(result.isOk(), "requestRemotePermission failed:", result); /// console.log("remote permission result:", result.value); /// ``` - #[wire(request_id = 10)] + #[wire(request_id = 2)] async fn request_remote_permission( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/preimage.rs b/rust/crates/truapi/src/api/preimage.rs index 5037f8cb3..9a6b4c567 100644 --- a/rust/crates/truapi/src/api/preimage.rs +++ b/rust/crates/truapi/src/api/preimage.rs @@ -4,10 +4,11 @@ use crate::versioned::preimage::{ RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, RemotePreimageSubmitError, RemotePreimageSubmitRequest, RemotePreimageSubmitResponse, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// Preimage lookup and submission methods. +#[wire_trait(id = 10)] #[crate::async_trait] pub trait Preimage: Send + Sync { /// Subscribe to preimage lookups for a given key. @@ -26,7 +27,7 @@ pub trait Preimage: Send + Sync { /// assert(item.value === value, "preimage lookup returned the wrong value:", item); /// console.log("preimage lookup received:", item); /// ``` - #[wire(start_id = 64)] + #[wire(start_id = 0)] async fn lookup_subscribe( &self, _cx: &CallContext, @@ -43,7 +44,7 @@ pub trait Preimage: Send + Sync { /// assert(result.isOk(), "submit failed:", result); /// console.log("preimage submitted:", result.value); /// ``` - #[wire(request_id = 68)] + #[wire(request_id = 4)] async fn submit( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/resource_allocation.rs b/rust/crates/truapi/src/api/resource_allocation.rs index 3f9f90b0f..500ac9840 100644 --- a/rust/crates/truapi/src/api/resource_allocation.rs +++ b/rust/crates/truapi/src/api/resource_allocation.rs @@ -4,10 +4,11 @@ use crate::versioned::resource_allocation::{ HostRequestResourceAllocationError, HostRequestResourceAllocationRequest, HostRequestResourceAllocationResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// Resource pre-allocation (allowance management). +#[wire_trait(id = 11)] #[crate::async_trait] pub trait ResourceAllocation: Send + Sync { /// Request the host to pre-allocate one or more resources. @@ -38,7 +39,7 @@ pub trait ResourceAllocation: Send + Sync { /// ); /// console.log("resource allocation outcomes:", result.value.outcomes); /// ``` - #[wire(request_id = 130)] + #[wire(request_id = 0)] async fn request( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 010d39a31..8acb4f799 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -12,10 +12,11 @@ use crate::versioned::signing::{ HostSignRawResponse, HostSignRawWithLegacyAccountError, HostSignRawWithLegacyAccountRequest, HostSignRawWithLegacyAccountResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// Signing operations. +#[wire_trait(id = 12)] #[crate::async_trait] pub trait Signing: Send + Sync { /// Construct a transaction for a product account. @@ -61,7 +62,7 @@ pub trait Signing: Send + Sync { /// console.log(`${version} transaction created:`, result.value); /// } /// ``` - #[wire(request_id = 30)] + #[wire(request_id = 0)] async fn create_transaction( &self, _cx: &CallContext, @@ -118,7 +119,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "createTransactionWithLegacyAccount failed:", result); /// console.log("transaction created:", result.value); /// ``` - #[wire(request_id = 32)] + #[wire(request_id = 2)] async fn create_transaction_with_legacy_account( &self, _cx: &CallContext, @@ -150,7 +151,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signRawWithLegacyAccount failed:", result); /// console.log("raw bytes signed:", result.value); /// ``` - #[wire(request_id = 34)] + #[wire(request_id = 4)] async fn sign_raw_with_legacy_account( &self, _cx: &CallContext, @@ -196,7 +197,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signPayloadWithLegacyAccount failed:", result); /// console.log("payload signed:", result.value); /// ``` - #[wire(request_id = 36)] + #[wire(request_id = 6)] async fn sign_payload_with_legacy_account( &self, _cx: &CallContext, @@ -226,7 +227,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signRaw failed:", result); /// console.log("raw bytes signed:", result.value); /// ``` - #[wire(request_id = 114)] + #[wire(request_id = 8)] async fn sign_raw( &self, _cx: &CallContext, @@ -263,7 +264,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signPayload failed:", result); /// console.log("payload signed:", result.value); /// ``` - #[wire(request_id = 116)] + #[wire(request_id = 10)] async fn sign_payload( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index ae8bdbef8..7554806e1 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -9,10 +9,11 @@ use crate::versioned::statement_store::{ RemoteStatementStoreSubscribeError, RemoteStatementStoreSubscribeItem, RemoteStatementStoreSubscribeRequest, }; -use crate::wire; use crate::{CallContext, CallError, Subscription}; +use crate::{wire, wire_trait}; /// Statement store methods. +#[wire_trait(id = 13)] #[crate::async_trait] pub trait StatementStore: Send + Sync { /// Subscribe to statements matching a topic filter. @@ -57,7 +58,7 @@ pub trait StatementStore: Send + Sync { /// const page = await waitForStatement(); /// console.log("subscribe received", page); /// ``` - #[wire(start_id = 56)] + #[wire(start_id = 0)] async fn subscribe( &self, _cx: &CallContext, @@ -99,7 +100,7 @@ pub trait StatementStore: Send + Sync { /// console.log("proof created:", result.value); /// } /// ``` - #[wire(request_id = 60)] + #[wire(request_id = 4)] async fn create_proof( &self, _cx: &CallContext, @@ -126,7 +127,7 @@ pub trait StatementStore: Send + Sync { /// assert(result.isOk(), "createProof failed:", result); /// console.log("proof created:", result.value); /// ``` - #[wire(request_id = 132)] + #[wire(request_id = 8)] async fn create_proof_authorized( &self, _cx: &CallContext, @@ -158,7 +159,7 @@ pub trait StatementStore: Send + Sync { /// assert(result.isOk(), "submit failed:", result); /// console.log("statement submitted"); /// ``` - #[wire(request_id = 62)] + #[wire(request_id = 6)] async fn submit( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 741e91f81..969ef3252 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -7,11 +7,12 @@ use crate::versioned::system::{ HostInfoRequest, HostInfoResponse, HostNavigateToError, HostNavigateToRequest, HostNavigateToResponse, }; -use crate::wire; use crate::{CallContext, CallError}; +use crate::{wire, wire_trait}; /// General-purpose TrUAPI methods for handshake, feature detection, /// navigation, and runtime information. +#[wire_trait(id = 0)] #[crate::async_trait] pub trait System: Send + Sync { /// Negotiate the wire codec version with the product. @@ -28,7 +29,7 @@ pub trait System: Send + Sync { request: HostHandshakeRequest, ) -> Result> { let HostHandshakeRequest::V1(version) = request; - if version.codec_version == 1 { + if version.codec_version == 2 { Ok(HostHandshakeResponse::V1) } else { Err(CallError::Domain(HostHandshakeError::V1( @@ -75,7 +76,7 @@ pub trait System: Send + Sync { /// assert(result.isOk(), "navigateTo failed:", result); /// console.log("navigation succeeded"); /// ``` - #[wire(request_id = 6)] + #[wire(request_id = 4)] async fn navigate_to( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/theme.rs b/rust/crates/truapi/src/api/theme.rs index 7fcbad818..2d323f9db 100644 --- a/rust/crates/truapi/src/api/theme.rs +++ b/rust/crates/truapi/src/api/theme.rs @@ -1,10 +1,11 @@ //! Unified [`Theme`] trait. use crate::versioned::theme::HostThemeSubscribeItem; -use crate::wire; use crate::{CallContext, Subscription}; +use crate::{wire, wire_trait}; /// Host theme subscription. +#[wire_trait(id = 14)] #[crate::async_trait] pub trait Theme: Send + Sync { /// Subscribe to host theme changes. @@ -17,7 +18,7 @@ pub trait Theme: Send + Sync { /// ); /// console.log("theme received:", theme); /// ``` - #[wire(start_id = 104)] + #[wire(start_id = 0)] async fn subscribe(&self, _cx: &CallContext) -> Subscription { Subscription::empty() } diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index 06d3557f4..dac95d4f2 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -199,7 +199,7 @@ pub mod latest { pub type RemotePermissionResponse = LatestOf; } -pub use truapi_macros::{service, wire}; +pub use truapi_macros::{service, wire, wire_trait}; /// Per-message id carried from the transport frame. pub type RequestId = String; diff --git a/scripts/codegen.sh b/scripts/codegen.sh index 4221535fe..94854f45c 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -12,7 +12,7 @@ # --platform-ts-output js/packages/truapi-host/src/generated # --platform-wasm-adapter-output js/packages/truapi-host/src/generated # --platform-rust-output rust/crates/truapi-server/src/wasm -# --codec-version 1 +# --codec-version 2 # # The client surface defaults to the latest wire version any versioned # wrapper exposes; pass `--client-version V` to pin to an older one. @@ -43,7 +43,7 @@ cargo run -p truapi-codegen -- \ --platform-wasm-adapter-output js/packages/truapi-host/src/generated \ --platform-rust-output rust/crates/truapi-server/src/wasm \ --explorer-output js/packages/truapi/src/explorer \ - --codec-version 1 + --codec-version 2 rustfmt +"$NIGHTLY_TOOLCHAIN" --edition 2024 \ rust/crates/truapi-server/src/generated/dispatcher.rs \ From 4458b32308ba110beba296614d78493b80795d3c Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 7 Aug 2026 13:06:09 +0530 Subject: [PATCH 02/16] fix(wire): lift trait ids clear of the codec 1 method range --- .changeset/wire-trait-method-split.md | 14 + .claude/skills/e2e-dotli/SKILL.md | 2 +- CLAUDE.md | 4 +- CONTRIBUTING.md | 2 +- README.md | 6 +- docs/design/truapi-protocol.md | 356 +++++++++--------- docs/local-e2e-testing.md | 2 +- .../Tests/TrUAPIWsBridgeTests.swift | 6 +- js/packages/truapi/README.md | 8 +- js/packages/truapi/src/client.test.ts | 80 +++- js/packages/truapi/src/client.ts | 113 ++++-- js/packages/truapi/src/wire-equality.test.ts | 25 +- rust/crates/truapi-codegen/README.md | 4 +- rust/crates/truapi-codegen/src/main.rs | 2 +- rust/crates/truapi-codegen/src/rust.rs | 79 ++-- .../truapi-codegen/src/rust/wire_table.rs | 19 +- rust/crates/truapi-codegen/src/rustdoc.rs | 82 +++- rust/crates/truapi-codegen/src/ts.rs | 60 +-- rust/crates/truapi-server/README.md | 19 +- rust/crates/truapi-server/src/core.rs | 15 +- rust/crates/truapi-server/src/dispatcher.rs | 18 + rust/crates/truapi-server/src/frame.rs | 41 +- rust/crates/truapi-server/src/logging.rs | 54 ++- rust/crates/truapi-server/src/ws_bridge.rs | 9 +- .../truapi-server/tests/golden_frame.rs | 2 +- .../tests/snapshots/golden-account-get.bin | Bin 15 -> 15 bytes rust/crates/truapi/README.md | 2 +- rust/crates/truapi/src/api/account.rs | 2 +- rust/crates/truapi/src/api/chain.rs | 2 +- rust/crates/truapi/src/api/chat.rs | 2 +- rust/crates/truapi/src/api/coin_payment.rs | 2 +- rust/crates/truapi/src/api/entropy.rs | 2 +- rust/crates/truapi/src/api/local_storage.rs | 2 +- rust/crates/truapi/src/api/notifications.rs | 2 +- rust/crates/truapi/src/api/payment.rs | 2 +- rust/crates/truapi/src/api/permissions.rs | 2 +- rust/crates/truapi/src/api/preimage.rs | 2 +- .../truapi/src/api/resource_allocation.rs | 2 +- rust/crates/truapi/src/api/signing.rs | 2 +- rust/crates/truapi/src/api/statement_store.rs | 2 +- rust/crates/truapi/src/api/system.rs | 4 +- rust/crates/truapi/src/api/theme.rs | 2 +- rust/crates/truapi/src/lib.rs | 20 + scripts/snapshot-version.sh | 2 +- 44 files changed, 732 insertions(+), 346 deletions(-) create mode 100644 .changeset/wire-trait-method-split.md diff --git a/.changeset/wire-trait-method-split.md b/.changeset/wire-trait-method-split.md new file mode 100644 index 000000000..e8ff10ed6 --- /dev/null +++ b/.changeset/wire-trait-method-split.md @@ -0,0 +1,14 @@ +--- +"@parity/truapi": minor +"@parity/truapi-host": minor +--- + +Address every frame with a two-byte `(trait, method)` wire discriminant. The +trait byte names the API trait and the method byte addresses a method within +it, so each trait owns a full 256-slot method space and method ids restart at +0 in every trait. + +This is wire codec version 2. A codec version 1 peer cannot exchange frames +with a codec version 2 peer in either direction: the handshake itself rides +the changed envelope, so the mismatch cannot be negotiated in band. Hosts and +products must move together. diff --git a/.claude/skills/e2e-dotli/SKILL.md b/.claude/skills/e2e-dotli/SKILL.md index bce9f6e48..80671baf5 100644 --- a/.claude/skills/e2e-dotli/SKILL.md +++ b/.claude/skills/e2e-dotli/SKILL.md @@ -80,7 +80,7 @@ uses the iframe `postMessage` provider. - Connection chip stays on **Handshaking** → handshake is failing. Check: - The dotli console for `Unknown wire tag` / - `Unknown wire discriminant` — wire-table mismatch between dotli's + `unknown wire discriminant pair` — wire-table mismatch between dotli's vendored `@parity/truapi` and the just-built one. - The playground console for `decodeWireMessage` errors — the inbound frame's discriminant is unknown (the playground's diff --git a/CLAUDE.md b/CLAUDE.md index d82e81527..125415575 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ This repo is the single source of truth for the TrUAPI protocol. It vendors `dot rust/crates/ truapi/ Rust trait + type definitions for protocol versions v0.1 and v0.2 (canonical) truapi-codegen/ rustdoc JSON → TypeScript client + Rust dispatcher - truapi-macros/ #[wire(id = N)] proc-macro + truapi-macros/ #[wire_trait(id = N)] + #[wire(...)] proc-macros truapi-platform/ Host syscall traits (storage, navigation, consent, ...) truapi-provider/ network provider backends (WebSocket RPC or smoldot light-client) truapi-server/ Rust runtime hosts implement; ships as WASM (browser/node) @@ -306,7 +306,7 @@ __truapi.setLogLevel("debug"); sessionStorage.setItem("dotli:truapi-debug", "1"); ``` -Reload after setting the debug-panel flag. Watch for `Unknown wire discriminant`, missing +Reload after setting the debug-panel flag. Watch for `unknown wire discriminant pair`, missing `@parity/truapi-host` imports, worker WASM instantiation failures, and debug-panel traffic disappearing when the login popup opens. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65c296a0..b42988ba2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Canonical design documentation lives in `docs/design/`. To propose updates or ad rust/crates/ truapi/ Rust trait + type definitions (source of truth) truapi-codegen/ rustdoc JSON → TypeScript client generator - truapi-macros/ #[wire(id = N)] proc-macro + truapi-macros/ #[wire_trait(id = N)] + #[wire(...)] proc-macros js/packages/ truapi/ @parity/truapi TypeScript package (generated TS is auto-generated and git-ignored) playground/ Next.js interactive playground diff --git a/README.md b/README.md index 06282047d..e4de8d6a5 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ See [`js/packages/truapi/README.md`](js/packages/truapi/README.md) for the full rust/crates/ truapi/ Rust traits, versioned envelopes, and latest payload re-exports truapi-codegen/ rustdoc JSON to TypeScript client + Rust dispatcher - truapi-macros/ #[wire(id = N)] proc-macro + truapi-macros/ #[wire_trait(id = N)] + #[wire(...)] proc-macros truapi-platform/ Host syscall traits used by truapi-server (storage, navigation, consent, ...) truapi-provider/ Network provider backends (WebSocket RPC or smoldot light-client) truapi-server/ Host runtime: dispatcher, typed SCALE logic, chain signing, WASM surface @@ -138,12 +138,12 @@ dependency on the crate: ## How it works -1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each method tagged `#[wire(id = N)]` for a stable byte-level dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. +1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each trait tagged `#[wire_trait(id = N)]` and each method tagged `#[wire(request_id = N)]` for a stable byte-level `(trait, method)` dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. 2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`. 3. Higher-level SDKs wrap the typed client; the transport encodes SCALE frames and ships them over `MessagePort` (or `postMessage` in iframe mode) to the host. 4. The host decodes the frame, dispatches to the matching trait method, encodes the response, and ships it back. -Wire ids are append-only: existing ids never change, so deployed products stay compatible across protocol revisions. Discriminant 255 is permanently reserved for a correlated protocol error, allowing either peer to reject API messages introduced after it was released instead of leaving the caller pending. +Wire ids are append-only per trait: a trait id is never reassigned and a method id is never renumbered or reused within its trait, so deployed products stay compatible across protocol revisions. New methods take the next free method ids in their own trait and leave every other trait untouched. Trait 255 is permanently reserved for a correlated protocol error, allowing either peer to reject API messages introduced after it was released instead of leaving the caller pending. ## Develop diff --git a/docs/design/truapi-protocol.md b/docs/design/truapi-protocol.md index 691945d46..791383ceb 100644 --- a/docs/design/truapi-protocol.md +++ b/docs/design/truapi-protocol.md @@ -174,187 +174,191 @@ The concrete handshake request, response, and error types are defined in the `tr Codec version 1 used a single flat `u8` discriminant shared across all traits. Codec version 2 replaces it with the `(trait, method)` pair. This table is the one-time mapping between the two numberings; it exists only to interpret captured codec-1 traffic and old fixtures, and is never extended — new methods only ever get codec-2 pairs. -Trait id assignment: +Trait id assignment. Ids start at 192 (`truapi::MIN_TRAIT_ID`): no codec-1 +implementation allocated a flat discriminant above 171, so no codec-1 frame's +first byte can name a codec-2 trait, and such a frame is reported as unroutable instead of decoding +into whichever trait would otherwise share its old id. Codegen rejects any +`#[wire_trait(id = N)]` below the floor. | Trait | Trait id | | --- | --- | -| `System` | 0 | -| `Account` | 1 | -| `Chain` | 2 | -| `Chat` | 3 | -| `CoinPayment` | 4 | -| `Entropy` | 5 | -| `LocalStorage` | 6 | -| `Notifications` | 7 | -| `Payment` | 8 | -| `Permissions` | 9 | -| `Preimage` | 10 | -| `ResourceAllocation` | 11 | -| `Signing` | 12 | -| `StatementStore` | 13 | -| `Theme` | 14 | +| `System` | 192 | +| `Account` | 193 | +| `Chain` | 194 | +| `Chat` | 195 | +| `CoinPayment` | 196 | +| `Entropy` | 197 | +| `LocalStorage` | 198 | +| `Notifications` | 199 | +| `Payment` | 200 | +| `Permissions` | 201 | +| `Preimage` | 202 | +| `ResourceAllocation` | 203 | +| `Signing` | 204 | +| `StatementStore` | 205 | +| `Theme` | 206 | Per-action mapping (codec-1 flat id → codec-2 `(trait, method)` pair): | Action | Codec-1 id | Codec-2 (trait, method) | | --- | --- | --- | -| `system_handshake_request` | 0 | (0, 0) | -| `system_handshake_response` | 1 | (0, 1) | -| `system_feature_supported_request` | 2 | (0, 2) | -| `system_feature_supported_response` | 3 | (0, 3) | -| `system_navigate_to_request` | 6 | (0, 4) | -| `system_navigate_to_response` | 7 | (0, 5) | -| `account_connection_status_subscribe_start` | 18 | (1, 0) | -| `account_connection_status_subscribe_stop` | 19 | (1, 1) | -| `account_connection_status_subscribe_interrupt` | 20 | (1, 2) | -| `account_connection_status_subscribe_receive` | 21 | (1, 3) | -| `account_get_account_request` | 22 | (1, 4) | -| `account_get_account_response` | 23 | (1, 5) | -| `account_get_account_alias_request` | 24 | (1, 6) | -| `account_get_account_alias_response` | 25 | (1, 7) | -| `account_create_account_proof_request` | 26 | (1, 8) | -| `account_create_account_proof_response` | 27 | (1, 9) | -| `account_get_legacy_accounts_request` | 28 | (1, 10) | -| `account_get_legacy_accounts_response` | 29 | (1, 11) | -| `account_get_user_id_request` | 110 | (1, 12) | -| `account_get_user_id_response` | 111 | (1, 13) | -| `account_request_login_request` | 112 | (1, 14) | -| `account_request_login_response` | 113 | (1, 15) | -| `account_sign_vrf_request` | 164 | (1, 16) | -| `account_sign_vrf_response` | 165 | (1, 17) | -| `chain_follow_head_subscribe_start` | 76 | (2, 0) | -| `chain_follow_head_subscribe_stop` | 77 | (2, 1) | -| `chain_follow_head_subscribe_interrupt` | 78 | (2, 2) | -| `chain_follow_head_subscribe_receive` | 79 | (2, 3) | -| `chain_get_head_header_request` | 80 | (2, 4) | -| `chain_get_head_header_response` | 81 | (2, 5) | -| `chain_get_head_body_request` | 82 | (2, 6) | -| `chain_get_head_body_response` | 83 | (2, 7) | -| `chain_get_head_storage_request` | 84 | (2, 8) | -| `chain_get_head_storage_response` | 85 | (2, 9) | -| `chain_call_head_request` | 86 | (2, 10) | -| `chain_call_head_response` | 87 | (2, 11) | -| `chain_unpin_head_request` | 88 | (2, 12) | -| `chain_unpin_head_response` | 89 | (2, 13) | -| `chain_continue_head_request` | 90 | (2, 14) | -| `chain_continue_head_response` | 91 | (2, 15) | -| `chain_stop_head_operation_request` | 92 | (2, 16) | -| `chain_stop_head_operation_response` | 93 | (2, 17) | -| `chain_get_spec_genesis_hash_request` | 94 | (2, 18) | -| `chain_get_spec_genesis_hash_response` | 95 | (2, 19) | -| `chain_get_spec_chain_name_request` | 96 | (2, 20) | -| `chain_get_spec_chain_name_response` | 97 | (2, 21) | -| `chain_get_spec_properties_request` | 98 | (2, 22) | -| `chain_get_spec_properties_response` | 99 | (2, 23) | -| `chain_broadcast_transaction_request` | 100 | (2, 24) | -| `chain_broadcast_transaction_response` | 101 | (2, 25) | -| `chain_stop_transaction_request` | 102 | (2, 26) | -| `chain_stop_transaction_response` | 103 | (2, 27) | -| `chat_create_room_request` | 38 | (3, 0) | -| `chat_create_room_response` | 39 | (3, 1) | -| `chat_register_bot_request` | 40 | (3, 2) | -| `chat_register_bot_response` | 41 | (3, 3) | -| `chat_list_subscribe_start` | 42 | (3, 4) | -| `chat_list_subscribe_stop` | 43 | (3, 5) | -| `chat_list_subscribe_interrupt` | 44 | (3, 6) | -| `chat_list_subscribe_receive` | 45 | (3, 7) | -| `chat_post_message_request` | 46 | (3, 8) | -| `chat_post_message_response` | 47 | (3, 9) | -| `chat_action_subscribe_start` | 48 | (3, 10) | -| `chat_action_subscribe_stop` | 49 | (3, 11) | -| `chat_action_subscribe_interrupt` | 50 | (3, 12) | -| `chat_action_subscribe_receive` | 51 | (3, 13) | -| `chat_custom_message_render_subscribe_start` | 52 | (3, 14) | -| `chat_custom_message_render_subscribe_stop` | 53 | (3, 15) | -| `chat_custom_message_render_subscribe_interrupt` | 54 | (3, 16) | -| `chat_custom_message_render_subscribe_receive` | 55 | (3, 17) | -| `coin_payment_create_purse_request` | 136 | (4, 0) | -| `coin_payment_create_purse_response` | 137 | (4, 1) | -| `coin_payment_query_purse_request` | 138 | (4, 2) | -| `coin_payment_query_purse_response` | 139 | (4, 3) | -| `coin_payment_rebalance_purse_start` | 140 | (4, 4) | -| `coin_payment_rebalance_purse_stop` | 141 | (4, 5) | -| `coin_payment_rebalance_purse_interrupt` | 142 | (4, 6) | -| `coin_payment_rebalance_purse_receive` | 143 | (4, 7) | -| `coin_payment_delete_purse_start` | 144 | (4, 8) | -| `coin_payment_delete_purse_stop` | 145 | (4, 9) | -| `coin_payment_delete_purse_interrupt` | 146 | (4, 10) | -| `coin_payment_delete_purse_receive` | 147 | (4, 11) | -| `coin_payment_create_receivable_request` | 148 | (4, 12) | -| `coin_payment_create_receivable_response` | 149 | (4, 13) | -| `coin_payment_create_cheque_request` | 150 | (4, 14) | -| `coin_payment_create_cheque_response` | 151 | (4, 15) | -| `coin_payment_deposit_start` | 152 | (4, 16) | -| `coin_payment_deposit_stop` | 153 | (4, 17) | -| `coin_payment_deposit_interrupt` | 154 | (4, 18) | -| `coin_payment_deposit_receive` | 155 | (4, 19) | -| `coin_payment_refund_start` | 156 | (4, 20) | -| `coin_payment_refund_stop` | 157 | (4, 21) | -| `coin_payment_refund_interrupt` | 158 | (4, 22) | -| `coin_payment_refund_receive` | 159 | (4, 23) | -| `coin_payment_listen_for_payment_start` | 160 | (4, 24) | -| `coin_payment_listen_for_payment_stop` | 161 | (4, 25) | -| `coin_payment_listen_for_payment_interrupt` | 162 | (4, 26) | -| `coin_payment_listen_for_payment_receive` | 163 | (4, 27) | -| `entropy_derive_request` | 108 | (5, 0) | -| `entropy_derive_response` | 109 | (5, 1) | -| `local_storage_read_request` | 12 | (6, 0) | -| `local_storage_read_response` | 13 | (6, 1) | -| `local_storage_write_request` | 14 | (6, 2) | -| `local_storage_write_response` | 15 | (6, 3) | -| `local_storage_clear_request` | 16 | (6, 4) | -| `local_storage_clear_response` | 17 | (6, 5) | -| `notifications_send_push_notification_request` | 4 | (7, 0) | -| `notifications_send_push_notification_response` | 5 | (7, 1) | -| `notifications_cancel_push_notification_request` | 134 | (7, 2) | -| `notifications_cancel_push_notification_response` | 135 | (7, 3) | -| `payment_balance_subscribe_start` | 118 | (8, 0) | -| `payment_balance_subscribe_stop` | 119 | (8, 1) | -| `payment_balance_subscribe_interrupt` | 120 | (8, 2) | -| `payment_balance_subscribe_receive` | 121 | (8, 3) | -| `payment_top_up_request` | 122 | (8, 4) | -| `payment_top_up_response` | 123 | (8, 5) | -| `payment_request_request` | 124 | (8, 6) | -| `payment_request_response` | 125 | (8, 7) | -| `payment_status_subscribe_start` | 126 | (8, 8) | -| `payment_status_subscribe_stop` | 127 | (8, 9) | -| `payment_status_subscribe_interrupt` | 128 | (8, 10) | -| `payment_status_subscribe_receive` | 129 | (8, 11) | -| `permissions_request_device_permission_request` | 8 | (9, 0) | -| `permissions_request_device_permission_response` | 9 | (9, 1) | -| `permissions_request_remote_permission_request` | 10 | (9, 2) | -| `permissions_request_remote_permission_response` | 11 | (9, 3) | -| `preimage_lookup_subscribe_start` | 64 | (10, 0) | -| `preimage_lookup_subscribe_stop` | 65 | (10, 1) | -| `preimage_lookup_subscribe_interrupt` | 66 | (10, 2) | -| `preimage_lookup_subscribe_receive` | 67 | (10, 3) | -| `preimage_submit_request` | 68 | (10, 4) | -| `preimage_submit_response` | 69 | (10, 5) | -| `resource_allocation_request_request` | 130 | (11, 0) | -| `resource_allocation_request_response` | 131 | (11, 1) | -| `signing_create_transaction_request` | 30 | (12, 0) | -| `signing_create_transaction_response` | 31 | (12, 1) | -| `signing_create_transaction_with_legacy_account_request` | 32 | (12, 2) | -| `signing_create_transaction_with_legacy_account_response` | 33 | (12, 3) | -| `signing_sign_raw_with_legacy_account_request` | 34 | (12, 4) | -| `signing_sign_raw_with_legacy_account_response` | 35 | (12, 5) | -| `signing_sign_payload_with_legacy_account_request` | 36 | (12, 6) | -| `signing_sign_payload_with_legacy_account_response` | 37 | (12, 7) | -| `signing_sign_raw_request` | 114 | (12, 8) | -| `signing_sign_raw_response` | 115 | (12, 9) | -| `signing_sign_payload_request` | 116 | (12, 10) | -| `signing_sign_payload_response` | 117 | (12, 11) | -| `statement_store_subscribe_start` | 56 | (13, 0) | -| `statement_store_subscribe_stop` | 57 | (13, 1) | -| `statement_store_subscribe_interrupt` | 58 | (13, 2) | -| `statement_store_subscribe_receive` | 59 | (13, 3) | -| `statement_store_create_proof_request` | 60 | (13, 4) | -| `statement_store_create_proof_response` | 61 | (13, 5) | -| `statement_store_submit_request` | 62 | (13, 6) | -| `statement_store_submit_response` | 63 | (13, 7) | -| `statement_store_create_proof_authorized_request` | 132 | (13, 8) | -| `statement_store_create_proof_authorized_response` | 133 | (13, 9) | -| `theme_subscribe_start` | 104 | (14, 0) | -| `theme_subscribe_stop` | 105 | (14, 1) | -| `theme_subscribe_interrupt` | 106 | (14, 2) | -| `theme_subscribe_receive` | 107 | (14, 3) | +| `system_handshake_request` | 0 | (192, 0) | +| `system_handshake_response` | 1 | (192, 1) | +| `system_feature_supported_request` | 2 | (192, 2) | +| `system_feature_supported_response` | 3 | (192, 3) | +| `system_navigate_to_request` | 6 | (192, 4) | +| `system_navigate_to_response` | 7 | (192, 5) | +| `account_connection_status_subscribe_start` | 18 | (193, 0) | +| `account_connection_status_subscribe_stop` | 19 | (193, 1) | +| `account_connection_status_subscribe_interrupt` | 20 | (193, 2) | +| `account_connection_status_subscribe_receive` | 21 | (193, 3) | +| `account_get_account_request` | 22 | (193, 4) | +| `account_get_account_response` | 23 | (193, 5) | +| `account_get_account_alias_request` | 24 | (193, 6) | +| `account_get_account_alias_response` | 25 | (193, 7) | +| `account_create_account_proof_request` | 26 | (193, 8) | +| `account_create_account_proof_response` | 27 | (193, 9) | +| `account_get_legacy_accounts_request` | 28 | (193, 10) | +| `account_get_legacy_accounts_response` | 29 | (193, 11) | +| `account_get_user_id_request` | 110 | (193, 12) | +| `account_get_user_id_response` | 111 | (193, 13) | +| `account_request_login_request` | 112 | (193, 14) | +| `account_request_login_response` | 113 | (193, 15) | +| `account_sign_vrf_request` | 164 | (193, 16) | +| `account_sign_vrf_response` | 165 | (193, 17) | +| `chain_follow_head_subscribe_start` | 76 | (194, 0) | +| `chain_follow_head_subscribe_stop` | 77 | (194, 1) | +| `chain_follow_head_subscribe_interrupt` | 78 | (194, 2) | +| `chain_follow_head_subscribe_receive` | 79 | (194, 3) | +| `chain_get_head_header_request` | 80 | (194, 4) | +| `chain_get_head_header_response` | 81 | (194, 5) | +| `chain_get_head_body_request` | 82 | (194, 6) | +| `chain_get_head_body_response` | 83 | (194, 7) | +| `chain_get_head_storage_request` | 84 | (194, 8) | +| `chain_get_head_storage_response` | 85 | (194, 9) | +| `chain_call_head_request` | 86 | (194, 10) | +| `chain_call_head_response` | 87 | (194, 11) | +| `chain_unpin_head_request` | 88 | (194, 12) | +| `chain_unpin_head_response` | 89 | (194, 13) | +| `chain_continue_head_request` | 90 | (194, 14) | +| `chain_continue_head_response` | 91 | (194, 15) | +| `chain_stop_head_operation_request` | 92 | (194, 16) | +| `chain_stop_head_operation_response` | 93 | (194, 17) | +| `chain_get_spec_genesis_hash_request` | 94 | (194, 18) | +| `chain_get_spec_genesis_hash_response` | 95 | (194, 19) | +| `chain_get_spec_chain_name_request` | 96 | (194, 20) | +| `chain_get_spec_chain_name_response` | 97 | (194, 21) | +| `chain_get_spec_properties_request` | 98 | (194, 22) | +| `chain_get_spec_properties_response` | 99 | (194, 23) | +| `chain_broadcast_transaction_request` | 100 | (194, 24) | +| `chain_broadcast_transaction_response` | 101 | (194, 25) | +| `chain_stop_transaction_request` | 102 | (194, 26) | +| `chain_stop_transaction_response` | 103 | (194, 27) | +| `chat_create_room_request` | 38 | (195, 0) | +| `chat_create_room_response` | 39 | (195, 1) | +| `chat_register_bot_request` | 40 | (195, 2) | +| `chat_register_bot_response` | 41 | (195, 3) | +| `chat_list_subscribe_start` | 42 | (195, 4) | +| `chat_list_subscribe_stop` | 43 | (195, 5) | +| `chat_list_subscribe_interrupt` | 44 | (195, 6) | +| `chat_list_subscribe_receive` | 45 | (195, 7) | +| `chat_post_message_request` | 46 | (195, 8) | +| `chat_post_message_response` | 47 | (195, 9) | +| `chat_action_subscribe_start` | 48 | (195, 10) | +| `chat_action_subscribe_stop` | 49 | (195, 11) | +| `chat_action_subscribe_interrupt` | 50 | (195, 12) | +| `chat_action_subscribe_receive` | 51 | (195, 13) | +| `chat_custom_message_render_subscribe_start` | 52 | (195, 14) | +| `chat_custom_message_render_subscribe_stop` | 53 | (195, 15) | +| `chat_custom_message_render_subscribe_interrupt` | 54 | (195, 16) | +| `chat_custom_message_render_subscribe_receive` | 55 | (195, 17) | +| `coin_payment_create_purse_request` | 136 | (196, 0) | +| `coin_payment_create_purse_response` | 137 | (196, 1) | +| `coin_payment_query_purse_request` | 138 | (196, 2) | +| `coin_payment_query_purse_response` | 139 | (196, 3) | +| `coin_payment_rebalance_purse_start` | 140 | (196, 4) | +| `coin_payment_rebalance_purse_stop` | 141 | (196, 5) | +| `coin_payment_rebalance_purse_interrupt` | 142 | (196, 6) | +| `coin_payment_rebalance_purse_receive` | 143 | (196, 7) | +| `coin_payment_delete_purse_start` | 144 | (196, 8) | +| `coin_payment_delete_purse_stop` | 145 | (196, 9) | +| `coin_payment_delete_purse_interrupt` | 146 | (196, 10) | +| `coin_payment_delete_purse_receive` | 147 | (196, 11) | +| `coin_payment_create_receivable_request` | 148 | (196, 12) | +| `coin_payment_create_receivable_response` | 149 | (196, 13) | +| `coin_payment_create_cheque_request` | 150 | (196, 14) | +| `coin_payment_create_cheque_response` | 151 | (196, 15) | +| `coin_payment_deposit_start` | 152 | (196, 16) | +| `coin_payment_deposit_stop` | 153 | (196, 17) | +| `coin_payment_deposit_interrupt` | 154 | (196, 18) | +| `coin_payment_deposit_receive` | 155 | (196, 19) | +| `coin_payment_refund_start` | 156 | (196, 20) | +| `coin_payment_refund_stop` | 157 | (196, 21) | +| `coin_payment_refund_interrupt` | 158 | (196, 22) | +| `coin_payment_refund_receive` | 159 | (196, 23) | +| `coin_payment_listen_for_payment_start` | 160 | (196, 24) | +| `coin_payment_listen_for_payment_stop` | 161 | (196, 25) | +| `coin_payment_listen_for_payment_interrupt` | 162 | (196, 26) | +| `coin_payment_listen_for_payment_receive` | 163 | (196, 27) | +| `entropy_derive_request` | 108 | (197, 0) | +| `entropy_derive_response` | 109 | (197, 1) | +| `local_storage_read_request` | 12 | (198, 0) | +| `local_storage_read_response` | 13 | (198, 1) | +| `local_storage_write_request` | 14 | (198, 2) | +| `local_storage_write_response` | 15 | (198, 3) | +| `local_storage_clear_request` | 16 | (198, 4) | +| `local_storage_clear_response` | 17 | (198, 5) | +| `notifications_send_push_notification_request` | 4 | (199, 0) | +| `notifications_send_push_notification_response` | 5 | (199, 1) | +| `notifications_cancel_push_notification_request` | 134 | (199, 2) | +| `notifications_cancel_push_notification_response` | 135 | (199, 3) | +| `payment_balance_subscribe_start` | 118 | (200, 0) | +| `payment_balance_subscribe_stop` | 119 | (200, 1) | +| `payment_balance_subscribe_interrupt` | 120 | (200, 2) | +| `payment_balance_subscribe_receive` | 121 | (200, 3) | +| `payment_top_up_request` | 122 | (200, 4) | +| `payment_top_up_response` | 123 | (200, 5) | +| `payment_request_request` | 124 | (200, 6) | +| `payment_request_response` | 125 | (200, 7) | +| `payment_status_subscribe_start` | 126 | (200, 8) | +| `payment_status_subscribe_stop` | 127 | (200, 9) | +| `payment_status_subscribe_interrupt` | 128 | (200, 10) | +| `payment_status_subscribe_receive` | 129 | (200, 11) | +| `permissions_request_device_permission_request` | 8 | (201, 0) | +| `permissions_request_device_permission_response` | 9 | (201, 1) | +| `permissions_request_remote_permission_request` | 10 | (201, 2) | +| `permissions_request_remote_permission_response` | 11 | (201, 3) | +| `preimage_lookup_subscribe_start` | 64 | (202, 0) | +| `preimage_lookup_subscribe_stop` | 65 | (202, 1) | +| `preimage_lookup_subscribe_interrupt` | 66 | (202, 2) | +| `preimage_lookup_subscribe_receive` | 67 | (202, 3) | +| `preimage_submit_request` | 68 | (202, 4) | +| `preimage_submit_response` | 69 | (202, 5) | +| `resource_allocation_request_request` | 130 | (203, 0) | +| `resource_allocation_request_response` | 131 | (203, 1) | +| `signing_create_transaction_request` | 30 | (204, 0) | +| `signing_create_transaction_response` | 31 | (204, 1) | +| `signing_create_transaction_with_legacy_account_request` | 32 | (204, 2) | +| `signing_create_transaction_with_legacy_account_response` | 33 | (204, 3) | +| `signing_sign_raw_with_legacy_account_request` | 34 | (204, 4) | +| `signing_sign_raw_with_legacy_account_response` | 35 | (204, 5) | +| `signing_sign_payload_with_legacy_account_request` | 36 | (204, 6) | +| `signing_sign_payload_with_legacy_account_response` | 37 | (204, 7) | +| `signing_sign_raw_request` | 114 | (204, 8) | +| `signing_sign_raw_response` | 115 | (204, 9) | +| `signing_sign_payload_request` | 116 | (204, 10) | +| `signing_sign_payload_response` | 117 | (204, 11) | +| `statement_store_subscribe_start` | 56 | (205, 0) | +| `statement_store_subscribe_stop` | 57 | (205, 1) | +| `statement_store_subscribe_interrupt` | 58 | (205, 2) | +| `statement_store_subscribe_receive` | 59 | (205, 3) | +| `statement_store_create_proof_request` | 60 | (205, 4) | +| `statement_store_create_proof_response` | 61 | (205, 5) | +| `statement_store_submit_request` | 62 | (205, 6) | +| `statement_store_submit_response` | 63 | (205, 7) | +| `statement_store_create_proof_authorized_request` | 132 | (205, 8) | +| `statement_store_create_proof_authorized_response` | 133 | (205, 9) | +| `theme_subscribe_start` | 104 | (206, 0) | +| `theme_subscribe_stop` | 105 | (206, 1) | +| `theme_subscribe_interrupt` | 106 | (206, 2) | +| `theme_subscribe_receive` | 107 | (206, 3) | diff --git a/docs/local-e2e-testing.md b/docs/local-e2e-testing.md index 1547c917c..eddbe817f 100644 --- a/docs/local-e2e-testing.md +++ b/docs/local-e2e-testing.md @@ -241,7 +241,7 @@ iframe via `window.parent` and uses the iframe `postMessage` provider. If the connection chip stays on _Handshaking_, the handshake is failing. Check: -- The dotli console for `Unknown wire tag` / `Unknown wire discriminant` +- The dotli console for `Unknown wire tag` / `unknown wire discriminant pair` errors — wire-table mismatch between the dotli vendored copy of `@parity/truapi` and the just-built one. - The playground console for `decodeWireMessage` errors — the inbound diff --git a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift index 00213a3fe..6c9cc658c 100644 --- a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift +++ b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift @@ -82,8 +82,10 @@ private extension TrUAPIWsBridgeTests { ) } - // wire_table.rs: SYSTEM_FEATURE_SUPPORTED.request_id = 2 - static let featureSupportedRequestDiscriminant = Data([0x02]) + // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 0, request_id: 2 }. + // Both bytes are load-bearing: a lone method byte is read as the trait and + // routes into a different trait's method 0 rather than failing. + static let featureSupportedRequestDiscriminant = Data([0x00, 0x02]) // wire_table.rs: SYSTEM_HOST_INFO.request_id = 192 static let hostInfoRequestDiscriminant = Data([0xC0]) diff --git a/js/packages/truapi/README.md b/js/packages/truapi/README.md index 9a456eb04..e6b38e12e 100644 --- a/js/packages/truapi/README.md +++ b/js/packages/truapi/README.md @@ -132,10 +132,14 @@ package root returns the bare `WireProvider`. Frames are SCALE encoded: ```text -[requestId: SCALE str][discriminant: u8][payload bytes...] +[requestId: SCALE str][trait: u8][method: u8][payload bytes...] ``` -The discriminant table is generated from Rust `#[wire(request_id = N)]` and `#[wire(start_id = N)]` annotations and is written to `src/generated/wire-table.ts`. Discriminant 255 is reserved for method-independent protocol errors. When a peer rejects an unknown API message with that frame, requests resolve as `CallError.Unsupported` and subscriptions terminate with an `UnsupportedMessageError` cause. +The discriminant is a `(trait, method)` pair: the trait byte names the API trait and the method byte addresses a method within it, so method ids restart at 0 in every trait. The table is generated from the Rust trait-level `#[wire_trait(id = N)]` annotation plus the method-level `#[wire(request_id = N)]` and `#[wire(start_id = N)]` annotations, and is written to `src/generated/wire-table.ts`. + +This layout is wire codec version 2 and is not compatible with codec version 1, which addressed methods with a single flat byte. + +The pair `(255, 255)` is reserved for method-independent protocol errors. When a peer rejects an unknown API message with that frame, requests resolve as `CallError.Unsupported` and subscriptions terminate with an `UnsupportedMessageError` cause carrying the unsupported `(trait, method)` pair. ## Generated files diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 48d20a48e..263711248 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -71,6 +71,20 @@ function providerFixture() { } /** Encode a V1 host-handshake response result payload. */ +/** Encode the `UnsupportedProtocolVersion` handshake response payload. */ +function unsupportedHandshakeResponsePayload(): Uint8Array { + return versionedV1(ScaleResult(_void, CallError(T.VersionedHostHandshakeError))).enc({ + tag: "V1", + value: { + success: false, + value: { + tag: "Domain", + value: { tag: "V1", value: { tag: "UnsupportedProtocolVersion", value: undefined } }, + }, + }, + }); +} + function handshakeResponsePayload(value: { success: true; value: undefined }): Uint8Array { return versionedV1(ScaleResult(_void, CallError(T.VersionedHostHandshakeError))).enc({ tag: "V1", @@ -202,7 +216,7 @@ describe("generated client transport", () => { const expectedPayload = T.VersionedHostAccountGetRequest.enc({ tag: "V1", value: request }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 1; // account trait + expectedFrame[str.enc("p:1").length] = 193; // account trait expectedFrame[str.enc("p:1").length + 1] = 4; // get_account request expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); @@ -222,7 +236,7 @@ describe("generated client transport", () => { }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 0; // system trait + expectedFrame[str.enc("p:1").length] = 192; // system trait expectedFrame[str.enc("p:1").length + 1] = 0; // handshake request expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); @@ -625,6 +639,68 @@ describe("generated client transport", () => { expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); }); + it("refuses a codec 1 handshake ping and stays usable", () => { + const fixture = providerFixture(); + const transport = createTransport(fixture.provider); + const client = createClient(transport); + + // A codec 1 host frames its ping as [requestId][u8 id=0][V1][codec=1]. + // Read against the two-byte discriminant that is trait 0, method 0 -- + // and trait 0 is below the codec 2 floor, so it can never name a real + // trait. The ping is refused rather than answered on a wire the peer + // cannot parse anyway. + const legacyFrame = new Uint8Array([ + ...str.enc("h:1"), + 0x00, // old flat discriminant, read as the trait byte + 0x00, // old V1 tag, read as the method byte + 0x01, // old codecVersion, read as the whole payload + ]); + fixture.receive(legacyFrame); + + expect(fixture.sent.length).toBe(0); + + // The transport must survive: a ping it cannot parse is a peer + // problem, not grounds for tearing down every pending call. + void client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Left", value: 0 } } }); + expect(fixture.sent.length).toBe(1); + }); + + it("ignores a response whose trait does not match the pending request", async () => { + const fixture = providerFixture(); + const transport = createTransport(fixture.provider); + const client = createClient(transport); + + const response = client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Left", value: 0 } } }); + + // Right request id, right method id, neighbouring trait: what a whole + // trait of discriminant skew looks like from the product side. + const skewed = unwrap( + encodeWireMessage({ + requestId: "p:1", + payload: { + traitId: W.ACCOUNT_GET_ACCOUNT.trait + 1, + methodId: W.ACCOUNT_GET_ACCOUNT.response, + value: accountGetResponsePayload({ + success: false, + value: { + tag: "Domain", + value: { tag: "V1", value: { tag: "NotConnected", value: undefined } }, + }, + }), + }, + }), + "encode skewed account_get response", + ); + fixture.receive(skewed); + + // The frame is refused rather than mistaken for the real response. + const settled = await Promise.race([ + response.then(() => "settled" as const), + Promise.resolve().then(() => "pending" as const), + ]); + expect(settled).toBe("pending"); + }); + it("decodes receive frames as wire wrappers and delivers inner values", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); diff --git a/js/packages/truapi/src/client.ts b/js/packages/truapi/src/client.ts index e3c5c6c2f..0d65a3308 100644 --- a/js/packages/truapi/src/client.ts +++ b/js/packages/truapi/src/client.ts @@ -60,6 +60,19 @@ export interface CreateTransportOptions { codecVersion?: number; } +/** + * Report a frame the transport received but cannot act on. + * + * Every such frame is a disagreement with the peer about the wire, and the + * transport has no channel to answer on: the caller is left waiting and + * "the host dropped it" is indistinguishable from "the host never sent it". + * Warn so the mismatch is diagnosable from the console instead of presenting + * as an unexplained hang. + */ +function reportProtocolViolation(detail: string): void { + console.warn(`[truapi] ${detail}`); +} + /** * Convert a positive protocol version number into the generated version tag * used by TrUAPI wire wrappers. @@ -77,6 +90,12 @@ type HandshakeResponse = ResultPayload< >; const HANDSHAKE_WIRE_VERSION = 1; +/** + * How long a `system_handshake` call waits for the host's answer. Matches the + * allowance the protocol spec gives the handshake. + */ +const HANDSHAKE_TIMEOUT_MS = 10_000; + /** * Build the versioned handshake response codec for the selected wire version. */ @@ -325,17 +344,18 @@ export function createTransport( payload.traitId === W.SYSTEM_HANDSHAKE.trait && payload.methodId === W.SYSTEM_HANDSHAKE.request ) { - // Auto-respond to inbound `host_handshake_request` frames. - // - // Legacy hosts shipping `@novasamatech/host-api@0.6.x` (e.g. dotli) - // initiate their own handshake from the host side at startup and ping - // the iframe with `host_handshake_request` every 50ms until they see a - // matching response. The legacy host-api `createTransport` registered - // an internal handler for this message; preserving that behaviour - // keeps `@parity/truapi` a drop-in replacement for legacy bridges. + // Auto-respond to inbound `host_handshake_request` frames. Hosts ping + // the product at startup and repeat until they see a matching response, + // so this handler must always answer and must never tear the transport + // down: a host whose codec this client cannot speak is exactly the peer + // that needs an answer it can act on. // // Respond with the handshake method's selected wire version. The inner - // request carries the wire codec version. + // request carries the wire codec version. A request body this client + // cannot decode is itself a codec mismatch -- a codec 1 host's frame + // reads as `(0, 0)` here with the old envelope's payload shifted by a + // byte -- so it earns the same unsupported-version answer rather than a + // raw SCALE error. let response: Uint8Array; try { const request = unwrapVersionedWireValue( @@ -347,8 +367,12 @@ export function createTransport( ? encodeSuccessfulHandshakeResponse(HANDSHAKE_WIRE_VERSION) : encodeUnsupportedHandshakeResponse(HANDSHAKE_WIRE_VERSION); } catch (error) { - closeWithError(toError(error)); - return; + reportProtocolViolation( + `undecodable handshake request from the host (expected wire codec ${codecVersion}): ${ + toError(error).message + }`, + ); + response = encodeUnsupportedHandshakeResponse(HANDSHAKE_WIRE_VERSION); } try { send({ @@ -394,6 +418,13 @@ export function createTransport( payload.traitId !== p.ids.trait || payload.methodId !== p.ids.response ) { + // The host answered this request id on a discriminant the method does + // not own. Dropping it unreported leaves the caller waiting forever + // with no clue why, and a whole-trait skew is what a codec mismatch + // looks like from here. + reportProtocolViolation( + `ignoring frame for request ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}), expected (${p.ids.trait}, ${p.ids.response})`, + ); return; } pending.delete(requestId); @@ -427,26 +458,31 @@ export function createTransport( ) { subscriptions.delete(requestId); subscription.onInterrupt?.(payload.value); - return; + } else { + reportProtocolViolation( + `ignoring frame for subscription ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}), expected receive (${subscription.ids.trait}, ${subscription.ids.receive}) or interrupt (${subscription.ids.trait}, ${subscription.ids.interrupt})`, + ); } + return; } if (UNANSWERED_WIRE_IDS.has(`${payload.traitId}:${payload.methodId}`)) { return; } + // Not pending, no subscription, and not a client-bound frame we ignore by + // design: this build does not implement the pair. Report it locally AND + // answer the peer - a log alone leaves the sender waiting forever. + reportProtocolViolation( + `unsupported frame with discriminant (${payload.traitId}, ${payload.methodId}): request ${requestId} is not pending and has no subscription`, + ); try { send({ requestId, payload: { traitId: PROTOCOL_ERROR_TRAIT_ID, methodId: PROTOCOL_ERROR_METHOD_ID, - value: new Uint8Array([ - 0, - 0, - payload.traitId, - payload.methodId, - ]), + value: new Uint8Array([0, 0, payload.traitId, payload.methodId]), }, }); } catch { @@ -580,15 +616,48 @@ export function createTransport( } const requestId = `p:${++idCounter}`; + // The handshake is the one method with a bounded answer: it takes no + // host-side confirmation and settles the codec question before any + // real traffic. A peer that implements the protocol error now answers a + // discriminant it does not know, which settles this call as + // `Unsupported`; the deadline covers the peer that answers NOTHING - + // an older host, or one whose codec skew leaves the frame unroutable - + // so the call that exists to detect the mismatch cannot hang on it. + const deadline = + ids.trait === W.SYSTEM_HANDSHAKE.trait && + ids.request === W.SYSTEM_HANDSHAKE.request + ? setTimeout(() => { + if (!pending.delete(requestId)) { + return; + } + reject( + new Error( + `TrUAPI handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms; the host did not answer on wire codec ${codecVersion}`, + ), + ); + }, HANDSHAKE_TIMEOUT_MS) + : undefined; + pending.set(requestId, { ids, - resolve: (response) => resolve(decodeResponse(response)), - resolveUnsupported: () => + resolve: (response) => { + clearTimeout(deadline); + resolve(decodeResponse(response)); + }, + // Clears the deadline like the other two: an explicit `Unsupported` + // settles the call, and leaving the timer armed holds the event loop + // open for the rest of the timeout for nothing. + resolveUnsupported: () => { + clearTimeout(deadline); resolve({ success: false, value: { tag: "Unsupported" }, - }), - reject, + }); + }, + reject: (error) => { + clearTimeout(deadline); + reject(error); + }, }); try { send({ diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts index 5aa37a050..d4f4764d2 100644 --- a/js/packages/truapi/src/wire-equality.test.ts +++ b/js/packages/truapi/src/wire-equality.test.ts @@ -39,11 +39,12 @@ function unwrap(result: Result, message: string): T { } describe("encodeWireMessage / decodeWireMessage wire equality", () => { - it("pins the handshake frame end-to-end: requestId + 0x00 0x00 + payload", () => { - // Trait 0 = system, method 0 = handshake request. This locks the - // system trait to discriminant zero: the handshake is the first frame - // either side sends, so its envelope must never drift. - expect(W.SYSTEM_HANDSHAKE.trait).toBe(0); + it("pins the handshake frame end-to-end: requestId + 0xc0 0x00 + payload", () => { + // Trait 192 = system, method 0 = handshake request. This locks the + // system trait to the first id above the codec 1 flat-method range: + // the handshake is the first frame either side sends, so its envelope + // must never drift, and a codec 1 peer's frame must never reach it. + expect(W.SYSTEM_HANDSHAKE.trait).toBe(192); expect(W.SYSTEM_HANDSHAKE.request).toBe(0); const inner = new Uint8Array([0x00, 0x02]); // V1 variant + codec_version=2 @@ -58,17 +59,17 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }), "encode handshake_request", ); - // [0c 70 3a 31] "p:1" + [00] system trait + [00] handshake request + payload. - expect(toHex(encoded)).toBe("0c703a3100000002"); - expect(toHex(encoded)).toBe(toHex(expectedWire(0, 0, inner))); + // [0c 70 3a 31] "p:1" + [c0] system trait + [00] handshake request + payload. + expect(toHex(encoded)).toBe("0c703a31c0000002"); + expect(toHex(encoded)).toBe(toHex(expectedWire(192, 0, inner))); const decoded = unwrap(decodeWireMessage(encoded), "decode handshake_request"); - expect(decoded.payload.traitId).toBe(0); + expect(decoded.payload.traitId).toBe(192); expect(decoded.payload.methodId).toBe(0); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); - it("encodes account_get_request (pair (1, 4)) to match the golden fixture", () => { + it("encodes account_get_request (pair (193, 4)) to match the golden fixture", () => { // payload = V1(("foo", 0u32)); same vector as the Rust golden fixture. const inner = new Uint8Array([ 0x00, // V1 variant @@ -89,8 +90,8 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }), "encode account_get_request", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(1, 4, inner))); - expect(toHex(encoded)).toBe("0c703a310104000c666f6f00000000"); + expect(toHex(encoded)).toBe(toHex(expectedWire(193, 4, inner))); + expect(toHex(encoded)).toBe("0c703a31c104000c666f6f00000000"); }); it("round-trips a local_storage_read frame through encode + decode", () => { diff --git a/rust/crates/truapi-codegen/README.md b/rust/crates/truapi-codegen/README.md index e924f0901..26f440030 100644 --- a/rust/crates/truapi-codegen/README.md +++ b/rust/crates/truapi-codegen/README.md @@ -61,7 +61,7 @@ cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ --output js/packages/truapi/src/generated \ --version V2 \ - --codec-version 1 + --codec-version 2 ``` ## Typical workflow @@ -72,7 +72,7 @@ cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ --output js/packages/truapi/src/generated \ --version V2 \ - --codec-version 1 + --codec-version 2 ``` The repo wraps both steps in [`scripts/codegen.sh`](../../../scripts/codegen.sh), which is what you should run from the repo root. diff --git a/rust/crates/truapi-codegen/src/main.rs b/rust/crates/truapi-codegen/src/main.rs index b3d846263..8b084e1af 100644 --- a/rust/crates/truapi-codegen/src/main.rs +++ b/rust/crates/truapi-codegen/src/main.rs @@ -45,7 +45,7 @@ struct Cli { client_version: Option, /// Wire codec version for generated handshake calls. - #[arg(long, default_value_t = 2)] + #[arg(long, default_value_t = truapi::WIRE_CODEC_VERSION)] codec_version: u8, /// Output directory for generated playground metadata (optional). diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index f34026614..e58b76a6c 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -270,7 +270,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(0), + wire_trait_id: Some(192), methods: vec![make_subscription_method("connection_status_subscribe", 18)], docs: None, }], @@ -302,14 +302,14 @@ mod tests { TraitDef { name: "StatementStore".to_string(), module_path: Vec::new(), - wire_trait_id: Some(1), + wire_trait_id: Some(193), methods: vec![make_request_method("submit", 62)], docs: None, }, TraitDef { name: "Preimage".to_string(), module_path: Vec::new(), - wire_trait_id: Some(2), + wire_trait_id: Some(194), methods: vec![make_request_method("submit", 68)], docs: None, }, @@ -356,14 +356,14 @@ mod tests { TraitDef { name: "Foo".to_string(), module_path: Vec::new(), - wire_trait_id: Some(3), + wire_trait_id: Some(195), methods: vec![make_request_method("bar_baz", 10)], docs: None, }, TraitDef { name: "FooBar".to_string(), module_path: Vec::new(), - wire_trait_id: Some(4), + wire_trait_id: Some(196), methods: vec![make_request_method("baz", 12)], docs: None, }, @@ -394,7 +394,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![make_request_method("request_device_permission", 8)], docs: None, }], @@ -421,7 +421,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![ make_request_method("alpha", 10), make_request_method("beta", 10), @@ -434,7 +434,7 @@ mod tests { let err = generate_wire_table(&api).expect_err("duplicate ids must error"); let msg = format!("{err}"); assert!( - msg.contains("wire id (5, 10) reused"), + msg.contains("wire id (197, 10) reused"), "unexpected error message: {msg}", ); } @@ -448,14 +448,14 @@ mod tests { TraitDef { name: "StatementStore".to_string(), module_path: Vec::new(), - wire_trait_id: Some(13), + wire_trait_id: Some(205), methods: vec![make_request_method("submit", 0)], docs: None, }, TraitDef { name: "Preimage".to_string(), module_path: Vec::new(), - wire_trait_id: Some(10), + wire_trait_id: Some(202), methods: vec![make_request_method("submit", 0)], docs: None, }, @@ -466,11 +466,11 @@ mod tests { let table = generate_wire_table(&api).expect("wire_table"); assert!( - table.contains("trait_id: 13,"), + table.contains("trait_id: 205,"), "missing trait id 13:\n{table}" ); assert!( - table.contains("trait_id: 10,"), + table.contains("trait_id: 202,"), "missing trait id 10:\n{table}" ); } @@ -483,14 +483,14 @@ mod tests { TraitDef { name: "StatementStore".to_string(), module_path: Vec::new(), - wire_trait_id: Some(4), + wire_trait_id: Some(196), methods: vec![make_request_method("submit", 0)], docs: None, }, TraitDef { name: "Preimage".to_string(), module_path: Vec::new(), - wire_trait_id: Some(4), + wire_trait_id: Some(196), methods: vec![make_request_method("submit", 0)], docs: None, }, @@ -502,7 +502,7 @@ mod tests { let err = generate_wire_table(&api).expect_err("duplicate trait ids must error"); let msg = format!("{err}"); assert!( - msg.contains("wire trait id 4 reused"), + msg.contains("wire trait id 196 reused"), "unexpected error message: {msg}", ); } @@ -556,6 +556,32 @@ mod tests { assert!( msg.contains("wire trait id 255 reused") && msg.contains("reserved for protocol errors"), + "unexpected error message: {msg}", + ); + } + + /// A trait id inside the range codec 1 could address must be refused. + /// Codec 2 reads that byte as the trait, so a low id would let a codec 1 + /// frame decode into a registered trait and execute the wrong method + /// instead of being reported as unroutable. + #[test] + fn wire_table_rejects_trait_id_below_the_codec_1_floor() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + wire_trait_id: Some(MIN_TRAIT_ID - 1), + methods: vec![make_request_method("request_device_permission", 8)], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + + let err = generate_wire_table(&api).expect_err("a below-floor trait id must error"); + let msg = format!("{err}"); + assert!( + msg.contains("below the minimum"), "unexpected error message: {msg}", ); } @@ -571,7 +597,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(7), + wire_trait_id: Some(MIN_TRAIT_ID), methods: vec![method], docs: None, }], @@ -579,7 +605,8 @@ mod tests { types: vec![], }; - generate_wire_table(&api).expect("(7, 255) is an ordinary address"); + generate_wire_table(&api) + .expect("(MIN_TRAIT_ID, 255) is an ordinary address"); } /// Pin `wire_const_name`'s `convert_case::Case::UpperSnake` behavior: @@ -623,7 +650,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![method], docs: None, }], @@ -647,7 +674,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(0), + wire_trait_id: Some(192), methods: vec![method], docs: None, }], @@ -672,7 +699,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![method], docs: None, }], @@ -696,7 +723,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(0), + wire_trait_id: Some(192), methods: vec![method], docs: None, }], @@ -728,7 +755,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![method], docs: None, }], @@ -762,7 +789,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![method], docs: None, }], @@ -797,7 +824,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![method], docs: None, }], @@ -827,7 +854,7 @@ mod tests { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), - wire_trait_id: Some(5), + wire_trait_id: Some(197), methods: vec![method], docs: None, }], @@ -867,7 +894,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(0), + wire_trait_id: Some(192), methods: vec![method], docs: None, }], diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs index 5e830ac39..afa6e537c 100644 --- a/rust/crates/truapi-codegen/src/rust/wire_table.rs +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -104,14 +104,27 @@ pub fn generate_wire_table(api: &ApiDefinition) -> Result { } /// The trait's wire discriminant. Every API trait must carry a -/// `#[wire_trait(id = N)]` annotation. +/// `#[wire_trait(id = N)]` annotation whose id is at least +/// [`MIN_TRAIT_ID`] and is not 255, which is reserved for protocol errors +/// (that one is caught as a collision against the seeded reservation, not +/// here). fn trait_wire_id(trait_def: &TraitDef) -> Result { - trait_def.wire_trait_id.ok_or_else(|| { + let id = trait_def.wire_trait_id.ok_or_else(|| { anyhow::anyhow!( "trait `{}` is missing #[wire_trait(id = N)] annotation", trait_def.name ) - }) + })?; + if id < MIN_TRAIT_ID { + bail!( + "trait `{}` has wire trait id {id}, below the minimum {MIN_TRAIT_ID}: \ + ids under {MIN_TRAIT_ID} are reserved so that a codec 1 frame, whose \ + single flat method byte never exceeded {MAX_CODEC_1_METHOD_ID}, can \ + never be mistaken for a codec 2 trait", + trait_def.name + ); + } + Ok(id) } fn method_entry(trait_def: &TraitDef, trait_id: u8, method: &MethodDef) -> Result { diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index 59efcbe8b..3a232e01a 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -6,6 +6,8 @@ use std::collections::{BTreeMap, HashMap}; use anyhow::{Context, Result, bail}; use serde::Deserialize; +pub use truapi::{MAX_CODEC_1_METHOD_ID, MIN_TRAIT_ID}; + /// Minimum rustdoc JSON `format_version` the extractors are tested against. /// Emitted by nightly 2026-02-23 (rustc 1.95.0-nightly); older formats may /// encode item shapes differently and are rejected outright. @@ -644,10 +646,15 @@ fn extract_trait( } } + let wire_trait_id = match item.docs.as_deref() { + Some(docs) => extract_wire_trait_id(&name, docs)?, + None => None, + }; + Ok(TraitDef { name, module_path, - wire_trait_id: item.docs.as_deref().and_then(extract_wire_trait_id), + wire_trait_id, methods, docs: item.docs.clone(), }) @@ -817,20 +824,33 @@ fn extract_marker_value<'a>(docs: &'a str, marker: &str) -> Option<&'a str> { /// Annotated traits carry the marker via the `#[wire_trait(id = N)]` /// proc-macro, which appends a hidden doc string so it propagates through /// rustdoc JSON. -fn extract_wire_trait_id(docs: &str) -> Option { +/// +/// The marker owns its whole line and its value must parse as a `u8`: a +/// malformed value is an error rather than a silent truncation, and a trait +/// carrying more than one marker is rejected outright. Without that a +/// hand-written doc line could quietly outrank the attribute and move a +/// trait's whole method block to a different address on the wire. +fn extract_wire_trait_id(trait_name: &str, docs: &str) -> Result> { + let mut found = None; for line in docs.lines() { - let line = line.trim_start(); - let Some(value) = line.strip_prefix("@wire_trait_id=") else { + let Some(value) = line.trim().strip_prefix("@wire_trait_id=") else { continue; }; - let end = value - .find(|c: char| !c.is_ascii_digit()) - .unwrap_or(value.len()); - if let Ok(id) = value[..end].parse::() { - return Some(id); + let id: u8 = value.trim().parse().with_context(|| { + format!( + "Trait `{trait_name}` has a malformed `@wire_trait_id={value}` marker; \ + expected a value in 0..=255" + ) + })?; + if found.is_some() { + bail!( + "Trait `{trait_name}` carries more than one `@wire_trait_id` marker; \ + exactly one `#[wire_trait(id = N)]` attribute must own the trait id" + ); } + found = Some(id); } - None + Ok(found) } /// Extracts `@wire__id=N` markers from a doc comment block. Annotated @@ -1531,12 +1551,46 @@ mod tests { #[test] fn extract_wire_trait_id_reads_marker() { assert_eq!( - extract_wire_trait_id("Trait summary.\n\n@wire_trait_id=14\n"), + extract_wire_trait_id("Theme", "Trait summary.\n\n@wire_trait_id=14\n").unwrap(), Some(14) ); - assert_eq!(extract_wire_trait_id("Trait summary."), None); - // Out-of-range values are ignored rather than truncated. - assert_eq!(extract_wire_trait_id("@wire_trait_id=300"), None); + assert_eq!( + extract_wire_trait_id("Theme", "Trait summary.").unwrap(), + None + ); + } + + /// A value the attribute could never emit must fail loudly instead of + /// truncating to a valid id or degrading into "missing annotation". + #[test] + fn extract_wire_trait_id_rejects_malformed_markers() { + for docs in [ + "@wire_trait_id=300", + "@wire_trait_id=", + "@wire_trait_id=12abc", + "@wire_trait_id=-1", + "@wire_trait_id=1 2", + ] { + let err = extract_wire_trait_id("Theme", docs) + .expect_err("malformed marker must be rejected"); + assert!( + format!("{err:#}").contains("malformed"), + "unexpected error for {docs:?}: {err:#}" + ); + } + } + + /// A hand-written doc line must not be able to outrank the attribute: the + /// proc-macro appends its marker last, so a silent first-wins or last-wins + /// rule would let prose move the trait's whole method block on the wire. + #[test] + fn extract_wire_trait_id_rejects_a_second_marker() { + let err = extract_wire_trait_id("Theme", "@wire_trait_id=99\n@wire_trait_id=14\n") + .expect_err("a forged second marker must be rejected"); + assert!( + format!("{err:#}").contains("more than one"), + "unexpected error: {err:#}" + ); } #[test] diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index 33820fd62..d0bf81bed 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -680,14 +680,27 @@ fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result Result { - trait_def.wire_trait_id.ok_or_else(|| { + let id = trait_def.wire_trait_id.ok_or_else(|| { anyhow::anyhow!( "trait `{}` is missing #[wire_trait(id = N)] annotation", trait_def.name ) - }) + })?; + if id < MIN_TRAIT_ID { + bail!( + "trait `{}` has wire trait id {id}, below the minimum {MIN_TRAIT_ID}: \ + ids under {MIN_TRAIT_ID} are reserved so that a codec 1 frame, whose \ + single flat method byte never exceeded {MAX_CODEC_1_METHOD_ID}, can \ + never be mistaken for a codec 2 trait", + trait_def.name + ); + } + Ok(id) } fn method_is_included( @@ -2571,14 +2584,14 @@ mod tests { let json_rpc = TraitDef { name: "JsonRpc".to_string(), module_path: Vec::new(), - wire_trait_id: Some(6), + wire_trait_id: Some(198), methods: Vec::new(), docs: None, }; let system = TraitDef { name: "System".to_string(), module_path: Vec::new(), - wire_trait_id: Some(7), + wire_trait_id: Some(199), methods: Vec::new(), docs: None, }; @@ -2617,7 +2630,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(8), + wire_trait_id: Some(200), methods, docs: None, }], @@ -2846,7 +2859,7 @@ mod tests { .expect("generate wire table"); assert!(source.contains("export const EXAMPLE_STREAM = {")); - assert!(source.contains(" trait: 8,")); + assert!(source.contains(" trait: 200,")); assert!(source.contains(" start: 2,")); assert!(source.contains(" receive: 5,")); assert!(source.contains("export const EXAMPLE_LATER = {")); @@ -2872,7 +2885,7 @@ mod tests { ) .expect_err("duplicate ids must error"); - assert!(err.to_string().contains("wire id (8, 3) reused")); + assert!(err.to_string().contains("wire id (200, 3) reused")); } /// Trait 255 is reserved for protocol errors, so no API trait may declare @@ -2903,7 +2916,8 @@ mod tests { let mut method = request_method("explicit_request", Some(255)); method.wire.response_id = Some(1); - generate_wire_table(&api(vec![method]), 2).expect("(8, 255) is an ordinary address"); + generate_wire_table(&api(vec![method]), 2) + .expect("(200, 255) is an ordinary address"); } /// Version filtering must not become an escape hatch: a trait that declares @@ -3008,14 +3022,14 @@ mod tests { TraitDef { name: "Alpha".to_string(), module_path: Vec::new(), - wire_trait_id: Some(0), + wire_trait_id: Some(192), methods: vec![request_method("first", Some(0))], docs: None, }, TraitDef { name: "Beta".to_string(), module_path: Vec::new(), - wire_trait_id: Some(1), + wire_trait_id: Some(193), methods: vec![request_method("second", Some(0))], docs: None, }, @@ -3028,8 +3042,8 @@ mod tests { assert!(source.contains("export const ALPHA_FIRST = {")); assert!(source.contains("export const BETA_SECOND = {")); - assert!(source.contains(" trait: 0,")); - assert!(source.contains(" trait: 1,")); + assert!(source.contains(" trait: 192,")); + assert!(source.contains(" trait: 193,")); } /// Two traits must not share a wire trait id. @@ -3040,14 +3054,14 @@ mod tests { TraitDef { name: "Alpha".to_string(), module_path: Vec::new(), - wire_trait_id: Some(3), + wire_trait_id: Some(195), methods: vec![request_method("first", Some(0))], docs: None, }, TraitDef { name: "Beta".to_string(), module_path: Vec::new(), - wire_trait_id: Some(3), + wire_trait_id: Some(195), methods: vec![request_method("second", Some(0))], docs: None, }, @@ -3057,7 +3071,7 @@ mod tests { }; let err = generate_wire_table(&api, 2).expect_err("duplicate trait ids must error"); - assert!(err.to_string().contains("wire trait id 3 reused")); + assert!(err.to_string().contains("wire trait id 195 reused")); } /// A trait without `#[wire_trait(id = N)]` must fail emission. @@ -3085,7 +3099,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(8), + wire_trait_id: Some(200), methods: vec![ request_method_with_wrappers( "legacy", @@ -3132,7 +3146,7 @@ mod tests { TraitDef { name: "Legacy".to_string(), module_path: Vec::new(), - wire_trait_id: Some(9), + wire_trait_id: Some(201), methods: vec![request_method_with_wrappers( "legacy_call", Some(2), @@ -3145,7 +3159,7 @@ mod tests { TraitDef { name: "FutureOnly".to_string(), module_path: Vec::new(), - wire_trait_id: Some(10), + wire_trait_id: Some(202), methods: vec![request_method_with_wrappers( "future_call", Some(4), @@ -3183,7 +3197,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(8), + wire_trait_id: Some(200), methods: vec![MethodDef { name: "example_call".to_string(), kind: MethodKind::Request, @@ -3233,7 +3247,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(8), + wire_trait_id: Some(200), methods: vec![ MethodDef { name: "legacy_call".to_string(), @@ -3301,7 +3315,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(8), + wire_trait_id: Some(200), methods: vec![MethodDef { name: "example_call".to_string(), kind: MethodKind::Request, @@ -3348,7 +3362,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(8), + wire_trait_id: Some(200), methods: vec![MethodDef { name: "example_call".to_string(), kind: MethodKind::Request, diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md index fc7c68e02..69ab45aed 100644 --- a/rust/crates/truapi-server/README.md +++ b/rust/crates/truapi-server/README.md @@ -242,15 +242,18 @@ session/SSO crypto, key derivation, and permission policy, while all I/O Every frame on the wire is encoded as: ```text -[requestId: SCALE str][discriminant: u8][payload bytes...] +[requestId: SCALE str][trait: u8][method: u8][payload bytes...] ``` -The discriminant identifies a method + frame kind via the auto-generated -[`crate::generated::wire_table::WIRE_TABLE`]. Each method's ids are exposed -as a named const (`PREIMAGE_SUBMIT`, ...); both `WIRE_TABLE` and the generated -dispatcher reference those consts. Method ordering is part of the wire -protocol; only ever append. +The `(trait, method)` discriminant pair identifies a method + frame kind via +the auto-generated [`crate::generated::wire_table::WIRE_TABLE`]. The trait +byte comes from the trait-level `#[wire_trait(id = N)]` annotation; the method +byte addresses a method within that trait, so method ids restart at 0 in every +trait. Each method's ids are exposed as a named const (`PREIMAGE_SUBMIT`, ...); +both `WIRE_TABLE` and the generated dispatcher reference those consts. Trait +ids and per-trait method ordering are part of the wire protocol; only ever +append within a trait. The payload bytes are the SCALE-encoded inner value, inlined without a -length prefix. The discriminant is carried directly as `Payload::id`, and the -dispatcher routes on that numeric id via id-keyed tables. +length prefix. The pair is carried as `Payload::trait_id` and +`Payload::method_id`, and the dispatcher routes on it via pair-keyed tables. diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index 5f8eeeabe..dd04dc099 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -113,7 +113,20 @@ impl TrUApiCore { /// richer response shape is a separate API decision. #[instrument(skip_all, fields(runtime.method = "core.receive_from_product"))] pub async fn receive_from_product(&self, frame: &[u8]) -> Option> { - let message = ProtocolMessage::decode(&mut &*frame).ok()?; + let message = match ProtocolMessage::decode(&mut &*frame) { + Ok(message) => message, + Err(err) => { + // An undecodable frame is a wire mismatch on the product's + // side. Report it: dropping it unreported is indistinguishable + // from the product never having sent it, and the product is + // left waiting for a response that will never come. + tracing::error!( + frame_len = frame.len(), + "undecodable product frame; dropping frame: {err}" + ); + return None; + } + }; let transport = Arc::new(ResponseTransport::default()); self.dispatcher .dispatch(message, transport.clone() as Arc) diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index 46812c6a8..d0ecb0134 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use futures::future::BoxFuture; use parity_scale_codec::Encode; use tracing::instrument; +use truapi::{MIN_TRAIT_ID, WIRE_CODEC_VERSION}; use crate::frame::{ PROTOCOL_ERROR_KEY, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, ProtocolErrorV1, @@ -204,6 +205,23 @@ impl Dispatcher { // understood. No log - a peer speaking a wire we do not know could // otherwise flood the host's logs one frame at a time. let (trait_id, method_id) = key; + if trait_id < MIN_TRAIT_ID { + // No trait is addressed below this floor, so the first byte + // cannot be a trait id. A codec 1 peer, whose frames carry a + // single flat method byte here, lands in exactly this range - + // worth saying out loud, because it explains total + // incompatibility in one line. + tracing::error!( + request_id = %message.request_id, + trait_id, + method_id, + "trait id {trait_id} is below the codec {WIRE_CODEC_VERSION} minimum \ + {MIN_TRAIT_ID}; the peer appears to be speaking codec 1" + ); + } + // Answer either way. A codec 1 peer cannot decode this reply, but a + // codec 2 peer that simply asked for something unimplemented can, + // and dropping the frame would leave it waiting forever. transport.send(ProtocolMessage { request_id: message.request_id, payload: Payload { diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index 6269e60c9..f03cecdf2 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -294,11 +294,12 @@ mod tests { } #[test] - fn handshake_request_encodes_with_discriminant_pair_zero_zero() { + fn handshake_request_encodes_with_the_system_trait_pair() { // SCALE-encoded HostHandshakeRequest::V1(2u8) = [0u8 variant][2u8 codec_version] let inner: Vec = vec![0x00, 0x02]; - let msg = build(0, 0, inner.clone()); - assert_eq!(msg.encode(), expected_wire(0, 0, &inner)); + // system trait = 192, handshake request = 0. + let msg = build(192, 0, inner.clone()); + assert_eq!(msg.encode(), expected_wire(192, 0, &inner)); } #[test] @@ -306,15 +307,15 @@ mod tests { let mut inner = vec![0x00]; // V1 variant "foo".to_string().encode_to(&mut inner); 0u32.encode_to(&mut inner); - // account trait = 1, get_account request = 4. - let msg = build(1, 4, inner.clone()); - assert_eq!(msg.encode(), expected_wire(1, 4, &inner)); + // account trait = 193, get_account request = 4. + let msg = build(193, 4, inner.clone()); + assert_eq!(msg.encode(), expected_wire(193, 4, &inner)); } #[test] fn round_trip_preserves_ids_and_value() { let inner: Vec = vec![0x00, 0x42, 0xab, 0xcd]; - let msg = build(6, 0, inner.clone()); + let msg = build(198, 0, inner.clone()); let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); assert_eq!(decoded, msg); } @@ -376,7 +377,7 @@ mod tests { /// regression where `Decode` mishandles a frame whose payload is empty for /// `_stop` / `_interrupt` (no inner data) but non-empty for `_start` / /// `_receive`. The ids are the `account_connection_status_subscribe` - /// quartet (trait 1, methods 0..=3). + /// quartet (trait 193, methods 0..=3). #[test] fn subscription_phases_round_trip_through_codec() { let cases: &[(u8, Vec)] = &[ @@ -386,11 +387,11 @@ mod tests { (3, vec![0x01, 0x02, 0x03, 0x04]), // receive ]; for (method_id, value) in cases { - let msg = build(1, *method_id, value.clone()); + let msg = build(193, *method_id, value.clone()); let bytes = msg.encode(); assert_eq!( bytes, - expected_wire(1, *method_id, value), + expected_wire(193, *method_id, value), "encode mismatch for method id {method_id}" ); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); @@ -406,17 +407,17 @@ mod tests { #[test] fn id_helpers_resolve_known_methods() { let handshake = request_ids("system_handshake").expect("known request method"); - assert_eq!(handshake.trait_id, 0); + assert_eq!(handshake.trait_id, 192); assert_eq!(handshake.request_id, 0); assert_eq!(handshake.response_id, 1); let get_account = request_ids("account_get_account").expect("known request method"); - assert_eq!(get_account.trait_id, 1); + assert_eq!(get_account.trait_id, 193); assert_eq!(get_account.request_id, 4); let sub = subscription_ids("account_connection_status_subscribe").expect("known subscription"); - assert_eq!(sub.trait_id, 1); + assert_eq!(sub.trait_id, 193); assert_eq!(sub.start_id, 0); assert_eq!(sub.stop_id, 1); assert_eq!(sub.interrupt_id, 2); @@ -432,10 +433,10 @@ mod tests { /// handle `remaining_len == 0` without erroring or reading past EOF. #[test] fn empty_payload_round_trips() { - // local_storage_clear_response = (6, 5). - let msg = build(6, 5, Vec::new()); + // local_storage_clear_response = (198, 5). + let msg = build(198, 5, Vec::new()); let bytes = msg.encode(); - // [SCALE compact-len 0x0c][p][:][1][u8 6][u8 5] = 4 + 2 = 6 bytes total + // [SCALE compact-len 0x0c][p][:][1][u8 198][u8 5] = 4 + 2 = 6 bytes total assert_eq!(bytes.len(), 6); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); assert_eq!(decoded, msg); @@ -449,7 +450,7 @@ mod tests { let msg = ProtocolMessage { request_id: long_id, payload: Payload { - trait_id: 1, + trait_id: 193, method_id: 4, value: vec![0x00, 0xab, 0xcd], }, @@ -495,7 +496,7 @@ mod tests { let msg = ProtocolMessage { request_id: String::new(), payload: Payload { - trait_id: 1, + trait_id: 193, method_id: 4, value: vec![0x00, 0x01, 0x02], }, @@ -513,7 +514,7 @@ mod tests { let msg = ProtocolMessage { request_id: "héllo-世界-🦀".to_string(), payload: Payload { - trait_id: 1, + trait_id: 193, method_id: 4, value: vec![0x00, 0x01], }, @@ -527,7 +528,7 @@ mod tests { #[test] fn large_payload_round_trips() { let big = vec![0xa5u8; 100 * 1024]; - let msg = build(1, 4, big); + let msg = build(193, 4, big); let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); assert_eq!(decoded, msg); } diff --git a/rust/crates/truapi-server/src/logging.rs b/rust/crates/truapi-server/src/logging.rs index 6141aad90..fd52f53d7 100644 --- a/rust/crates/truapi-server/src/logging.rs +++ b/rust/crates/truapi-server/src/logging.rs @@ -4,7 +4,12 @@ //! `#[instrument]` spans flow through a single subscriber installed once by //! [`init`]. A reloadable [`LevelFilter`] decides what reaches the console, so //! the verbosity is tunable at runtime via [`set_level`] (exposed to JS as -//! `setLogLevel`). Disabled by default ([`LevelFilter::OFF`]). +//! `setLogLevel`). The default floor is [`DEFAULT_LEVEL`]: the crate reserves +//! `ERROR` for protocol violations a peer cannot see any other way (an +//! undecodable frame, an unroutable discriminant pair), so a host that never +//! calls `setLogLevel` still learns that its peer is speaking a wire it does +//! not understand. Everything chattier stays off until asked for, and +//! `setLogLevel("off")` silences the channel completely. //! //! On wasm each level maps to the matching `console` method //! (`error`/`warn`/`info`/`debug`); on native everything goes to stderr. @@ -30,13 +35,18 @@ use tracing_subscriber::reload; static RELOAD_HANDLE: OnceLock> = OnceLock::new(); static TRACE_SPANS: AtomicBool = AtomicBool::new(false); +/// Verbosity applied until a host calls [`set_level`], and the fallback for an +/// unrecognised level string. `ERROR` is reserved for protocol violations, so +/// this floor keeps those visible without emitting routine traffic. +pub const DEFAULT_LEVEL: LevelFilter = LevelFilter::ERROR; + /// Install the global subscriber. Idempotent: the first call wins, later /// calls (and a foreign subscriber already being set) are no-ops. pub fn init() { if RELOAD_HANDLE.get().is_some() { return; } - let (filter, handle) = reload::Layer::::new(LevelFilter::OFF); + let (filter, handle) = reload::Layer::::new(DEFAULT_LEVEL); let subscriber = Registry::default().with(ConsoleLayer.with_filter(filter)); if tracing::subscriber::set_global_default(subscriber).is_ok() { let _ = RELOAD_HANDLE.set(handle); @@ -63,15 +73,18 @@ pub fn set_level_from_str(level: &str) { tracing::info!(level, "log level set"); } -/// Parse a host-supplied level string. Unknown values disable logging. +/// Parse a host-supplied level string. Only an explicit `"off"` silences the +/// channel; an unrecognised value falls back to [`DEFAULT_LEVEL`] so a typo +/// cannot quietly suppress protocol violations. pub fn parse_level(level: &str) -> LevelFilter { match level.to_ascii_lowercase().as_str() { + "off" | "none" => LevelFilter::OFF, "error" => LevelFilter::ERROR, "warn" | "warning" => LevelFilter::WARN, "info" => LevelFilter::INFO, "debug" => LevelFilter::DEBUG, "trace" => LevelFilter::TRACE, - _ => LevelFilter::OFF, + _ => DEFAULT_LEVEL, } } @@ -208,3 +221,36 @@ fn emit(level: Level, line: &str) { Level::DEBUG | Level::TRACE => web_sys::console::debug_1(&js), } } + +#[cfg(test)] +mod tests { + use super::*; + + /// The floor must stay at `ERROR` so a host that never calls + /// `setLogLevel` still sees protocol violations, and a typo must not be + /// able to silence them. + #[test] + fn parse_level_defaults_to_the_error_floor() { + assert_eq!(DEFAULT_LEVEL, LevelFilter::ERROR); + assert_eq!(parse_level("nonsense"), DEFAULT_LEVEL); + assert_eq!(parse_level(""), DEFAULT_LEVEL); + } + + /// Silencing the channel stays possible, but only on purpose. + #[test] + fn parse_level_silences_only_on_an_explicit_request() { + assert_eq!(parse_level("off"), LevelFilter::OFF); + assert_eq!(parse_level("OFF"), LevelFilter::OFF); + assert_eq!(parse_level("none"), LevelFilter::OFF); + } + + #[test] + fn parse_level_reads_each_named_level() { + assert_eq!(parse_level("error"), LevelFilter::ERROR); + assert_eq!(parse_level("warn"), LevelFilter::WARN); + assert_eq!(parse_level("warning"), LevelFilter::WARN); + assert_eq!(parse_level("info"), LevelFilter::INFO); + assert_eq!(parse_level("debug"), LevelFilter::DEBUG); + assert_eq!(parse_level("trace"), LevelFilter::TRACE); + } +} diff --git a/rust/crates/truapi-server/src/ws_bridge.rs b/rust/crates/truapi-server/src/ws_bridge.rs index 5bd2a1bfe..5deb96e75 100644 --- a/rust/crates/truapi-server/src/ws_bridge.rs +++ b/rust/crates/truapi-server/src/ws_bridge.rs @@ -405,8 +405,15 @@ async fn handle_connection( Ok(WsMessage::Binary(bytes)) => { in_flight.retain(|task| !task.is_finished()); let product_runtime = product_runtime.clone(); + let frame_logger = logger.clone(); in_flight.push(tokio::spawn(async move { - let _ = product_runtime.receive_frame(bytes.to_vec()).await; + // A frame the runtime cannot decode is a wire mismatch on + // the peer's side. Report it: dropping it unreported is + // indistinguishable from the peer never having sent it, + // and the peer is left waiting for a response forever. + if let Err(err) = product_runtime.receive_frame(bytes.to_vec()).await { + frame_logger("truapi.ws_bridge.frame_error", &err.to_string()); + } })); } Ok(WsMessage::Text(_)) => { diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs index f6aa3f62c..f9249a557 100644 --- a/rust/crates/truapi-server/tests/golden_frame.rs +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -11,7 +11,7 @@ //! //! On the wire (15 bytes): //! [0c 70 3a 31] requestId = compact-len(3) + "p:1" -//! [01] trait discriminant 1 = account +//! [c1] trait discriminant 193 = account //! [04] method discriminant 4 = get_account request //! [00] versioned wrapper variant V1 //! [0c 66 6f 6f] "foo" diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin index d14ecd70a7c811793b3ce3ca6e1624e479aacd6a..2a9d37f11dd20652823880f620c6a67674e856c1 100644 GIT binary patch literal 15 Ucmd-nurfTz!oZW3pU(gU033q?{Qv*} literal 15 Ucmd-nurg$1Vc<#2&u0Jv02D(4Jpcdz diff --git a/rust/crates/truapi/README.md b/rust/crates/truapi/README.md index 5edb6a7cd..b4b1a63e6 100644 --- a/rust/crates/truapi/README.md +++ b/rust/crates/truapi/README.md @@ -10,7 +10,7 @@ It defines: - **Versioned data types** under `v01` and `versioned`. - **Domain API traits** under `api/`, plus the composed `TrUApi` trait. -- **Wire ids** via per-method `#[wire(id = N)]` annotations that pin the byte-level method table. +- **Wire ids** via trait-level `#[wire_trait(id = N)]` and per-method `#[wire(request_id = N)]` annotations that pin the byte-level `(trait, method)` dispatch table. - **Subscription primitives** through `Subscription` for streamed host responses. - **Authoring types** like `CallContext`, `CallError`, and `CancellationToken`. diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index 09f235a19..78e3dbd9d 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -18,7 +18,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Account lookup, aliasing, and proof generation. -#[wire_trait(id = 1)] +#[wire_trait(id = 193)] #[crate::async_trait] pub trait Account: Send + Sync { /// Subscribe to account connection status changes. diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index ab2fc54d2..b6a922093 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -23,7 +23,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Chain interaction methods. -#[wire_trait(id = 2)] +#[wire_trait(id = 194)] #[crate::async_trait] pub trait Chain: Send + Sync { /// Follow the chain head and receive block events. diff --git a/rust/crates/truapi/src/api/chat.rs b/rust/crates/truapi/src/api/chat.rs index 74e50157e..91d065a05 100644 --- a/rust/crates/truapi/src/api/chat.rs +++ b/rust/crates/truapi/src/api/chat.rs @@ -11,7 +11,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Chat room, bot, and message APIs. -#[wire_trait(id = 3)] +#[wire_trait(id = 195)] #[crate::service(required_execution = Worker)] #[crate::async_trait] pub trait Chat: Send + Sync { diff --git a/rust/crates/truapi/src/api/coin_payment.rs b/rust/crates/truapi/src/api/coin_payment.rs index e33592c67..3829c56cc 100644 --- a/rust/crates/truapi/src/api/coin_payment.rs +++ b/rust/crates/truapi/src/api/coin_payment.rs @@ -22,7 +22,7 @@ use crate::{wire, wire_trait}; /// RFC 0017 describes `Resolvable` values for long-running operations. /// TrUAPI represents those as subscriptions whose items are the RFC status /// updates. -#[wire_trait(id = 4)] +#[wire_trait(id = 196)] #[crate::async_trait] pub trait CoinPayment: Send + Sync { /// Create a new firewalled CoinPayment purse. diff --git a/rust/crates/truapi/src/api/entropy.rs b/rust/crates/truapi/src/api/entropy.rs index cbad2ca66..bd501edd5 100644 --- a/rust/crates/truapi/src/api/entropy.rs +++ b/rust/crates/truapi/src/api/entropy.rs @@ -7,7 +7,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Deterministic entropy derivation. -#[wire_trait(id = 5)] +#[wire_trait(id = 197)] #[crate::async_trait] pub trait Entropy: Send + Sync { /// Derive deterministic entropy. diff --git a/rust/crates/truapi/src/api/local_storage.rs b/rust/crates/truapi/src/api/local_storage.rs index 1f1dc92a4..86e7b5669 100644 --- a/rust/crates/truapi/src/api/local_storage.rs +++ b/rust/crates/truapi/src/api/local_storage.rs @@ -9,7 +9,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Local key/value storage scoped to the calling product. -#[wire_trait(id = 6)] +#[wire_trait(id = 198)] #[crate::async_trait] pub trait LocalStorage: Send + Sync { /// Read a value by key. diff --git a/rust/crates/truapi/src/api/notifications.rs b/rust/crates/truapi/src/api/notifications.rs index 3701afeb3..12853a532 100644 --- a/rust/crates/truapi/src/api/notifications.rs +++ b/rust/crates/truapi/src/api/notifications.rs @@ -9,7 +9,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Notification methods for locally-rendered push notifications. -#[wire_trait(id = 7)] +#[wire_trait(id = 199)] #[crate::async_trait] pub trait Notifications: Send + Sync { /// Send a push notification to the user. diff --git a/rust/crates/truapi/src/api/payment.rs b/rust/crates/truapi/src/api/payment.rs index 31b957c47..b913957aa 100644 --- a/rust/crates/truapi/src/api/payment.rs +++ b/rust/crates/truapi/src/api/payment.rs @@ -11,7 +11,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Payment request and balance/status subscription methods. -#[wire_trait(id = 8)] +#[wire_trait(id = 200)] #[crate::async_trait] pub trait Payment: Send + Sync { /// Subscribe to payment balance updates. diff --git a/rust/crates/truapi/src/api/permissions.rs b/rust/crates/truapi/src/api/permissions.rs index d03c64101..408ee0122 100644 --- a/rust/crates/truapi/src/api/permissions.rs +++ b/rust/crates/truapi/src/api/permissions.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Permission request methods. -#[wire_trait(id = 9)] +#[wire_trait(id = 201)] #[crate::async_trait] pub trait Permissions: Send + Sync { /// Request a device-capability permission from the user. diff --git a/rust/crates/truapi/src/api/preimage.rs b/rust/crates/truapi/src/api/preimage.rs index 9a6b4c567..dbf1d521a 100644 --- a/rust/crates/truapi/src/api/preimage.rs +++ b/rust/crates/truapi/src/api/preimage.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Preimage lookup and submission methods. -#[wire_trait(id = 10)] +#[wire_trait(id = 202)] #[crate::async_trait] pub trait Preimage: Send + Sync { /// Subscribe to preimage lookups for a given key. diff --git a/rust/crates/truapi/src/api/resource_allocation.rs b/rust/crates/truapi/src/api/resource_allocation.rs index 500ac9840..30cd78031 100644 --- a/rust/crates/truapi/src/api/resource_allocation.rs +++ b/rust/crates/truapi/src/api/resource_allocation.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Resource pre-allocation (allowance management). -#[wire_trait(id = 11)] +#[wire_trait(id = 203)] #[crate::async_trait] pub trait ResourceAllocation: Send + Sync { /// Request the host to pre-allocate one or more resources. diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 8acb4f799..f121d6fe1 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -16,7 +16,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Signing operations. -#[wire_trait(id = 12)] +#[wire_trait(id = 204)] #[crate::async_trait] pub trait Signing: Send + Sync { /// Construct a transaction for a product account. diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 7554806e1..024d24070 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -13,7 +13,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Statement store methods. -#[wire_trait(id = 13)] +#[wire_trait(id = 205)] #[crate::async_trait] pub trait StatementStore: Send + Sync { /// Subscribe to statements matching a topic filter. diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 969ef3252..7c2f548ac 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -12,7 +12,7 @@ use crate::{wire, wire_trait}; /// General-purpose TrUAPI methods for handshake, feature detection, /// navigation, and runtime information. -#[wire_trait(id = 0)] +#[wire_trait(id = 192)] #[crate::async_trait] pub trait System: Send + Sync { /// Negotiate the wire codec version with the product. @@ -29,7 +29,7 @@ pub trait System: Send + Sync { request: HostHandshakeRequest, ) -> Result> { let HostHandshakeRequest::V1(version) = request; - if version.codec_version == 2 { + if version.codec_version == crate::WIRE_CODEC_VERSION { Ok(HostHandshakeResponse::V1) } else { Err(CallError::Domain(HostHandshakeError::V1( diff --git a/rust/crates/truapi/src/api/theme.rs b/rust/crates/truapi/src/api/theme.rs index 2d323f9db..cde5041e6 100644 --- a/rust/crates/truapi/src/api/theme.rs +++ b/rust/crates/truapi/src/api/theme.rs @@ -5,7 +5,7 @@ use crate::{CallContext, Subscription}; use crate::{wire, wire_trait}; /// Host theme subscription. -#[wire_trait(id = 14)] +#[wire_trait(id = 206)] #[crate::async_trait] pub trait Theme: Send + Sync { /// Subscribe to host theme changes. diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index dac95d4f2..878c7ee6f 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -201,6 +201,26 @@ pub mod latest { pub use truapi_macros::{service, wire, wire_trait}; +/// Wire codec version this crate defines. Frames address a method with a +/// `(trait, method)` byte pair; codec 1 used a single flat method byte. The +/// handshake accepts only this version, and codegen stamps it into the +/// generated clients, so every peer derives it from here. +pub const WIRE_CODEC_VERSION: u8 = 2; + +/// Highest method discriminant any codec 1 implementation assigned, and so +/// the largest first byte a codec 1 frame can carry. This crate reached 164; +/// `triangle-js-sdks` `host-api` went further, allocating 166..=171 to the +/// RFC-0024 ring VRF methods, so the bound is theirs rather than ours. +pub const MAX_CODEC_1_METHOD_ID: u8 = 171; + +/// Lowest wire trait id [`WIRE_CODEC_VERSION`] may assign. The first byte +/// after the request id is the trait, so keeping every trait id above +/// [`MAX_CODEC_1_METHOD_ID`] means a codec 1 frame can never decode into a +/// registered trait: it is reported as unroutable instead of executing +/// whichever trait happens to share its old flat id. Codegen rejects any +/// `#[wire_trait(id = N)]` below this floor. +pub const MIN_TRAIT_ID: u8 = 192; + /// Per-message id carried from the transport frame. pub type RequestId = String; diff --git a/scripts/snapshot-version.sh b/scripts/snapshot-version.sh index 1f79a86d1..8935a76a4 100755 --- a/scripts/snapshot-version.sh +++ b/scripts/snapshot-version.sh @@ -77,7 +77,7 @@ codegen_args=( --playground-output "$TMP_DIR/playground" --explorer-output "$TMP_DIR/explorer" --strip-examples - --codec-version 1 + --codec-version 2 ) if [ -n "$WIRE_VERSION" ]; then codegen_args+=(--client-version "$WIRE_VERSION") From 23ed9a0a3db6490cbee728dd8bd373da3e7ec28b Mon Sep 17 00:00:00 2001 From: Nidish Date: Mon, 10 Aug 2026 13:29:54 +0530 Subject: [PATCH 03/16] fix(wire): carry the trait byte through every post-rebase call site and fixture --- docs/design/truapi-protocol.md | 10 +- .../Tests/TrUAPIWsBridgeTests.swift | 4 +- js/packages/truapi/src/client.test.ts | 16 ++- js/packages/truapi/src/client.ts | 25 +++- rust/crates/truapi-server/src/host_core.rs | 12 +- rust/crates/truapi-server/src/subscription.rs | 115 ++++++++---------- rust/crates/truapi/src/lib.rs | 2 +- 7 files changed, 98 insertions(+), 86 deletions(-) diff --git a/docs/design/truapi-protocol.md b/docs/design/truapi-protocol.md index 791383ceb..219d4967e 100644 --- a/docs/design/truapi-protocol.md +++ b/docs/design/truapi-protocol.md @@ -59,7 +59,7 @@ The two bytes after the `requestId` are the **`(trait, method)` discriminant pai Actions are not written by hand. They are derived mechanically from the TrUAPI methods, so the high-level method signature and the wire format can never drift apart. One method expands into several actions depending on its shape: a plain call becomes a request/response pair, while a subscription becomes a small lifecycle of start, stop, interrupt, and receive messages. -Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `0` (so a handshake request frame always starts `[requestId][0x00][0x00]`). Each action carries an explicit method discriminant within its trait — its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id` — assigned per method via the `#[wire(...)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. +Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `192` — the lowest id the codec permits (see the appendix) — so a handshake request frame always starts `[requestId][0xC0][0x00]`. Each action carries an explicit method discriminant within its trait — its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id` — assigned per method via the `#[wire(...)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. Payloads are versioned independently of the discriminant pair, so a single message can evolve without renumbering anything around it. The current version `V1` encodes as discriminant `0`: @@ -268,10 +268,10 @@ Per-action mapping (codec-1 flat id → codec-2 `(trait, method)` pair): | `chat_action_subscribe_stop` | 49 | (195, 11) | | `chat_action_subscribe_interrupt` | 50 | (195, 12) | | `chat_action_subscribe_receive` | 51 | (195, 13) | -| `chat_custom_message_render_subscribe_start` | 52 | (195, 14) | -| `chat_custom_message_render_subscribe_stop` | 53 | (195, 15) | -| `chat_custom_message_render_subscribe_interrupt` | 54 | (195, 16) | -| `chat_custom_message_render_subscribe_receive` | 55 | (195, 17) | +| `chat_custom_message_render_start` | 52 | (195, 14) | +| `chat_custom_message_render_stop` | 53 | (195, 15) | +| `chat_custom_message_render_interrupt` | 54 | (195, 16) | +| `chat_custom_message_render_receive` | 55 | (195, 17) | | `coin_payment_create_purse_request` | 136 | (196, 0) | | `coin_payment_create_purse_response` | 137 | (196, 1) | | `coin_payment_query_purse_request` | 138 | (196, 2) | diff --git a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift index 6c9cc658c..0cef3725f 100644 --- a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift +++ b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift @@ -82,10 +82,10 @@ private extension TrUAPIWsBridgeTests { ) } - // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 0, request_id: 2 }. + // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 192, request_id: 2 }. // Both bytes are load-bearing: a lone method byte is read as the trait and // routes into a different trait's method 0 rather than failing. - static let featureSupportedRequestDiscriminant = Data([0x00, 0x02]) + static let featureSupportedRequestDiscriminant = Data([0xC0, 0x02]) // wire_table.rs: SYSTEM_HOST_INFO.request_id = 192 static let hostInfoRequestDiscriminant = Data([0xC0]) diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 263711248..2798544f9 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -116,7 +116,8 @@ function rendererStart( encodeWireMessage({ requestId, payload: { - id: W.CHAT_CUSTOM_MESSAGE_RENDER.start, + traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.start, value: T.VersionedProductChatCustomMessageRenderRequest.enc({ tag: "V1", value: request, @@ -132,7 +133,8 @@ function rendererReceive(requestId: string, node: T.CustomRendererNode): Uint8Ar encodeWireMessage({ requestId, payload: { - id: W.CHAT_CUSTOM_MESSAGE_RENDER.receive, + traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.receive, value: T.VersionedProductChatCustomMessageRenderItem.enc({ tag: "V1", value: node, @@ -148,7 +150,8 @@ function rendererInterrupt(requestId: string): Uint8Array { encodeWireMessage({ requestId, payload: { - id: W.CHAT_CUSTOM_MESSAGE_RENDER.interrupt, + traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.interrupt, value: new Uint8Array([0]), }, }), @@ -161,7 +164,8 @@ function rendererStop(requestId: string): Uint8Array { encodeWireMessage({ requestId, payload: { - id: W.CHAT_CUSTOM_MESSAGE_RENDER.stop, + traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.stop, value: new Uint8Array(), }, }), @@ -661,7 +665,7 @@ describe("generated client transport", () => { // The transport must survive: a ping it cannot parse is a peer // problem, not grounds for tearing down every pending call. - void client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Left", value: 0 } } }); + void client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Index", value: 0 } } }); expect(fixture.sent.length).toBe(1); }); @@ -670,7 +674,7 @@ describe("generated client transport", () => { const transport = createTransport(fixture.provider); const client = createClient(transport); - const response = client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Left", value: 0 } } }); + const response = client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Index", value: 0 } } }); // Right request id, right method id, neighbouring trait: what a whole // trait of discriminant skew looks like from the product side. diff --git a/js/packages/truapi/src/client.ts b/js/packages/truapi/src/client.ts index 0d65a3308..8df879bc6 100644 --- a/js/packages/truapi/src/client.ts +++ b/js/packages/truapi/src/client.ts @@ -180,6 +180,14 @@ function unwrapVersionedWireValue(value: unknown): unknown { return isVersionedWireValue(value) ? value.value : value; } +/** + * Map key for a `(trait, method)` wire discriminant pair. Both bytes together + * identify a frame, so neither half alone is a usable key. + */ +function pairKey(traitId: number, methodId: number): string { + return `${traitId}:${methodId}`; +} + /** * Decode `V1(UnsupportedMessage { trait_id, method_id })`. Codec 2 addresses a * frame by a pair, so the payload is four bytes: version index, error variant @@ -389,7 +397,7 @@ export function createTransport( return; } - const hostRoute = hostRoutes.get(`${payload.traitId}:${payload.methodId}`); + const hostRoute = hostRoutes.get(pairKey(payload.traitId, payload.methodId)); if (hostRoute) { startHostSubscription(hostRoute, requestId, payload.value); return; @@ -522,7 +530,8 @@ export function createTransport( send({ requestId, payload: { - id: route.ids.interrupt, + traitId: route.ids.trait, + methodId: route.ids.interrupt, value: route.interruptPayload, }, }); @@ -577,7 +586,11 @@ export function createTransport( try { send({ requestId, - payload: { id: route.ids.receive, value: route.encodeItem(item) }, + payload: { + traitId: route.ids.trait, + methodId: route.ids.receive, + value: route.encodeItem(item), + }, }); } catch { interruptHostSubscription(route, requestId); @@ -744,8 +757,8 @@ export function createTransport( interruptPayload, bufferCapacity, }: RegisterHostInitiatedSubscriptionParams) { - const routeKey = `${ids.trait}:${ids.start}`; - if (hostRoutes.has(routeKey)) { + const key = pairKey(ids.trait, ids.start); + if (hostRoutes.has(key)) { throw new Error( `host-initiated subscription (${ids.trait}, ${ids.start}) is already registered`, ); @@ -759,7 +772,7 @@ export function createTransport( buffered: [], instances: new Map(), }; - hostRoutes.set(routeKey, route); + hostRoutes.set(key, route); return { setHandler(handler: HostInitiatedSubscriptionHandler) { const installed = handler as ( diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index 3988971c4..6b0bf8840 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -1410,7 +1410,8 @@ mod tests { let frame = ProtocolMessage { request_id: "chat:1".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; @@ -1420,7 +1421,8 @@ mod tests { let frames = sink.frames.lock().unwrap(); assert_eq!(frames.len(), 1); let response = ProtocolMessage::decode(&mut frames[0].as_slice()).unwrap(); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); let expected = crate::frame::encode_versioned_err_payload( truapi::CallError::::Denied, 1, @@ -1483,7 +1485,8 @@ mod tests { let frame = ProtocolMessage { request_id: "chat:actions".into(), payload: Payload { - id: ids.start_id, + trait_id: ids.trait_id, + method_id: ids.start_id, value: Vec::new(), }, }; @@ -1494,7 +1497,8 @@ mod tests { assert_eq!(frames.len(), 1); let response = ProtocolMessage::decode(&mut frames[0].as_slice()).unwrap(); assert_eq!(response.request_id, "chat:actions"); - assert_eq!(response.payload.id, ids.interrupt_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.interrupt_id); assert!(response.payload.value.is_empty()); } diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index 0657a7572..b1545c1cb 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -396,7 +396,8 @@ impl HostInitiatedSubscriptionManager { transport.send(ProtocolMessage { request_id: request_id.clone(), payload: Payload { - id: ids.start_id, + trait_id: ids.trait_id, + method_id: ids.start_id, value: payload, }, }); @@ -426,17 +427,11 @@ impl HostInitiatedSubscriptionManager { .expect("host subscription state mutex poisoned"); let slot = state.active.get(&message.request_id)?; let key = (message.payload.trait_id, message.payload.method_id); - if key == (slot.ids.trait_id, slot.ids.receive_id) { - let sender = slot.sender.clone(); - drop(state); - let _ = sender.unbounded_send(HostInitiatedFrame::Item(message.payload.value)); - } else if key == (slot.ids.trait_id, slot.ids.interrupt_id) { - // Deliver the terminal before dropping the sender, so the stream - // reports a declining product rather than a silent end. - let sender = slot.sender.clone(); - let _ = sender.unbounded_send(HostInitiatedFrame::Interrupt); - state.active.remove(&message.request_id); - } else if key == PROTOCOL_ERROR_KEY { + // The protocol-error check MUST precede the trait guard. A protocol + // error is addressed to the reserved trait, never to this slot's, so + // guarding on the trait first would make the arm below dead code and + // silently drop the frame that reports our start as unsupported. + if key == PROTOCOL_ERROR_KEY { let Ok(VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { trait_id, method_id, @@ -452,6 +447,21 @@ impl HostInitiatedSubscriptionManager { let sender = slot.sender.clone(); let _ = sender.unbounded_send(HostInitiatedFrame::Unsupported); state.active.remove(&message.request_id); + return None; + } + if message.payload.trait_id != slot.ids.trait_id { + return None; + } + if message.payload.method_id == slot.ids.receive_id { + let sender = slot.sender.clone(); + drop(state); + let _ = sender.unbounded_send(HostInitiatedFrame::Item(message.payload.value)); + } else if message.payload.method_id == slot.ids.interrupt_id { + // Deliver the terminal before dropping the sender, so the stream + // reports a declining product rather than a silent end. + let sender = slot.sender.clone(); + let _ = sender.unbounded_send(HostInitiatedFrame::Interrupt); + state.active.remove(&message.request_id); } None } @@ -494,7 +504,8 @@ impl HostInitiatedSubscription { self.transport.send(ProtocolMessage { request_id: self.request_id.clone(), payload: Payload { - id: self.ids.stop_id, + trait_id: self.ids.trait_id, + method_id: self.ids.stop_id, value: Vec::new(), }, }); @@ -633,10 +644,23 @@ mod tests { fn host_ids() -> SubscriptionFrameIds { SubscriptionFrameIds { - start_id: 52, - stop_id: 53, - interrupt_id: 54, - receive_id: 55, + trait_id: 195, + start_id: 14, + stop_id: 15, + interrupt_id: 16, + receive_id: 17, + } + } + + /// Product frame on [`host_ids`]'s trait, carrying one of its method ids. + fn host_frame(request_id: &str, method_id: u8, value: Vec) -> ProtocolMessage { + ProtocolMessage { + request_id: request_id.into(), + payload: Payload { + trait_id: host_ids().trait_id, + method_id, + value, + }, } } @@ -648,18 +672,13 @@ mod tests { let mut subscription = manager.start::(host_ids(), vec![0xaa], transport); assert_eq!(transport_typed.sent()[0].request_id, "h:1"); - assert_eq!(transport_typed.sent()[0].payload.id, 52); + assert_eq!(transport_typed.sent()[0].payload.trait_id, 195); + assert_eq!(transport_typed.sent()[0].payload.method_id, 14); assert_eq!(transport_typed.sent()[0].payload.value, vec![0xaa]); assert!( manager - .handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - id: 55, - value: 7_u32.encode(), - }, - }) + .handle_message(host_frame("h:1", 17, 7_u32.encode())) .is_none() ); assert_eq!( @@ -671,7 +690,8 @@ mod tests { let frames = transport_typed.sent(); assert_eq!(frames.len(), 2); assert_eq!(frames[1].request_id, "h:1"); - assert_eq!(frames[1].payload.id, 53); + assert_eq!(frames[1].payload.trait_id, 195); + assert_eq!(frames[1].payload.method_id, 15); assert!(frames[1].payload.value.is_empty()); } @@ -771,20 +791,8 @@ mod tests { let mut malformed = manager.start::(host_ids(), vec![], transport.clone()); let mut healthy = manager.start::(host_ids(), vec![], transport); - manager.handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - id: 55, - value: vec![0xff], - }, - }); - manager.handle_message(ProtocolMessage { - request_id: "h:2".into(), - payload: Payload { - id: 55, - value: 9_u32.encode(), - }, - }); + manager.handle_message(host_frame("h:1", 17, vec![0xff])); + manager.handle_message(host_frame("h:2", 17, 9_u32.encode())); // A partial tree left on screen as final is the failure this prevents. assert!(matches!( @@ -794,7 +802,8 @@ mod tests { assert_eq!(futures::executor::block_on(malformed.next()), None); assert_eq!(futures::executor::block_on(healthy.next()), Some(Ok(9))); assert_eq!(transport_typed.sent()[2].request_id, "h:1"); - assert_eq!(transport_typed.sent()[2].payload.id, 53); + assert_eq!(transport_typed.sent()[2].payload.trait_id, 195); + assert_eq!(transport_typed.sent()[2].payload.method_id, 15); } #[test] @@ -804,13 +813,7 @@ mod tests { let manager = HostInitiatedSubscriptionManager::new(); let mut declined = manager.start::(host_ids(), vec![], transport); - manager.handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - id: 54, - value: vec![0], - }, - }); + manager.handle_message(host_frame("h:1", 16, vec![0])); assert!(matches!( futures::executor::block_on(declined.next()), @@ -930,20 +933,8 @@ mod tests { assert_eq!(first_transport_typed.sent()[0].request_id, "h:1"); assert_eq!(second_transport_typed.sent()[0].request_id, "h:1"); - first.handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - id: 55, - value: 7_u32.encode(), - }, - }); - second.handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - id: 55, - value: 9_u32.encode(), - }, - }); + first.handle_message(host_frame("h:1", 17, 7_u32.encode())); + second.handle_message(host_frame("h:1", 17, 9_u32.encode())); assert_eq!( futures::executor::block_on(first_render.next()), diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index 878c7ee6f..34c178889 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -208,7 +208,7 @@ pub use truapi_macros::{service, wire, wire_trait}; pub const WIRE_CODEC_VERSION: u8 = 2; /// Highest method discriminant any codec 1 implementation assigned, and so -/// the largest first byte a codec 1 frame can carry. This crate reached 164; +/// the largest first byte a codec 1 frame can carry. This crate reached 165; /// `triangle-js-sdks` `host-api` went further, allocating 166..=171 to the /// RFC-0024 ring VRF methods, so the bound is theirs rather than ours. pub const MAX_CODEC_1_METHOD_ID: u8 = 171; From 0c6fb85a685bd552b901a161e35d3eeba027936b Mon Sep 17 00:00:00 2001 From: Nidish Date: Mon, 10 Aug 2026 13:46:38 +0530 Subject: [PATCH 04/16] fix(wire): regenerate the account_get golden payload against DerivationIndex and guard it with a typed decode --- js/packages/truapi/src/wire-equality.test.ts | 29 +++++--- rust/crates/truapi-server/src/frame.rs | 5 ++ .../truapi-server/tests/golden_frame.rs | 66 ++++++++++++++---- .../tests/snapshots/golden-account-get.bin | Bin 15 -> 16 bytes 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts index d4f4764d2..b7c97e70f 100644 --- a/js/packages/truapi/src/wire-equality.test.ts +++ b/js/packages/truapi/src/wire-equality.test.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from "bun:test"; import { str } from "./scale.js"; import { decodeWireMessage, encodeWireMessage } from "./transport.js"; +import * as T from "./generated/types.js"; import * as W from "./generated/wire-table.js"; function toHex(u: Uint8Array): string { @@ -70,15 +71,21 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }); it("encodes account_get_request (pair (193, 4)) to match the golden fixture", () => { - // payload = V1(("foo", 0u32)); same vector as the Rust golden fixture. - const inner = new Uint8Array([ - 0x00, // V1 variant - ...str.enc("foo"), // compact-len + utf8 - 0x00, - 0x00, - 0x00, - 0x00, // u32 = 0 LE - ]); + // Same vector as the Rust golden fixture + // (`truapi-server/tests/snapshots/golden-account-get.bin`). Encoded + // through the generated codec rather than assembled byte by byte: a + // hand-rolled payload keeps encoding the layout it was written against + // long after the type has moved on, which is exactly how the Rust + // fixture went stale across the 0.6.0 `DerivationIndex` change. + const inner = T.VersionedHostAccountGetRequest.enc({ + tag: "V1", + value: { + productAccountId: { + dotNsIdentifier: "foo", + derivationIndex: { tag: "Index", value: 0 }, + }, + }, + }); const encoded = unwrap( encodeWireMessage({ requestId: "p:1", @@ -91,7 +98,9 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { "encode account_get_request", ); expect(toHex(encoded)).toBe(toHex(expectedWire(193, 4, inner))); - expect(toHex(encoded)).toBe("0c703a31c104000c666f6f00000000"); + // [0c 70 3a 31] "p:1" + [c1 04] pair + [00] V1 + [0c 66 6f 6f] "foo" + // + [00] DerivationIndex::Index + [00 00 00 00] u32 = 0. + expect(toHex(encoded)).toBe("0c703a31c104000c666f6f0000000000"); }); it("round-trips a local_storage_read frame through encode + decode", () => { diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index f03cecdf2..5fbc64229 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -302,6 +302,11 @@ mod tests { assert_eq!(msg.encode(), expected_wire(192, 0, &inner)); } + /// Pins where the pair lands for a multi-byte payload. The payload here is + /// an arbitrary blob, not a real `HostAccountGetRequest` — this layer + /// inlines payload bytes verbatim and never interprets them. The typed + /// layout of an `account_get_account` payload is asserted in + /// `tests/golden_frame.rs` against the golden fixture. #[test] fn get_account_request_encodes_with_discriminant_pair() { let mut inner = vec![0x00]; // V1 variant diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs index f9249a557..4a503f924 100644 --- a/rust/crates/truapi-server/tests/golden_frame.rs +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -1,52 +1,92 @@ //! Binary golden-frame regression test. //! -//! Loads `tests/snapshots/golden-account-get.bin` (the captured raw bytes -//! of an `account_get_account_request` frame) and asserts that -//! `ProtocolMessage::decode` produces the expected in-memory shape. +//! `tests/snapshots/golden-account-get.bin` holds the raw bytes of an +//! `account_get_account_request` frame. The tests assert both halves of the +//! envelope: the transport framing (`requestId` and the `(trait, method)` +//! discriminant pair) and the *typed decode of the payload*. +//! +//! Both halves are needed. The payload is inlined as opaque bytes, so a +//! `ProtocolMessage`-only assertion is satisfied by a payload of any length, +//! and round-tripping the in-memory shape cancels a symmetric layout change +//! out. Only decoding the payload into its current type reads what the bytes +//! actually say. //! //! The frame encodes: //! requestId = "p:1" //! payload = account_get_account_request, -//! inner = HostAccountGetRequest::V1(("foo", 0u32)) +//! inner = HostAccountGetRequest::V1(ProductAccountId { +//! dot_ns_identifier: "foo", +//! derivation_index: DerivationIndex::Index(0), +//! }) //! -//! On the wire (15 bytes): +//! On the wire (16 bytes): //! [0c 70 3a 31] requestId = compact-len(3) + "p:1" //! [c1] trait discriminant 193 = account //! [04] method discriminant 4 = get_account request //! [00] versioned wrapper variant V1 -//! [0c 66 6f 6f] "foo" +//! [0c 66 6f 6f] compact-len(3) + "foo" +//! [00] DerivationIndex variant Index //! [00 00 00 00] u32 = 0 //! //! If this test fails after a wire-protocol change, regenerate the file -//! deliberately and re-check the change against the wire table. +//! deliberately, re-check the change against the wire table, and treat a +//! payload layout change as breaking for every product built against an +//! older `@parity/truapi`. use parity_scale_codec::{Decode, Encode}; +use truapi::v01; +use truapi::versioned::account::HostAccountGetRequest; use truapi_server::frame::{Payload, ProtocolMessage}; use truapi_server::generated::wire_table; const GOLDEN: &[u8] = include_bytes!("snapshots/golden-account-get.bin"); +/// Payload byte count of the golden frame: one versioned-wrapper variant byte, +/// a compact-length-prefixed 3-byte identifier, one `DerivationIndex` variant +/// byte, and a `u32`. Spelled out term by term rather than measured from the +/// codec, so a layout change has to move this number by hand. +const GOLDEN_PAYLOAD_LEN: usize = 1 + 1 + 3 + 1 + 4; + +fn expected_request() -> HostAccountGetRequest { + HostAccountGetRequest::V1(v01::HostAccountGetRequest { + product_account_id: v01::ProductAccountId { + dot_ns_identifier: "foo".to_string(), + derivation_index: v01::DerivationIndex::Index(0), + }, + }) +} + #[test] fn golden_account_get_frame_decodes_to_expected_message() { let decoded = ProtocolMessage::decode(&mut &GOLDEN[..]) .expect("golden frame must decode with the current wire codec"); - let mut expected_inner = Vec::new(); - expected_inner.push(0x00u8); // V1 variant - "foo".to_string().encode_to(&mut expected_inner); - 0u32.encode_to(&mut expected_inner); - let expected = ProtocolMessage { request_id: "p:1".to_string(), payload: Payload { trait_id: wire_table::ACCOUNT_GET_ACCOUNT.trait_id, method_id: wire_table::ACCOUNT_GET_ACCOUNT.request_id, - value: expected_inner, + value: expected_request().encode(), }, }; assert_eq!(decoded, expected); } +#[test] +fn golden_account_get_payload_decodes_as_the_typed_request() { + let decoded = ProtocolMessage::decode(&mut &GOLDEN[..]).expect("decode"); + assert_eq!( + decoded.payload.value.len(), + GOLDEN_PAYLOAD_LEN, + "account_get_account request payload changed length; every product \ + built against an older @parity/truapi now fails to decode" + ); + + let request = HostAccountGetRequest::decode(&mut &decoded.payload.value[..]) + .expect("golden payload must decode as the typed request"); + assert_eq!(request, expected_request()); +} + #[test] fn golden_account_get_frame_round_trips() { // Encoding the in-memory shape must reproduce the on-disk bytes exactly. diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin index 2a9d37f11dd20652823880f620c6a67674e856c1..6bf459a6ca26ad65712614b5b9d0ef382ff288dd 100644 GIT binary patch literal 16 Ucmd-nurfTz!oZW3pU(gS03Uz?{Qv*} literal 15 Ucmd-nurfTz!oZW3pU(gU033q?{Qv*} From 61633a642071b1de79da36e7b1cfded48dceec63 Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 21 Aug 2026 14:39:58 +0530 Subject: [PATCH 05/16] fix(wire): carry the trait byte through main's new call sites --- rust/crates/truapi-server/src/host_core.rs | 6 ++++-- rust/crates/truapi-server/src/subscription.rs | 20 ++++++------------- .../truapi-server/tests/wire_result_shape.rs | 18 +++++++++++------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index 6b0bf8840..54732245a 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -1452,7 +1452,8 @@ mod tests { let frame = ProtocolMessage { request_id: "chat:bot".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; @@ -1462,7 +1463,8 @@ mod tests { let frames = sink.frames.lock().unwrap(); assert_eq!(frames.len(), 1); let response = ProtocolMessage::decode(&mut frames[0].as_slice()).unwrap(); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); let expected = crate::frame::encode_versioned_err_payload( truapi::CallError::::Denied, 1, diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index b1545c1cb..ad160bf34 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -711,26 +711,18 @@ mod tests { // One `Deeper` byte per level, terminated by `Leaf`. let mut bomb = vec![0x01; (MAX_SUBSCRIPTION_DECODE_DEPTH as usize) * 4]; bomb.push(0x00); - manager.handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - id: 55, - value: bomb, - }, - }); + manager.handle_message(host_frame("h:1", host_ids().receive_id, bomb)); assert!(matches!( futures::executor::block_on(nested.next()), Some(Err(_)) )); // A payload inside the bound still arrives, on its own subscription. - manager.handle_message(ProtocolMessage { - request_id: "h:2".into(), - payload: Payload { - id: 55, - value: NestedItem::Leaf.encode(), - }, - }); + manager.handle_message(host_frame( + "h:2", + host_ids().receive_id, + NestedItem::Leaf.encode(), + )); assert_eq!( futures::executor::block_on(healthy.next()), Some(Ok(NestedItem::Leaf)) diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index bee335622..74dc77c0a 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -83,13 +83,15 @@ fn get_chain_info_ok_response_round_trips_over_the_wire() { let frame = ProtocolMessage { request_id: "p:9".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; let response = dispatch(&core, frame); assert_eq!(response.request_id, "p:9"); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // Wire payload: [V1 disc=0x00][Ok disc=0x00][encoded response body]. let mut expected = vec![0x00u8, 0x00u8]; @@ -113,12 +115,14 @@ fn get_chain_info_unserved_chain_uses_err_discriminant() { let frame = ProtocolMessage { request_id: "p:10".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; let response = dispatch(&core, frame); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // Wire payload: [V1 disc=0x00][Err disc=0x01][encoded domain error]. let mut expected = vec![0x00u8, 0x01u8]; @@ -563,12 +567,14 @@ fn coin_payment_request_reports_unsupported_on_the_wire() { let frame = ProtocolMessage { request_id: "p:coin".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: request.encode(), }, }; let response = dispatch(&core, frame); - assert_eq!(response.payload.id, ids.response_id); + assert_eq!(response.payload.trait_id, ids.trait_id); + assert_eq!(response.payload.method_id, ids.response_id); // [V1 disc=0x00][Err disc=0x01][CallError::Unsupported=0x02], and nothing more. assert_eq!(response.payload.value, vec![0x00u8, 0x01u8, 0x02u8]); } From c5a19ae90d3176a793c911770dce7c4bc77df31f Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 28 Aug 2026 13:31:02 +0530 Subject: [PATCH 06/16] fix(wire): rebase onto main, allocate trait-relative ids for host_info and get_product_context --- js/packages/truapi/src/client.test.ts | 3 ++- .../truapi-server/tests/device_permission_revalidation.rs | 7 +++++-- rust/crates/truapi/src/api/system.rs | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 2798544f9..e4f1e7427 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -280,7 +280,8 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: "p:1", payload: { - id: W.SYSTEM_GET_PRODUCT_CONTEXT.response, + traitId: W.SYSTEM_GET_PRODUCT_CONTEXT.trait, + methodId: W.SYSTEM_GET_PRODUCT_CONTEXT.response, value: versionedV1( ScaleResult( T.HostGetProductContextResponse, diff --git a/rust/crates/truapi-server/tests/device_permission_revalidation.rs b/rust/crates/truapi-server/tests/device_permission_revalidation.rs index 093b4fac2..eebf2f104 100644 --- a/rust/crates/truapi-server/tests/device_permission_revalidation.rs +++ b/rust/crates/truapi-server/tests/device_permission_revalidation.rs @@ -72,7 +72,8 @@ fn request_camera(status: Option>) -> bool { let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { - id: ids.request_id, + trait_id: ids.trait_id, + method_id: ids.request_id, value: HostDevicePermissionRequest::V1(v01::HostDevicePermissionRequest::Camera) .encode(), }, @@ -84,7 +85,9 @@ fn request_camera(status: Option>) -> bool { let response = frames .iter() .map(|bytes| ProtocolMessage::decode(&mut &bytes[..]).expect("decode emitted frame")) - .find(|message| message.payload.id == ids.response_id) + .find(|message| { + message.payload.trait_id == ids.trait_id && message.payload.method_id == ids.response_id + }) .expect("dispatcher emitted a device-permission response"); // Wire payload is [version disc][Ok disc][body]. Assert the whole thing diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 7c2f548ac..9961ae1b8 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -96,7 +96,7 @@ pub trait System: Send + Sync { /// const info = result.value; /// console.log(`${info.name} ${info.version} on ${info.platform}`); /// ``` - #[wire(request_id = 192)] + #[wire(request_id = 6)] async fn host_info( &self, cx: &CallContext, @@ -110,7 +110,7 @@ pub trait System: Send + Sync { /// assert(context.isOk(), "getProductContext failed:", context); /// console.log("product id:", context.value.productId); /// ``` - #[wire(request_id = 190)] + #[wire(request_id = 8)] async fn get_product_context( &self, _cx: &CallContext, From 165c8c1fab105ee871c1101067e1744423b6b2ff Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 28 Aug 2026 13:59:37 +0530 Subject: [PATCH 07/16] fix(wire): move the trait-id floor past main's true codec 1 ceiling main independently extended codec 1's flat numbering to 192 via System::host_info, matching this branch's MIN_TRAIT_ID exactly and breaking the invariant that a codec 1 frame can never look like a valid codec 2 trait id. Every trait id shifts +1 (193..207), MAX_CODEC_1_METHOD_ID moves to 192 and MIN_TRAIT_ID to 193 so the floor is strictly above the known ceiling again, and every fixture, golden file, and generated artifact that pinned the old numbers is regenerated or hand-updated to match. --- js/packages/truapi/src/client.test.ts | 4 +- js/packages/truapi/src/wire-equality.test.ts | 22 +++++----- rust/crates/truapi-codegen/src/rust.rs | 6 +-- rust/crates/truapi-codegen/src/ts.rs | 6 +-- rust/crates/truapi-server/src/frame.rs | 40 +++++++++--------- .../truapi-server/tests/golden_frame.rs | 2 +- .../tests/snapshots/golden-account-get.bin | Bin 16 -> 16 bytes rust/crates/truapi/src/api/account.rs | 2 +- rust/crates/truapi/src/api/chain.rs | 2 +- rust/crates/truapi/src/api/chat.rs | 2 +- rust/crates/truapi/src/api/coin_payment.rs | 2 +- rust/crates/truapi/src/api/entropy.rs | 2 +- rust/crates/truapi/src/api/local_storage.rs | 2 +- rust/crates/truapi/src/api/notifications.rs | 2 +- rust/crates/truapi/src/api/payment.rs | 2 +- rust/crates/truapi/src/api/permissions.rs | 2 +- rust/crates/truapi/src/api/preimage.rs | 2 +- .../truapi/src/api/resource_allocation.rs | 2 +- rust/crates/truapi/src/api/signing.rs | 2 +- rust/crates/truapi/src/api/statement_store.rs | 2 +- rust/crates/truapi/src/api/system.rs | 2 +- rust/crates/truapi/src/api/theme.rs | 2 +- rust/crates/truapi/src/lib.rs | 12 +++--- 23 files changed, 62 insertions(+), 60 deletions(-) diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index e4f1e7427..53344b207 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -220,7 +220,7 @@ describe("generated client transport", () => { const expectedPayload = T.VersionedHostAccountGetRequest.enc({ tag: "V1", value: request }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 193; // account trait + expectedFrame[str.enc("p:1").length] = 194; // account trait expectedFrame[str.enc("p:1").length + 1] = 4; // get_account request expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); @@ -240,7 +240,7 @@ describe("generated client transport", () => { }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 192; // system trait + expectedFrame[str.enc("p:1").length] = 193; // system trait expectedFrame[str.enc("p:1").length + 1] = 0; // handshake request expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts index b7c97e70f..985d9efba 100644 --- a/js/packages/truapi/src/wire-equality.test.ts +++ b/js/packages/truapi/src/wire-equality.test.ts @@ -40,12 +40,12 @@ function unwrap(result: Result, message: string): T { } describe("encodeWireMessage / decodeWireMessage wire equality", () => { - it("pins the handshake frame end-to-end: requestId + 0xc0 0x00 + payload", () => { - // Trait 192 = system, method 0 = handshake request. This locks the + it("pins the handshake frame end-to-end: requestId + 0xc1 0x00 + payload", () => { + // Trait 193 = system, method 0 = handshake request. This locks the // system trait to the first id above the codec 1 flat-method range: // the handshake is the first frame either side sends, so its envelope // must never drift, and a codec 1 peer's frame must never reach it. - expect(W.SYSTEM_HANDSHAKE.trait).toBe(192); + expect(W.SYSTEM_HANDSHAKE.trait).toBe(193); expect(W.SYSTEM_HANDSHAKE.request).toBe(0); const inner = new Uint8Array([0x00, 0x02]); // V1 variant + codec_version=2 @@ -60,17 +60,17 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }), "encode handshake_request", ); - // [0c 70 3a 31] "p:1" + [c0] system trait + [00] handshake request + payload. - expect(toHex(encoded)).toBe("0c703a31c0000002"); - expect(toHex(encoded)).toBe(toHex(expectedWire(192, 0, inner))); + // [0c 70 3a 31] "p:1" + [c1] system trait + [00] handshake request + payload. + expect(toHex(encoded)).toBe("0c703a31c1000002"); + expect(toHex(encoded)).toBe(toHex(expectedWire(193, 0, inner))); const decoded = unwrap(decodeWireMessage(encoded), "decode handshake_request"); - expect(decoded.payload.traitId).toBe(192); + expect(decoded.payload.traitId).toBe(193); expect(decoded.payload.methodId).toBe(0); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); - it("encodes account_get_request (pair (193, 4)) to match the golden fixture", () => { + it("encodes account_get_request (pair (194, 4)) to match the golden fixture", () => { // Same vector as the Rust golden fixture // (`truapi-server/tests/snapshots/golden-account-get.bin`). Encoded // through the generated codec rather than assembled byte by byte: a @@ -97,10 +97,10 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }), "encode account_get_request", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(193, 4, inner))); - // [0c 70 3a 31] "p:1" + [c1 04] pair + [00] V1 + [0c 66 6f 6f] "foo" + expect(toHex(encoded)).toBe(toHex(expectedWire(194, 4, inner))); + // [0c 70 3a 31] "p:1" + [c2 04] pair + [00] V1 + [0c 66 6f 6f] "foo" // + [00] DerivationIndex::Index + [00 00 00 00] u32 = 0. - expect(toHex(encoded)).toBe("0c703a31c104000c666f6f0000000000"); + expect(toHex(encoded)).toBe("0c703a31c204000c666f6f0000000000"); }); it("round-trips a local_storage_read frame through encode + decode", () => { diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index e58b76a6c..5d9f0080c 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -270,7 +270,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(192), + wire_trait_id: Some(193), methods: vec![make_subscription_method("connection_status_subscribe", 18)], docs: None, }], @@ -674,7 +674,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(192), + wire_trait_id: Some(193), methods: vec![method], docs: None, }], @@ -723,7 +723,7 @@ mod tests { traits: vec![TraitDef { name: "Account".to_string(), module_path: Vec::new(), - wire_trait_id: Some(192), + wire_trait_id: Some(193), methods: vec![method], docs: None, }], diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index d0bf81bed..adfb98c61 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -3022,14 +3022,14 @@ mod tests { TraitDef { name: "Alpha".to_string(), module_path: Vec::new(), - wire_trait_id: Some(192), + wire_trait_id: Some(193), methods: vec![request_method("first", Some(0))], docs: None, }, TraitDef { name: "Beta".to_string(), module_path: Vec::new(), - wire_trait_id: Some(193), + wire_trait_id: Some(194), methods: vec![request_method("second", Some(0))], docs: None, }, @@ -3042,8 +3042,8 @@ mod tests { assert!(source.contains("export const ALPHA_FIRST = {")); assert!(source.contains("export const BETA_SECOND = {")); - assert!(source.contains(" trait: 192,")); assert!(source.contains(" trait: 193,")); + assert!(source.contains(" trait: 194,")); } /// Two traits must not share a wire trait id. diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index 5fbc64229..ea0f0526b 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -297,9 +297,9 @@ mod tests { fn handshake_request_encodes_with_the_system_trait_pair() { // SCALE-encoded HostHandshakeRequest::V1(2u8) = [0u8 variant][2u8 codec_version] let inner: Vec = vec![0x00, 0x02]; - // system trait = 192, handshake request = 0. - let msg = build(192, 0, inner.clone()); - assert_eq!(msg.encode(), expected_wire(192, 0, &inner)); + // system trait = 193, handshake request = 0. + let msg = build(193, 0, inner.clone()); + assert_eq!(msg.encode(), expected_wire(193, 0, &inner)); } /// Pins where the pair lands for a multi-byte payload. The payload here is @@ -312,15 +312,15 @@ mod tests { let mut inner = vec![0x00]; // V1 variant "foo".to_string().encode_to(&mut inner); 0u32.encode_to(&mut inner); - // account trait = 193, get_account request = 4. - let msg = build(193, 4, inner.clone()); - assert_eq!(msg.encode(), expected_wire(193, 4, &inner)); + // account trait = 194, get_account request = 4. + let msg = build(194, 4, inner.clone()); + assert_eq!(msg.encode(), expected_wire(194, 4, &inner)); } #[test] fn round_trip_preserves_ids_and_value() { let inner: Vec = vec![0x00, 0x42, 0xab, 0xcd]; - let msg = build(198, 0, inner.clone()); + let msg = build(199, 0, inner.clone()); let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); assert_eq!(decoded, msg); } @@ -382,7 +382,7 @@ mod tests { /// regression where `Decode` mishandles a frame whose payload is empty for /// `_stop` / `_interrupt` (no inner data) but non-empty for `_start` / /// `_receive`. The ids are the `account_connection_status_subscribe` - /// quartet (trait 193, methods 0..=3). + /// quartet (trait 194, methods 0..=3). #[test] fn subscription_phases_round_trip_through_codec() { let cases: &[(u8, Vec)] = &[ @@ -392,11 +392,11 @@ mod tests { (3, vec![0x01, 0x02, 0x03, 0x04]), // receive ]; for (method_id, value) in cases { - let msg = build(193, *method_id, value.clone()); + let msg = build(194, *method_id, value.clone()); let bytes = msg.encode(); assert_eq!( bytes, - expected_wire(193, *method_id, value), + expected_wire(194, *method_id, value), "encode mismatch for method id {method_id}" ); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); @@ -412,17 +412,17 @@ mod tests { #[test] fn id_helpers_resolve_known_methods() { let handshake = request_ids("system_handshake").expect("known request method"); - assert_eq!(handshake.trait_id, 192); + assert_eq!(handshake.trait_id, 193); assert_eq!(handshake.request_id, 0); assert_eq!(handshake.response_id, 1); let get_account = request_ids("account_get_account").expect("known request method"); - assert_eq!(get_account.trait_id, 193); + assert_eq!(get_account.trait_id, 194); assert_eq!(get_account.request_id, 4); let sub = subscription_ids("account_connection_status_subscribe").expect("known subscription"); - assert_eq!(sub.trait_id, 193); + assert_eq!(sub.trait_id, 194); assert_eq!(sub.start_id, 0); assert_eq!(sub.stop_id, 1); assert_eq!(sub.interrupt_id, 2); @@ -438,10 +438,10 @@ mod tests { /// handle `remaining_len == 0` without erroring or reading past EOF. #[test] fn empty_payload_round_trips() { - // local_storage_clear_response = (198, 5). - let msg = build(198, 5, Vec::new()); + // local_storage_clear_response = (199, 5). + let msg = build(199, 5, Vec::new()); let bytes = msg.encode(); - // [SCALE compact-len 0x0c][p][:][1][u8 198][u8 5] = 4 + 2 = 6 bytes total + // [SCALE compact-len 0x0c][p][:][1][u8 199][u8 5] = 4 + 2 = 6 bytes total assert_eq!(bytes.len(), 6); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); assert_eq!(decoded, msg); @@ -455,7 +455,7 @@ mod tests { let msg = ProtocolMessage { request_id: long_id, payload: Payload { - trait_id: 193, + trait_id: 194, method_id: 4, value: vec![0x00, 0xab, 0xcd], }, @@ -501,7 +501,7 @@ mod tests { let msg = ProtocolMessage { request_id: String::new(), payload: Payload { - trait_id: 193, + trait_id: 194, method_id: 4, value: vec![0x00, 0x01, 0x02], }, @@ -519,7 +519,7 @@ mod tests { let msg = ProtocolMessage { request_id: "héllo-世界-🦀".to_string(), payload: Payload { - trait_id: 193, + trait_id: 194, method_id: 4, value: vec![0x00, 0x01], }, @@ -533,7 +533,7 @@ mod tests { #[test] fn large_payload_round_trips() { let big = vec![0xa5u8; 100 * 1024]; - let msg = build(193, 4, big); + let msg = build(194, 4, big); let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); assert_eq!(decoded, msg); } diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs index 4a503f924..849fd5bd6 100644 --- a/rust/crates/truapi-server/tests/golden_frame.rs +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -21,7 +21,7 @@ //! //! On the wire (16 bytes): //! [0c 70 3a 31] requestId = compact-len(3) + "p:1" -//! [c1] trait discriminant 193 = account +//! [c2] trait discriminant 194 = account //! [04] method discriminant 4 = get_account request //! [00] versioned wrapper variant V1 //! [0c 66 6f 6f] compact-len(3) + "foo" diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin index 6bf459a6ca26ad65712614b5b9d0ef382ff288dd..8bb6351121f63e01ca64b62425fd8c3925ff4618 100644 GIT binary patch literal 16 Ucmd-nurfTv!oZW3pU(gS03VD3{r~^~ literal 16 Ucmd-nurfTz!oZW3pU(gS03Uz?{Qv*} diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index 78e3dbd9d..0e23c4829 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -18,7 +18,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Account lookup, aliasing, and proof generation. -#[wire_trait(id = 193)] +#[wire_trait(id = 194)] #[crate::async_trait] pub trait Account: Send + Sync { /// Subscribe to account connection status changes. diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index b6a922093..f21bdaccf 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -23,7 +23,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Chain interaction methods. -#[wire_trait(id = 194)] +#[wire_trait(id = 195)] #[crate::async_trait] pub trait Chain: Send + Sync { /// Follow the chain head and receive block events. diff --git a/rust/crates/truapi/src/api/chat.rs b/rust/crates/truapi/src/api/chat.rs index 91d065a05..40a08394f 100644 --- a/rust/crates/truapi/src/api/chat.rs +++ b/rust/crates/truapi/src/api/chat.rs @@ -11,7 +11,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Chat room, bot, and message APIs. -#[wire_trait(id = 195)] +#[wire_trait(id = 196)] #[crate::service(required_execution = Worker)] #[crate::async_trait] pub trait Chat: Send + Sync { diff --git a/rust/crates/truapi/src/api/coin_payment.rs b/rust/crates/truapi/src/api/coin_payment.rs index 3829c56cc..9fc424c97 100644 --- a/rust/crates/truapi/src/api/coin_payment.rs +++ b/rust/crates/truapi/src/api/coin_payment.rs @@ -22,7 +22,7 @@ use crate::{wire, wire_trait}; /// RFC 0017 describes `Resolvable` values for long-running operations. /// TrUAPI represents those as subscriptions whose items are the RFC status /// updates. -#[wire_trait(id = 196)] +#[wire_trait(id = 197)] #[crate::async_trait] pub trait CoinPayment: Send + Sync { /// Create a new firewalled CoinPayment purse. diff --git a/rust/crates/truapi/src/api/entropy.rs b/rust/crates/truapi/src/api/entropy.rs index bd501edd5..5cf7a17ab 100644 --- a/rust/crates/truapi/src/api/entropy.rs +++ b/rust/crates/truapi/src/api/entropy.rs @@ -7,7 +7,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Deterministic entropy derivation. -#[wire_trait(id = 197)] +#[wire_trait(id = 198)] #[crate::async_trait] pub trait Entropy: Send + Sync { /// Derive deterministic entropy. diff --git a/rust/crates/truapi/src/api/local_storage.rs b/rust/crates/truapi/src/api/local_storage.rs index 86e7b5669..5f9107118 100644 --- a/rust/crates/truapi/src/api/local_storage.rs +++ b/rust/crates/truapi/src/api/local_storage.rs @@ -9,7 +9,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Local key/value storage scoped to the calling product. -#[wire_trait(id = 198)] +#[wire_trait(id = 199)] #[crate::async_trait] pub trait LocalStorage: Send + Sync { /// Read a value by key. diff --git a/rust/crates/truapi/src/api/notifications.rs b/rust/crates/truapi/src/api/notifications.rs index 12853a532..e0b7e58ff 100644 --- a/rust/crates/truapi/src/api/notifications.rs +++ b/rust/crates/truapi/src/api/notifications.rs @@ -9,7 +9,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Notification methods for locally-rendered push notifications. -#[wire_trait(id = 199)] +#[wire_trait(id = 200)] #[crate::async_trait] pub trait Notifications: Send + Sync { /// Send a push notification to the user. diff --git a/rust/crates/truapi/src/api/payment.rs b/rust/crates/truapi/src/api/payment.rs index b913957aa..94f8ff12a 100644 --- a/rust/crates/truapi/src/api/payment.rs +++ b/rust/crates/truapi/src/api/payment.rs @@ -11,7 +11,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Payment request and balance/status subscription methods. -#[wire_trait(id = 200)] +#[wire_trait(id = 201)] #[crate::async_trait] pub trait Payment: Send + Sync { /// Subscribe to payment balance updates. diff --git a/rust/crates/truapi/src/api/permissions.rs b/rust/crates/truapi/src/api/permissions.rs index 408ee0122..19b9c022e 100644 --- a/rust/crates/truapi/src/api/permissions.rs +++ b/rust/crates/truapi/src/api/permissions.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Permission request methods. -#[wire_trait(id = 201)] +#[wire_trait(id = 202)] #[crate::async_trait] pub trait Permissions: Send + Sync { /// Request a device-capability permission from the user. diff --git a/rust/crates/truapi/src/api/preimage.rs b/rust/crates/truapi/src/api/preimage.rs index dbf1d521a..98a627fdf 100644 --- a/rust/crates/truapi/src/api/preimage.rs +++ b/rust/crates/truapi/src/api/preimage.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Preimage lookup and submission methods. -#[wire_trait(id = 202)] +#[wire_trait(id = 203)] #[crate::async_trait] pub trait Preimage: Send + Sync { /// Subscribe to preimage lookups for a given key. diff --git a/rust/crates/truapi/src/api/resource_allocation.rs b/rust/crates/truapi/src/api/resource_allocation.rs index 30cd78031..e303f0438 100644 --- a/rust/crates/truapi/src/api/resource_allocation.rs +++ b/rust/crates/truapi/src/api/resource_allocation.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Resource pre-allocation (allowance management). -#[wire_trait(id = 203)] +#[wire_trait(id = 204)] #[crate::async_trait] pub trait ResourceAllocation: Send + Sync { /// Request the host to pre-allocate one or more resources. diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index f121d6fe1..1fbaf9ff9 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -16,7 +16,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Signing operations. -#[wire_trait(id = 204)] +#[wire_trait(id = 205)] #[crate::async_trait] pub trait Signing: Send + Sync { /// Construct a transaction for a product account. diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 024d24070..3f739e554 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -13,7 +13,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Statement store methods. -#[wire_trait(id = 205)] +#[wire_trait(id = 206)] #[crate::async_trait] pub trait StatementStore: Send + Sync { /// Subscribe to statements matching a topic filter. diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 9961ae1b8..58e48ffd0 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -12,7 +12,7 @@ use crate::{wire, wire_trait}; /// General-purpose TrUAPI methods for handshake, feature detection, /// navigation, and runtime information. -#[wire_trait(id = 192)] +#[wire_trait(id = 193)] #[crate::async_trait] pub trait System: Send + Sync { /// Negotiate the wire codec version with the product. diff --git a/rust/crates/truapi/src/api/theme.rs b/rust/crates/truapi/src/api/theme.rs index cde5041e6..07c41ca52 100644 --- a/rust/crates/truapi/src/api/theme.rs +++ b/rust/crates/truapi/src/api/theme.rs @@ -5,7 +5,7 @@ use crate::{CallContext, Subscription}; use crate::{wire, wire_trait}; /// Host theme subscription. -#[wire_trait(id = 206)] +#[wire_trait(id = 207)] #[crate::async_trait] pub trait Theme: Send + Sync { /// Subscribe to host theme changes. diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index 34c178889..4ead6d3d1 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -208,10 +208,12 @@ pub use truapi_macros::{service, wire, wire_trait}; pub const WIRE_CODEC_VERSION: u8 = 2; /// Highest method discriminant any codec 1 implementation assigned, and so -/// the largest first byte a codec 1 frame can carry. This crate reached 165; -/// `triangle-js-sdks` `host-api` went further, allocating 166..=171 to the -/// RFC-0024 ring VRF methods, so the bound is theirs rather than ours. -pub const MAX_CODEC_1_METHOD_ID: u8 = 171; +/// the largest first byte a codec 1 frame can carry. `triangle-js-sdks` +/// `host-api` allocated 166..=171 to the RFC-0024 ring VRF methods; this +/// crate's own flat numbering later reached 192 via `System::host_info`, +/// added on `main` while codec 2 was still unmerged. 192 is the highest +/// known codec 1 discriminant across both, so it sets the ceiling. +pub const MAX_CODEC_1_METHOD_ID: u8 = 192; /// Lowest wire trait id [`WIRE_CODEC_VERSION`] may assign. The first byte /// after the request id is the trait, so keeping every trait id above @@ -219,7 +221,7 @@ pub const MAX_CODEC_1_METHOD_ID: u8 = 171; /// registered trait: it is reported as unroutable instead of executing /// whichever trait happens to share its old flat id. Codegen rejects any /// `#[wire_trait(id = N)]` below this floor. -pub const MIN_TRAIT_ID: u8 = 192; +pub const MIN_TRAIT_ID: u8 = 193; /// Per-message id carried from the transport frame. pub type RequestId = String; From 52739bbc58f4b80f4a1e970740a0140a222f4db4 Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 28 Aug 2026 19:29:06 +0530 Subject: [PATCH 08/16] fix(wire): correct the trait-id appendix to the 193 floor the code actually uses --- docs/design/truapi-protocol.md | 366 +++++++++++++++++---------------- 1 file changed, 185 insertions(+), 181 deletions(-) diff --git a/docs/design/truapi-protocol.md b/docs/design/truapi-protocol.md index 219d4967e..77521ebde 100644 --- a/docs/design/truapi-protocol.md +++ b/docs/design/truapi-protocol.md @@ -59,7 +59,7 @@ The two bytes after the `requestId` are the **`(trait, method)` discriminant pai Actions are not written by hand. They are derived mechanically from the TrUAPI methods, so the high-level method signature and the wire format can never drift apart. One method expands into several actions depending on its shape: a plain call becomes a request/response pair, while a subscription becomes a small lifecycle of start, stop, interrupt, and receive messages. -Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `192` — the lowest id the codec permits (see the appendix) — so a handshake request frame always starts `[requestId][0xC0][0x00]`. Each action carries an explicit method discriminant within its trait — its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id` — assigned per method via the `#[wire(...)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. +Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `193` — the lowest id the codec permits (see the appendix) — so a handshake request frame always starts `[requestId][0xC1][0x00]`. Each action carries an explicit method discriminant within its trait — its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id` — assigned per method via the `#[wire(...)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. Payloads are versioned independently of the discriminant pair, so a single message can evolve without renumbering anything around it. The current version `V1` encodes as discriminant `0`: @@ -174,191 +174,195 @@ The concrete handshake request, response, and error types are defined in the `tr Codec version 1 used a single flat `u8` discriminant shared across all traits. Codec version 2 replaces it with the `(trait, method)` pair. This table is the one-time mapping between the two numberings; it exists only to interpret captured codec-1 traffic and old fixtures, and is never extended — new methods only ever get codec-2 pairs. -Trait id assignment. Ids start at 192 (`truapi::MIN_TRAIT_ID`): no codec-1 -implementation allocated a flat discriminant above 171, so no codec-1 frame's -first byte can name a codec-2 trait, and such a frame is reported as unroutable instead of decoding -into whichever trait would otherwise share its old id. Codegen rejects any -`#[wire_trait(id = N)]` below the floor. +Trait id assignment. Ids start at 193 (`truapi::MIN_TRAIT_ID`), one past +`truapi::MAX_CODEC_1_METHOD_ID`: `triangle-js-sdks` `host-api` allocated +166..=171 to the RFC-0024 ring VRF methods, and this crate's own flat numbering +later reached 192 via `System::host_info`, added on `main` while codec 2 was +still unmerged. 192 is therefore the highest known codec-1 discriminant across +both, so no codec-1 frame's first byte can name a codec-2 trait, and such a +frame is reported as unroutable instead of decoding into whichever trait would +otherwise share its old id. Codegen rejects any `#[wire_trait(id = N)]` below +the floor. | Trait | Trait id | | --- | --- | -| `System` | 192 | -| `Account` | 193 | -| `Chain` | 194 | -| `Chat` | 195 | -| `CoinPayment` | 196 | -| `Entropy` | 197 | -| `LocalStorage` | 198 | -| `Notifications` | 199 | -| `Payment` | 200 | -| `Permissions` | 201 | -| `Preimage` | 202 | -| `ResourceAllocation` | 203 | -| `Signing` | 204 | -| `StatementStore` | 205 | -| `Theme` | 206 | +| `System` | 193 | +| `Account` | 194 | +| `Chain` | 195 | +| `Chat` | 196 | +| `CoinPayment` | 197 | +| `Entropy` | 198 | +| `LocalStorage` | 199 | +| `Notifications` | 200 | +| `Payment` | 201 | +| `Permissions` | 202 | +| `Preimage` | 203 | +| `ResourceAllocation` | 204 | +| `Signing` | 205 | +| `StatementStore` | 206 | +| `Theme` | 207 | Per-action mapping (codec-1 flat id → codec-2 `(trait, method)` pair): | Action | Codec-1 id | Codec-2 (trait, method) | | --- | --- | --- | -| `system_handshake_request` | 0 | (192, 0) | -| `system_handshake_response` | 1 | (192, 1) | -| `system_feature_supported_request` | 2 | (192, 2) | -| `system_feature_supported_response` | 3 | (192, 3) | -| `system_navigate_to_request` | 6 | (192, 4) | -| `system_navigate_to_response` | 7 | (192, 5) | -| `account_connection_status_subscribe_start` | 18 | (193, 0) | -| `account_connection_status_subscribe_stop` | 19 | (193, 1) | -| `account_connection_status_subscribe_interrupt` | 20 | (193, 2) | -| `account_connection_status_subscribe_receive` | 21 | (193, 3) | -| `account_get_account_request` | 22 | (193, 4) | -| `account_get_account_response` | 23 | (193, 5) | -| `account_get_account_alias_request` | 24 | (193, 6) | -| `account_get_account_alias_response` | 25 | (193, 7) | -| `account_create_account_proof_request` | 26 | (193, 8) | -| `account_create_account_proof_response` | 27 | (193, 9) | -| `account_get_legacy_accounts_request` | 28 | (193, 10) | -| `account_get_legacy_accounts_response` | 29 | (193, 11) | -| `account_get_user_id_request` | 110 | (193, 12) | -| `account_get_user_id_response` | 111 | (193, 13) | -| `account_request_login_request` | 112 | (193, 14) | -| `account_request_login_response` | 113 | (193, 15) | -| `account_sign_vrf_request` | 164 | (193, 16) | -| `account_sign_vrf_response` | 165 | (193, 17) | -| `chain_follow_head_subscribe_start` | 76 | (194, 0) | -| `chain_follow_head_subscribe_stop` | 77 | (194, 1) | -| `chain_follow_head_subscribe_interrupt` | 78 | (194, 2) | -| `chain_follow_head_subscribe_receive` | 79 | (194, 3) | -| `chain_get_head_header_request` | 80 | (194, 4) | -| `chain_get_head_header_response` | 81 | (194, 5) | -| `chain_get_head_body_request` | 82 | (194, 6) | -| `chain_get_head_body_response` | 83 | (194, 7) | -| `chain_get_head_storage_request` | 84 | (194, 8) | -| `chain_get_head_storage_response` | 85 | (194, 9) | -| `chain_call_head_request` | 86 | (194, 10) | -| `chain_call_head_response` | 87 | (194, 11) | -| `chain_unpin_head_request` | 88 | (194, 12) | -| `chain_unpin_head_response` | 89 | (194, 13) | -| `chain_continue_head_request` | 90 | (194, 14) | -| `chain_continue_head_response` | 91 | (194, 15) | -| `chain_stop_head_operation_request` | 92 | (194, 16) | -| `chain_stop_head_operation_response` | 93 | (194, 17) | -| `chain_get_spec_genesis_hash_request` | 94 | (194, 18) | -| `chain_get_spec_genesis_hash_response` | 95 | (194, 19) | -| `chain_get_spec_chain_name_request` | 96 | (194, 20) | -| `chain_get_spec_chain_name_response` | 97 | (194, 21) | -| `chain_get_spec_properties_request` | 98 | (194, 22) | -| `chain_get_spec_properties_response` | 99 | (194, 23) | -| `chain_broadcast_transaction_request` | 100 | (194, 24) | -| `chain_broadcast_transaction_response` | 101 | (194, 25) | -| `chain_stop_transaction_request` | 102 | (194, 26) | -| `chain_stop_transaction_response` | 103 | (194, 27) | -| `chat_create_room_request` | 38 | (195, 0) | -| `chat_create_room_response` | 39 | (195, 1) | -| `chat_register_bot_request` | 40 | (195, 2) | -| `chat_register_bot_response` | 41 | (195, 3) | -| `chat_list_subscribe_start` | 42 | (195, 4) | -| `chat_list_subscribe_stop` | 43 | (195, 5) | -| `chat_list_subscribe_interrupt` | 44 | (195, 6) | -| `chat_list_subscribe_receive` | 45 | (195, 7) | -| `chat_post_message_request` | 46 | (195, 8) | -| `chat_post_message_response` | 47 | (195, 9) | -| `chat_action_subscribe_start` | 48 | (195, 10) | -| `chat_action_subscribe_stop` | 49 | (195, 11) | -| `chat_action_subscribe_interrupt` | 50 | (195, 12) | -| `chat_action_subscribe_receive` | 51 | (195, 13) | -| `chat_custom_message_render_start` | 52 | (195, 14) | -| `chat_custom_message_render_stop` | 53 | (195, 15) | -| `chat_custom_message_render_interrupt` | 54 | (195, 16) | -| `chat_custom_message_render_receive` | 55 | (195, 17) | -| `coin_payment_create_purse_request` | 136 | (196, 0) | -| `coin_payment_create_purse_response` | 137 | (196, 1) | -| `coin_payment_query_purse_request` | 138 | (196, 2) | -| `coin_payment_query_purse_response` | 139 | (196, 3) | -| `coin_payment_rebalance_purse_start` | 140 | (196, 4) | -| `coin_payment_rebalance_purse_stop` | 141 | (196, 5) | -| `coin_payment_rebalance_purse_interrupt` | 142 | (196, 6) | -| `coin_payment_rebalance_purse_receive` | 143 | (196, 7) | -| `coin_payment_delete_purse_start` | 144 | (196, 8) | -| `coin_payment_delete_purse_stop` | 145 | (196, 9) | -| `coin_payment_delete_purse_interrupt` | 146 | (196, 10) | -| `coin_payment_delete_purse_receive` | 147 | (196, 11) | -| `coin_payment_create_receivable_request` | 148 | (196, 12) | -| `coin_payment_create_receivable_response` | 149 | (196, 13) | -| `coin_payment_create_cheque_request` | 150 | (196, 14) | -| `coin_payment_create_cheque_response` | 151 | (196, 15) | -| `coin_payment_deposit_start` | 152 | (196, 16) | -| `coin_payment_deposit_stop` | 153 | (196, 17) | -| `coin_payment_deposit_interrupt` | 154 | (196, 18) | -| `coin_payment_deposit_receive` | 155 | (196, 19) | -| `coin_payment_refund_start` | 156 | (196, 20) | -| `coin_payment_refund_stop` | 157 | (196, 21) | -| `coin_payment_refund_interrupt` | 158 | (196, 22) | -| `coin_payment_refund_receive` | 159 | (196, 23) | -| `coin_payment_listen_for_payment_start` | 160 | (196, 24) | -| `coin_payment_listen_for_payment_stop` | 161 | (196, 25) | -| `coin_payment_listen_for_payment_interrupt` | 162 | (196, 26) | -| `coin_payment_listen_for_payment_receive` | 163 | (196, 27) | -| `entropy_derive_request` | 108 | (197, 0) | -| `entropy_derive_response` | 109 | (197, 1) | -| `local_storage_read_request` | 12 | (198, 0) | -| `local_storage_read_response` | 13 | (198, 1) | -| `local_storage_write_request` | 14 | (198, 2) | -| `local_storage_write_response` | 15 | (198, 3) | -| `local_storage_clear_request` | 16 | (198, 4) | -| `local_storage_clear_response` | 17 | (198, 5) | -| `notifications_send_push_notification_request` | 4 | (199, 0) | -| `notifications_send_push_notification_response` | 5 | (199, 1) | -| `notifications_cancel_push_notification_request` | 134 | (199, 2) | -| `notifications_cancel_push_notification_response` | 135 | (199, 3) | -| `payment_balance_subscribe_start` | 118 | (200, 0) | -| `payment_balance_subscribe_stop` | 119 | (200, 1) | -| `payment_balance_subscribe_interrupt` | 120 | (200, 2) | -| `payment_balance_subscribe_receive` | 121 | (200, 3) | -| `payment_top_up_request` | 122 | (200, 4) | -| `payment_top_up_response` | 123 | (200, 5) | -| `payment_request_request` | 124 | (200, 6) | -| `payment_request_response` | 125 | (200, 7) | -| `payment_status_subscribe_start` | 126 | (200, 8) | -| `payment_status_subscribe_stop` | 127 | (200, 9) | -| `payment_status_subscribe_interrupt` | 128 | (200, 10) | -| `payment_status_subscribe_receive` | 129 | (200, 11) | -| `permissions_request_device_permission_request` | 8 | (201, 0) | -| `permissions_request_device_permission_response` | 9 | (201, 1) | -| `permissions_request_remote_permission_request` | 10 | (201, 2) | -| `permissions_request_remote_permission_response` | 11 | (201, 3) | -| `preimage_lookup_subscribe_start` | 64 | (202, 0) | -| `preimage_lookup_subscribe_stop` | 65 | (202, 1) | -| `preimage_lookup_subscribe_interrupt` | 66 | (202, 2) | -| `preimage_lookup_subscribe_receive` | 67 | (202, 3) | -| `preimage_submit_request` | 68 | (202, 4) | -| `preimage_submit_response` | 69 | (202, 5) | -| `resource_allocation_request_request` | 130 | (203, 0) | -| `resource_allocation_request_response` | 131 | (203, 1) | -| `signing_create_transaction_request` | 30 | (204, 0) | -| `signing_create_transaction_response` | 31 | (204, 1) | -| `signing_create_transaction_with_legacy_account_request` | 32 | (204, 2) | -| `signing_create_transaction_with_legacy_account_response` | 33 | (204, 3) | -| `signing_sign_raw_with_legacy_account_request` | 34 | (204, 4) | -| `signing_sign_raw_with_legacy_account_response` | 35 | (204, 5) | -| `signing_sign_payload_with_legacy_account_request` | 36 | (204, 6) | -| `signing_sign_payload_with_legacy_account_response` | 37 | (204, 7) | -| `signing_sign_raw_request` | 114 | (204, 8) | -| `signing_sign_raw_response` | 115 | (204, 9) | -| `signing_sign_payload_request` | 116 | (204, 10) | -| `signing_sign_payload_response` | 117 | (204, 11) | -| `statement_store_subscribe_start` | 56 | (205, 0) | -| `statement_store_subscribe_stop` | 57 | (205, 1) | -| `statement_store_subscribe_interrupt` | 58 | (205, 2) | -| `statement_store_subscribe_receive` | 59 | (205, 3) | -| `statement_store_create_proof_request` | 60 | (205, 4) | -| `statement_store_create_proof_response` | 61 | (205, 5) | -| `statement_store_submit_request` | 62 | (205, 6) | -| `statement_store_submit_response` | 63 | (205, 7) | -| `statement_store_create_proof_authorized_request` | 132 | (205, 8) | -| `statement_store_create_proof_authorized_response` | 133 | (205, 9) | -| `theme_subscribe_start` | 104 | (206, 0) | -| `theme_subscribe_stop` | 105 | (206, 1) | -| `theme_subscribe_interrupt` | 106 | (206, 2) | -| `theme_subscribe_receive` | 107 | (206, 3) | +| `system_handshake_request` | 0 | (193, 0) | +| `system_handshake_response` | 1 | (193, 1) | +| `system_feature_supported_request` | 2 | (193, 2) | +| `system_feature_supported_response` | 3 | (193, 3) | +| `system_navigate_to_request` | 6 | (193, 4) | +| `system_navigate_to_response` | 7 | (193, 5) | +| `account_connection_status_subscribe_start` | 18 | (194, 0) | +| `account_connection_status_subscribe_stop` | 19 | (194, 1) | +| `account_connection_status_subscribe_interrupt` | 20 | (194, 2) | +| `account_connection_status_subscribe_receive` | 21 | (194, 3) | +| `account_get_account_request` | 22 | (194, 4) | +| `account_get_account_response` | 23 | (194, 5) | +| `account_get_account_alias_request` | 24 | (194, 6) | +| `account_get_account_alias_response` | 25 | (194, 7) | +| `account_create_account_proof_request` | 26 | (194, 8) | +| `account_create_account_proof_response` | 27 | (194, 9) | +| `account_get_legacy_accounts_request` | 28 | (194, 10) | +| `account_get_legacy_accounts_response` | 29 | (194, 11) | +| `account_get_user_id_request` | 110 | (194, 12) | +| `account_get_user_id_response` | 111 | (194, 13) | +| `account_request_login_request` | 112 | (194, 14) | +| `account_request_login_response` | 113 | (194, 15) | +| `account_sign_vrf_request` | 164 | (194, 16) | +| `account_sign_vrf_response` | 165 | (194, 17) | +| `chain_follow_head_subscribe_start` | 76 | (195, 0) | +| `chain_follow_head_subscribe_stop` | 77 | (195, 1) | +| `chain_follow_head_subscribe_interrupt` | 78 | (195, 2) | +| `chain_follow_head_subscribe_receive` | 79 | (195, 3) | +| `chain_get_head_header_request` | 80 | (195, 4) | +| `chain_get_head_header_response` | 81 | (195, 5) | +| `chain_get_head_body_request` | 82 | (195, 6) | +| `chain_get_head_body_response` | 83 | (195, 7) | +| `chain_get_head_storage_request` | 84 | (195, 8) | +| `chain_get_head_storage_response` | 85 | (195, 9) | +| `chain_call_head_request` | 86 | (195, 10) | +| `chain_call_head_response` | 87 | (195, 11) | +| `chain_unpin_head_request` | 88 | (195, 12) | +| `chain_unpin_head_response` | 89 | (195, 13) | +| `chain_continue_head_request` | 90 | (195, 14) | +| `chain_continue_head_response` | 91 | (195, 15) | +| `chain_stop_head_operation_request` | 92 | (195, 16) | +| `chain_stop_head_operation_response` | 93 | (195, 17) | +| `chain_get_spec_genesis_hash_request` | 94 | (195, 18) | +| `chain_get_spec_genesis_hash_response` | 95 | (195, 19) | +| `chain_get_spec_chain_name_request` | 96 | (195, 20) | +| `chain_get_spec_chain_name_response` | 97 | (195, 21) | +| `chain_get_spec_properties_request` | 98 | (195, 22) | +| `chain_get_spec_properties_response` | 99 | (195, 23) | +| `chain_broadcast_transaction_request` | 100 | (195, 24) | +| `chain_broadcast_transaction_response` | 101 | (195, 25) | +| `chain_stop_transaction_request` | 102 | (195, 26) | +| `chain_stop_transaction_response` | 103 | (195, 27) | +| `chat_create_room_request` | 38 | (196, 0) | +| `chat_create_room_response` | 39 | (196, 1) | +| `chat_register_bot_request` | 40 | (196, 2) | +| `chat_register_bot_response` | 41 | (196, 3) | +| `chat_list_subscribe_start` | 42 | (196, 4) | +| `chat_list_subscribe_stop` | 43 | (196, 5) | +| `chat_list_subscribe_interrupt` | 44 | (196, 6) | +| `chat_list_subscribe_receive` | 45 | (196, 7) | +| `chat_post_message_request` | 46 | (196, 8) | +| `chat_post_message_response` | 47 | (196, 9) | +| `chat_action_subscribe_start` | 48 | (196, 10) | +| `chat_action_subscribe_stop` | 49 | (196, 11) | +| `chat_action_subscribe_interrupt` | 50 | (196, 12) | +| `chat_action_subscribe_receive` | 51 | (196, 13) | +| `chat_custom_message_render_start` | 52 | (196, 14) | +| `chat_custom_message_render_stop` | 53 | (196, 15) | +| `chat_custom_message_render_interrupt` | 54 | (196, 16) | +| `chat_custom_message_render_receive` | 55 | (196, 17) | +| `coin_payment_create_purse_request` | 136 | (197, 0) | +| `coin_payment_create_purse_response` | 137 | (197, 1) | +| `coin_payment_query_purse_request` | 138 | (197, 2) | +| `coin_payment_query_purse_response` | 139 | (197, 3) | +| `coin_payment_rebalance_purse_start` | 140 | (197, 4) | +| `coin_payment_rebalance_purse_stop` | 141 | (197, 5) | +| `coin_payment_rebalance_purse_interrupt` | 142 | (197, 6) | +| `coin_payment_rebalance_purse_receive` | 143 | (197, 7) | +| `coin_payment_delete_purse_start` | 144 | (197, 8) | +| `coin_payment_delete_purse_stop` | 145 | (197, 9) | +| `coin_payment_delete_purse_interrupt` | 146 | (197, 10) | +| `coin_payment_delete_purse_receive` | 147 | (197, 11) | +| `coin_payment_create_receivable_request` | 148 | (197, 12) | +| `coin_payment_create_receivable_response` | 149 | (197, 13) | +| `coin_payment_create_cheque_request` | 150 | (197, 14) | +| `coin_payment_create_cheque_response` | 151 | (197, 15) | +| `coin_payment_deposit_start` | 152 | (197, 16) | +| `coin_payment_deposit_stop` | 153 | (197, 17) | +| `coin_payment_deposit_interrupt` | 154 | (197, 18) | +| `coin_payment_deposit_receive` | 155 | (197, 19) | +| `coin_payment_refund_start` | 156 | (197, 20) | +| `coin_payment_refund_stop` | 157 | (197, 21) | +| `coin_payment_refund_interrupt` | 158 | (197, 22) | +| `coin_payment_refund_receive` | 159 | (197, 23) | +| `coin_payment_listen_for_payment_start` | 160 | (197, 24) | +| `coin_payment_listen_for_payment_stop` | 161 | (197, 25) | +| `coin_payment_listen_for_payment_interrupt` | 162 | (197, 26) | +| `coin_payment_listen_for_payment_receive` | 163 | (197, 27) | +| `entropy_derive_request` | 108 | (198, 0) | +| `entropy_derive_response` | 109 | (198, 1) | +| `local_storage_read_request` | 12 | (199, 0) | +| `local_storage_read_response` | 13 | (199, 1) | +| `local_storage_write_request` | 14 | (199, 2) | +| `local_storage_write_response` | 15 | (199, 3) | +| `local_storage_clear_request` | 16 | (199, 4) | +| `local_storage_clear_response` | 17 | (199, 5) | +| `notifications_send_push_notification_request` | 4 | (200, 0) | +| `notifications_send_push_notification_response` | 5 | (200, 1) | +| `notifications_cancel_push_notification_request` | 134 | (200, 2) | +| `notifications_cancel_push_notification_response` | 135 | (200, 3) | +| `payment_balance_subscribe_start` | 118 | (201, 0) | +| `payment_balance_subscribe_stop` | 119 | (201, 1) | +| `payment_balance_subscribe_interrupt` | 120 | (201, 2) | +| `payment_balance_subscribe_receive` | 121 | (201, 3) | +| `payment_top_up_request` | 122 | (201, 4) | +| `payment_top_up_response` | 123 | (201, 5) | +| `payment_request_request` | 124 | (201, 6) | +| `payment_request_response` | 125 | (201, 7) | +| `payment_status_subscribe_start` | 126 | (201, 8) | +| `payment_status_subscribe_stop` | 127 | (201, 9) | +| `payment_status_subscribe_interrupt` | 128 | (201, 10) | +| `payment_status_subscribe_receive` | 129 | (201, 11) | +| `permissions_request_device_permission_request` | 8 | (202, 0) | +| `permissions_request_device_permission_response` | 9 | (202, 1) | +| `permissions_request_remote_permission_request` | 10 | (202, 2) | +| `permissions_request_remote_permission_response` | 11 | (202, 3) | +| `preimage_lookup_subscribe_start` | 64 | (203, 0) | +| `preimage_lookup_subscribe_stop` | 65 | (203, 1) | +| `preimage_lookup_subscribe_interrupt` | 66 | (203, 2) | +| `preimage_lookup_subscribe_receive` | 67 | (203, 3) | +| `preimage_submit_request` | 68 | (203, 4) | +| `preimage_submit_response` | 69 | (203, 5) | +| `resource_allocation_request_request` | 130 | (204, 0) | +| `resource_allocation_request_response` | 131 | (204, 1) | +| `signing_create_transaction_request` | 30 | (205, 0) | +| `signing_create_transaction_response` | 31 | (205, 1) | +| `signing_create_transaction_with_legacy_account_request` | 32 | (205, 2) | +| `signing_create_transaction_with_legacy_account_response` | 33 | (205, 3) | +| `signing_sign_raw_with_legacy_account_request` | 34 | (205, 4) | +| `signing_sign_raw_with_legacy_account_response` | 35 | (205, 5) | +| `signing_sign_payload_with_legacy_account_request` | 36 | (205, 6) | +| `signing_sign_payload_with_legacy_account_response` | 37 | (205, 7) | +| `signing_sign_raw_request` | 114 | (205, 8) | +| `signing_sign_raw_response` | 115 | (205, 9) | +| `signing_sign_payload_request` | 116 | (205, 10) | +| `signing_sign_payload_response` | 117 | (205, 11) | +| `statement_store_subscribe_start` | 56 | (206, 0) | +| `statement_store_subscribe_stop` | 57 | (206, 1) | +| `statement_store_subscribe_interrupt` | 58 | (206, 2) | +| `statement_store_subscribe_receive` | 59 | (206, 3) | +| `statement_store_create_proof_request` | 60 | (206, 4) | +| `statement_store_create_proof_response` | 61 | (206, 5) | +| `statement_store_submit_request` | 62 | (206, 6) | +| `statement_store_submit_response` | 63 | (206, 7) | +| `statement_store_create_proof_authorized_request` | 132 | (206, 8) | +| `statement_store_create_proof_authorized_response` | 133 | (206, 9) | +| `theme_subscribe_start` | 104 | (207, 0) | +| `theme_subscribe_stop` | 105 | (207, 1) | +| `theme_subscribe_interrupt` | 106 | (207, 2) | +| `theme_subscribe_receive` | 107 | (207, 3) | From eaf757d3f2ca5d4dbef2157234343f709e6846fb Mon Sep 17 00:00:00 2001 From: Nidish Date: Fri, 28 Aug 2026 19:29:06 +0530 Subject: [PATCH 09/16] fix(wire): keep #547's answer-don't-drop semantics when re-keying to (trait, method) --- js/packages/truapi/src/client.test.ts | 85 +++++++++++++------ js/packages/truapi/src/client.ts | 42 ++++++--- js/packages/truapi/src/transport.ts | 8 ++ rust/crates/truapi-codegen/src/rust.rs | 5 +- rust/crates/truapi-codegen/src/ts.rs | 7 +- rust/crates/truapi-server/src/dispatcher.rs | 22 +++-- rust/crates/truapi-server/src/frame.rs | 8 +- rust/crates/truapi-server/src/subscription.rs | 7 +- .../truapi-server/tests/wire_result_shape.rs | 3 +- .../tests/wire_table_ts_parity.rs | 33 +++++++ 10 files changed, 158 insertions(+), 62 deletions(-) diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 53344b207..845451a18 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -343,13 +343,13 @@ describe("generated client transport", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const response = transport.request>({ - ids: { request: 194, response: 195 }, + ids: { trait: 200, request: 194, response: 195 }, payload: new Uint8Array(), decodeResponse: () => { throw new Error("protocol errors must bypass the method response decoder"); }, }); - fixture.receive(unsupportedMessage("p:1", 194)); + fixture.receive(unsupportedMessage("p:1", 200, 194)); expect((await response)._unsafeUnwrapErr()).toEqual({ tag: "Unsupported" }); @@ -362,7 +362,11 @@ describe("generated client transport", () => { unwrap( encodeWireMessage({ requestId: "p:2", - payload: { id: W.LOCAL_STORAGE_READ.response, value: new Uint8Array() }, + payload: { + traitId: W.LOCAL_STORAGE_READ.trait, + methodId: W.LOCAL_STORAGE_READ.response, + value: new Uint8Array(), + }, }), "encode follow-up response", ), @@ -376,21 +380,26 @@ describe("generated client transport", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const response = transport.request>({ - ids: { request: 194, response: 195 }, + ids: { trait: 200, request: 194, response: 195 }, payload: new Uint8Array(), decodeResponse: () => ({ success: true, value: "supported" }), }); - for (const [requestId, discriminant] of [ - ["p:99", 194], - ["p:1", 196], + for (const [requestId, traitId, methodId] of [ + // right pair, wrong request id + ["p:99", 200, 194], + // right request id, wrong method + ["p:1", 200, 196], + // right request id and method but the WRONG TRAIT - under a + // one-byte discriminant this was indistinguishable from a match + ["p:1", 201, 194], ] as const) { - fixture.receive(unsupportedMessage(requestId, discriminant)); + fixture.receive(unsupportedMessage(requestId, traitId, methodId)); } fixture.receive( unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: 195, value: new Uint8Array() }, + payload: { traitId: 200, methodId: 195, value: new Uint8Array() }, }), "encode supported response", ), @@ -467,18 +476,24 @@ describe("generated client transport", () => { }); it("closes the transport for every malformed protocol error shape", async () => { + // Re-derived for the two-byte address: a valid payload is now 4 bytes, + // so the old trailing-byte fixture `[0, 0, 194, 0]` decodes cleanly as + // the pair (194, 0) and would have silently stopped testing anything. const malformedPayloads = [ - [new Uint8Array([0, 0]), "expected 3 bytes, received 2"], - [new Uint8Array([0, 0, 194, 0]), "expected 3 bytes, received 4"], - [new Uint8Array([1, 0, 194]), "unsupported version 1"], - [new Uint8Array([0, 1, 194]), "unknown error discriminant 1"], + [new Uint8Array([0, 0]), "expected 4 bytes, received 2"], + // trait present, method truncated + [new Uint8Array([0, 0, 194]), "expected 4 bytes, received 3"], + // one trailing byte past a full pair + [new Uint8Array([0, 0, 194, 193, 0]), "expected 4 bytes, received 5"], + [new Uint8Array([1, 0, 194, 193]), "unsupported version 1"], + [new Uint8Array([0, 1, 194, 193]), "unknown error discriminant 1"], ] as const; for (const [payload, message] of malformedPayloads) { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const response = transport.request>({ - ids: { request: 194, response: 195 }, + ids: { trait: 200, request: 194, response: 195 }, payload: new Uint8Array(), decodeResponse: () => ({ success: true, value: undefined }), }); @@ -496,16 +511,16 @@ describe("generated client transport", () => { const incoming = unwrap( encodeWireMessage({ requestId: "h:future", - payload: { id: 194, value: new Uint8Array() }, + payload: { traitId: 200, methodId: 194, value: new Uint8Array() }, }), "encode unknown host request", ); fixture.receive(incoming); - expect(fixture.sent.map(toHex)).toEqual([toHex(unsupportedMessage("h:future", 194))]); + expect(fixture.sent.map(toHex)).toEqual([toHex(unsupportedMessage("h:future", 200, 194))]); - fixture.receive(unsupportedMessage("h:future", 194)); + fixture.receive(unsupportedMessage("h:future", 200, 194)); expect(fixture.sent).toHaveLength(1); }); @@ -517,8 +532,8 @@ describe("generated client transport", () => { encodeWireMessage({ requestId: "h:known", payload: { - id: W.CHAT_CUSTOM_MESSAGE_RENDER.start, - value: new Uint8Array(), + traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.start, value: new Uint8Array(), }, }), "encode known unhandled host start", @@ -526,7 +541,13 @@ describe("generated client transport", () => { ); expect(fixture.sent.map(toHex)).toEqual([ - toHex(unsupportedMessage("h:known", W.CHAT_CUSTOM_MESSAGE_RENDER.start)), + toHex( + unsupportedMessage( + "h:known", + W.CHAT_CUSTOM_MESSAGE_RENDER.trait, + W.CHAT_CUSTOM_MESSAGE_RENDER.start, + ), + ), ]); }); @@ -542,7 +563,7 @@ describe("generated client transport", () => { unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: 194, value: new Uint8Array() }, + payload: { traitId: 200, methodId: 194, value: new Uint8Array() }, }), "encode unknown correlated message", ), @@ -550,14 +571,18 @@ describe("generated client transport", () => { expect(fixture.sent.map(toHex)).toEqual([ toHex(fixture.sent[0]), - toHex(unsupportedMessage("p:1", 194)), + toHex(unsupportedMessage("p:1", 200, 194)), ]); fixture.receive( unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: W.LOCAL_STORAGE_READ.response, value: new Uint8Array() }, + payload: { + traitId: W.LOCAL_STORAGE_READ.trait, + methodId: W.LOCAL_STORAGE_READ.response, + value: new Uint8Array(), + }, }), "encode request response", ), @@ -576,7 +601,11 @@ describe("generated client transport", () => { const responseFrame = unwrap( encodeWireMessage({ requestId: "p:1", - payload: { id: W.LOCAL_STORAGE_READ.response, value: new Uint8Array() }, + payload: { + traitId: W.LOCAL_STORAGE_READ.trait, + methodId: W.LOCAL_STORAGE_READ.response, + value: new Uint8Array(), + }, }), "encode response", ); @@ -594,7 +623,7 @@ describe("generated client transport", () => { onReceive: (payload) => received.push(payload), }); subscription.unsubscribe(); - for (const id of [ + for (const methodId of [ W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.interrupt, ]) { @@ -602,7 +631,11 @@ describe("generated client transport", () => { unwrap( encodeWireMessage({ requestId: subscription.subscriptionId, - payload: { id, value: new Uint8Array() }, + payload: { + traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, + methodId, + value: new Uint8Array(), + }, }), "encode stale subscription frame", ), diff --git a/js/packages/truapi/src/client.ts b/js/packages/truapi/src/client.ts index 8df879bc6..e263c8f2c 100644 --- a/js/packages/truapi/src/client.ts +++ b/js/packages/truapi/src/client.ts @@ -3,6 +3,7 @@ import { errAsync, okAsync, ResultAsync } from "neverthrow"; import { decodeWireMessage, encodeWireMessage, + MIN_TRAIT_ID, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, type HostInitiatedSubscriptionHandler, @@ -342,7 +343,10 @@ export function createTransport( ) { subscriptions.delete(requestId); subscription.onClose?.( - new UnsupportedMessageError(unsupported.traitId, unsupported.methodId), + new UnsupportedMessageError( + unsupported.traitId, + unsupported.methodId, + ), ); } return; @@ -397,7 +401,9 @@ export function createTransport( return; } - const hostRoute = hostRoutes.get(pairKey(payload.traitId, payload.methodId)); + const hostRoute = hostRoutes.get( + pairKey(payload.traitId, payload.methodId), + ); if (hostRoute) { startHostSubscription(hostRoute, requestId, payload.value); return; @@ -430,18 +436,24 @@ export function createTransport( // not own. Dropping it unreported leaves the caller waiting forever // with no clue why, and a whole-trait skew is what a codec mismatch // looks like from here. + // + // Report it, then fall through rather than returning: the request stays + // pending (this frame is not its answer), and the frame itself is one + // this build cannot route, so it earns the same protocol-error reply as + // any other unroutable pair. A known client-bound pair is still filtered + // out by `UNANSWERED_WIRE_IDS` below. reportProtocolViolation( `ignoring frame for request ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}), expected (${p.ids.trait}, ${p.ids.response})`, ); + } else { + pending.delete(requestId); + try { + p.resolve(payload.value); + } catch (error) { + p.reject(toError(error)); + } return; } - pending.delete(requestId); - try { - p.resolve(payload.value); - } catch (error) { - p.reject(toError(error)); - } - return; } const subscription = subscriptions.get(requestId); @@ -479,11 +491,19 @@ export function createTransport( } // Not pending, no subscription, and not a client-bound frame we ignore by - // design: this build does not implement the pair. Report it locally AND - // answer the peer - a log alone leaves the sender waiting forever. + // design: this build does not implement the pair. reportProtocolViolation( `unsupported frame with discriminant (${payload.traitId}, ${payload.methodId}): request ${requestId} is not pending and has no subscription`, ); + // Answer only a peer that could read the answer. A trait byte below the + // floor is not a trait at all - it is a codec 1 peer's flat method id - and + // such a peer would read our `(255, 255)` reply as codec 1 discriminant 255 + // with a payload it cannot decode, and tear its own transport down over a + // malformed-protocol-error that says nothing about the real problem. The + // log above is the diagnostic for that case. + if (payload.traitId < MIN_TRAIT_ID) { + return; + } try { send({ requestId, diff --git a/js/packages/truapi/src/transport.ts b/js/packages/truapi/src/transport.ts index b271f5518..e50316c9f 100644 --- a/js/packages/truapi/src/transport.ts +++ b/js/packages/truapi/src/transport.ts @@ -12,6 +12,14 @@ export const PROTOCOL_ERROR_TRAIT_ID = 255 as const; /** Wire method discriminant reserved for method-independent protocol errors. **/ export const PROTOCOL_ERROR_METHOD_ID = 255 as const; +/** + * Lowest trait id the codec permits, mirroring `truapi::MIN_TRAIT_ID`. A frame + * whose trait byte falls below it is not naming a trait at all: that is where a + * codec 1 peer's single flat method byte lands. Kept in step with the Rust + * constant by `wire_table_ts_parity`. + **/ +export const MIN_TRAIT_ID = 193 as const; + /** The peer rejected an outbound frame because it does not support its API. **/ export class UnsupportedMessageError extends Error { /** Trait discriminant of the unsupported outbound frame. **/ diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index 5d9f0080c..9d21ff984 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -556,7 +556,7 @@ mod tests { assert!( msg.contains("wire trait id 255 reused") && msg.contains("reserved for protocol errors"), - "unexpected error message: {msg}", + "unexpected error message: {msg}", ); } @@ -605,8 +605,7 @@ mod tests { types: vec![], }; - generate_wire_table(&api) - .expect("(MIN_TRAIT_ID, 255) is an ordinary address"); + generate_wire_table(&api).expect("(MIN_TRAIT_ID, 255) is an ordinary address"); } /// Pin `wire_const_name`'s `convert_case::Case::UpperSnake` behavior: diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index adfb98c61..9618e17a3 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -638,6 +638,7 @@ fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result Drop for HostInitiatedSubscription { #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use super::*; + use crate::frame::{PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID}; use futures::FutureExt; use futures::stream; use parity_scale_codec::Encode; @@ -874,7 +874,8 @@ mod tests { manager.handle_message(ProtocolMessage { request_id: "h:1".into(), payload: Payload { - id: host_ids().receive_id, + trait_id: host_ids().trait_id, + method_id: host_ids().receive_id, value: 7_u32.encode(), }, }); diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index 74dc77c0a..4625180b8 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -28,8 +28,7 @@ use truapi::{CallError, v01}; use truapi_server::core::TrUApiCore; use truapi_server::frame::{ PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, ProtocolErrorV1, ProtocolMessage, - VersionedProtocolError, - request_ids, subscription_ids, + VersionedProtocolError, request_ids, subscription_ids, }; mod common; diff --git a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs index 624dcde64..3306097ac 100644 --- a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs +++ b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs @@ -240,3 +240,36 @@ fn rust_and_ts_wire_tables_agree() { `scripts/codegen.sh` so the codegen pipeline produces them in lockstep.", ); } + +/// `transport.ts` hand-mirrors two Rust constants that the generated table does +/// not carry: the codec's trait-id floor and the reserved protocol-error +/// address. They are hand-written on the TS side, so nothing but this test stops +/// them drifting - and a drift means one language answers frames the other +/// refuses. +#[test] +fn transport_ts_mirrors_the_rust_wire_constants() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../../js/packages/truapi/src/transport.ts"); + let src = std::fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("read {}: {err}", path.display())); + + for (name, expected) in [ + ("MIN_TRAIT_ID", truapi::MIN_TRAIT_ID), + ("PROTOCOL_ERROR_TRAIT_ID", 255), + ("PROTOCOL_ERROR_METHOD_ID", 255), + ] { + let needle = format!("export const {name} = "); + let start = src + .find(&needle) + .unwrap_or_else(|| panic!("transport.ts must export {name}")); + let rest = &src[start + needle.len()..]; + let digits: String = rest.chars().take_while(char::is_ascii_digit).collect(); + let actual: u8 = digits + .parse() + .unwrap_or_else(|err| panic!("{name} is not a u8 literal: {err}")); + assert_eq!( + actual, expected, + "transport.ts {name} = {actual} but Rust says {expected}", + ); + } +} From 82e66ec30b52aa64a3d0af0e1ad1a11343448691 Mon Sep 17 00:00:00 2001 From: Nidish Date: Mon, 31 Aug 2026 13:48:20 +0530 Subject: [PATCH 10/16] fix(wire): allocate trait id 208 for the locale trait main added --- .../truapi-codegen/tests/golden/wire_table.rs | 921 ++++++++++-------- .../truapi-server/src/generated/wire_table.rs | 921 ++++++++++-------- rust/crates/truapi/src/api/locale.rs | 5 +- 3 files changed, 1003 insertions(+), 844 deletions(-) diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs index 3a3783b85..891fce3c1 100644 --- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -2,31 +2,37 @@ //! //! Auto-generated by truapi-codegen. Do not edit. //! -//! Each method reserves either two ids (request/response) or four -//! (start/stop/interrupt/receive). The ids for each method are exposed -//! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the -//! generated dispatcher both reference those consts so the numbers live -//! in exactly one place. The table is sorted by request/start id. +//! Every frame carries a `(trait, method)` discriminant pair. Each +//! method reserves either two method ids (request/response) or four +//! (start/stop/interrupt/receive) within its trait. The ids for each +//! method are exposed as a named const (`PREIMAGE_SUBMIT`, ...); +//! [`WIRE_TABLE`] and the generated dispatcher both reference those +//! consts so the numbers live in exactly one place. The table is +//! sorted by (trait id, request/start id). /// Request method wire discriminants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RequestFrameIds { - /// Discriminant for the request frame. + /// Trait discriminant carried by both frames. + pub trait_id: u8, + /// Method discriminant for the request frame. pub request_id: u8, - /// Discriminant for the response frame. + /// Method discriminant for the response frame. pub response_id: u8, } /// Subscription method wire discriminants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SubscriptionFrameIds { - /// Discriminant for the start frame. + /// Trait discriminant carried by all four frames. + pub trait_id: u8, + /// Method discriminant for the start frame. pub start_id: u8, - /// Discriminant for the stop frame. + /// Method discriminant for the stop frame. pub stop_id: u8, - /// Discriminant for the interrupt frame (server-initiated termination). + /// Method discriminant for the interrupt frame (server-initiated termination). pub interrupt_id: u8, - /// Discriminant for each receive frame (a streamed item). + /// Method discriminant for each receive frame (a streamed item). pub receive_id: u8, } @@ -48,470 +54,543 @@ pub enum WireKind { /// Wire discriminants for `system_handshake`. pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 0, response_id: 1, }; /// Wire discriminants for `system_feature_supported`. pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 2, response_id: 3, }; -/// Wire discriminants for `notifications_send_push_notification`. -pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `system_navigate_to`. +pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 4, response_id: 5, }; -/// Wire discriminants for `system_navigate_to`. -pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `system_host_info`. +pub const SYSTEM_HOST_INFO: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 6, response_id: 7, }; -/// Wire discriminants for `permissions_request_device_permission`. -pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `system_get_product_context`. +pub const SYSTEM_GET_PRODUCT_CONTEXT: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 8, response_id: 9, }; -/// Wire discriminants for `permissions_request_remote_permission`. -pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { - request_id: 10, - response_id: 11, -}; - -/// Wire discriminants for `local_storage_read`. -pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { - request_id: 12, - response_id: 13, -}; - -/// Wire discriminants for `local_storage_write`. -pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { - request_id: 14, - response_id: 15, -}; - -/// Wire discriminants for `local_storage_clear`. -pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { - request_id: 16, - response_id: 17, -}; - /// Wire discriminants for `account_connection_status_subscribe`. pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 18, - stop_id: 19, - interrupt_id: 20, - receive_id: 21, + trait_id: 194, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; /// Wire discriminants for `account_get_account`. pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 22, - response_id: 23, + trait_id: 194, + request_id: 4, + response_id: 5, }; /// Wire discriminants for `account_get_account_alias`. pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { - request_id: 24, - response_id: 25, + trait_id: 194, + request_id: 6, + response_id: 7, }; /// Wire discriminants for `account_create_account_proof`. pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { - request_id: 26, - response_id: 27, + trait_id: 194, + request_id: 8, + response_id: 9, }; /// Wire discriminants for `account_get_legacy_accounts`. pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { - request_id: 28, - response_id: 29, -}; - -/// Wire discriminants for `signing_create_transaction`. -pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { - request_id: 30, - response_id: 31, -}; - -/// Wire discriminants for `signing_create_transaction_with_legacy_account`. -pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 32, - response_id: 33, -}; - -/// Wire discriminants for `signing_sign_raw_with_legacy_account`. -pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 34, - response_id: 35, -}; - -/// Wire discriminants for `signing_sign_payload_with_legacy_account`. -pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 36, - response_id: 37, -}; - -/// Wire discriminants for `chat_create_room`. -pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { - request_id: 38, - response_id: 39, -}; - -/// Wire discriminants for `chat_register_bot`. -pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { - request_id: 40, - response_id: 41, -}; - -/// Wire discriminants for `chat_list_subscribe`. -pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 42, - stop_id: 43, - interrupt_id: 44, - receive_id: 45, -}; - -/// Wire discriminants for `chat_post_message`. -pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { - request_id: 46, - response_id: 47, -}; - -/// Wire discriminants for `chat_action_subscribe`. -pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 48, - stop_id: 49, - interrupt_id: 50, - receive_id: 51, + trait_id: 194, + request_id: 10, + response_id: 11, }; -/// Wire discriminants for `chat_custom_message_render`. -pub const CHAT_CUSTOM_MESSAGE_RENDER: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 52, - stop_id: 53, - interrupt_id: 54, - receive_id: 55, +/// Wire discriminants for `account_get_user_id`. +pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 12, + response_id: 13, }; -/// Wire discriminants for `statement_store_subscribe`. -pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 56, - stop_id: 57, - interrupt_id: 58, - receive_id: 59, +/// Wire discriminants for `account_request_login`. +pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 14, + response_id: 15, }; -/// Wire discriminants for `statement_store_create_proof`. -pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { - request_id: 60, - response_id: 61, +/// Wire discriminants for `account_sign_vrf`. +pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 16, + response_id: 17, }; -/// Wire discriminants for `statement_store_submit`. -pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { - request_id: 62, - response_id: 63, +/// Wire discriminants for `account_register_ring_vrf_key`. +pub const ACCOUNT_REGISTER_RING_VRF_KEY: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 168, + response_id: 169, }; -/// Wire discriminants for `preimage_lookup_subscribe`. -pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 64, - stop_id: 65, - interrupt_id: 66, - receive_id: 67, +/// Wire discriminants for `account_list_ring_vrf_keys`. +pub const ACCOUNT_LIST_RING_VRF_KEYS: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 170, + response_id: 171, }; -/// Wire discriminants for `preimage_submit`. -pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { - request_id: 68, - response_id: 69, +/// Wire discriminants for `account_ring_vrf_sign`. +pub const ACCOUNT_RING_VRF_SIGN: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 172, + response_id: 173, }; /// Wire discriminants for `chain_follow_head_subscribe`. pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 76, - stop_id: 77, - interrupt_id: 78, - receive_id: 79, + trait_id: 195, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; /// Wire discriminants for `chain_get_head_header`. pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { - request_id: 80, - response_id: 81, + trait_id: 195, + request_id: 4, + response_id: 5, }; /// Wire discriminants for `chain_get_head_body`. pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { - request_id: 82, - response_id: 83, + trait_id: 195, + request_id: 6, + response_id: 7, }; /// Wire discriminants for `chain_get_head_storage`. pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { - request_id: 84, - response_id: 85, + trait_id: 195, + request_id: 8, + response_id: 9, }; /// Wire discriminants for `chain_call_head`. pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { - request_id: 86, - response_id: 87, + trait_id: 195, + request_id: 10, + response_id: 11, }; /// Wire discriminants for `chain_unpin_head`. pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { - request_id: 88, - response_id: 89, + trait_id: 195, + request_id: 12, + response_id: 13, }; /// Wire discriminants for `chain_continue_head`. pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { - request_id: 90, - response_id: 91, + trait_id: 195, + request_id: 14, + response_id: 15, }; /// Wire discriminants for `chain_stop_head_operation`. pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { - request_id: 92, - response_id: 93, + trait_id: 195, + request_id: 16, + response_id: 17, }; /// Wire discriminants for `chain_get_spec_genesis_hash`. pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { - request_id: 94, - response_id: 95, + trait_id: 195, + request_id: 18, + response_id: 19, }; /// Wire discriminants for `chain_get_spec_chain_name`. pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { - request_id: 96, - response_id: 97, + trait_id: 195, + request_id: 20, + response_id: 21, }; /// Wire discriminants for `chain_get_spec_properties`. pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { - request_id: 98, - response_id: 99, + trait_id: 195, + request_id: 22, + response_id: 23, }; /// Wire discriminants for `chain_broadcast_transaction`. pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { - request_id: 100, - response_id: 101, + trait_id: 195, + request_id: 24, + response_id: 25, }; /// Wire discriminants for `chain_stop_transaction`. pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { - request_id: 102, - response_id: 103, -}; - -/// Wire discriminants for `theme_subscribe`. -pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 104, - stop_id: 105, - interrupt_id: 106, - receive_id: 107, -}; - -/// Wire discriminants for `entropy_derive`. -pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { - request_id: 108, - response_id: 109, -}; - -/// Wire discriminants for `account_get_user_id`. -pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { - request_id: 110, - response_id: 111, -}; - -/// Wire discriminants for `account_request_login`. -pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { - request_id: 112, - response_id: 113, -}; - -/// Wire discriminants for `signing_sign_raw`. -pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { - request_id: 114, - response_id: 115, -}; - -/// Wire discriminants for `signing_sign_payload`. -pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { - request_id: 116, - response_id: 117, + trait_id: 195, + request_id: 26, + response_id: 27, }; -/// Wire discriminants for `payment_balance_subscribe`. -pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 118, - stop_id: 119, - interrupt_id: 120, - receive_id: 121, +/// Wire discriminants for `chain_get_chain_info`. +pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { + trait_id: 195, + request_id: 166, + response_id: 167, }; -/// Wire discriminants for `payment_top_up`. -pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { - request_id: 122, - response_id: 123, +/// Wire discriminants for `chat_create_room`. +pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { + trait_id: 196, + request_id: 0, + response_id: 1, }; -/// Wire discriminants for `payment_request`. -pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { - request_id: 124, - response_id: 125, +/// Wire discriminants for `chat_register_bot`. +pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { + trait_id: 196, + request_id: 2, + response_id: 3, }; -/// Wire discriminants for `payment_status_subscribe`. -pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 126, - stop_id: 127, - interrupt_id: 128, - receive_id: 129, +/// Wire discriminants for `chat_list_subscribe`. +pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 196, + start_id: 4, + stop_id: 5, + interrupt_id: 6, + receive_id: 7, }; -/// Wire discriminants for `resource_allocation_request`. -pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { - request_id: 130, - response_id: 131, +/// Wire discriminants for `chat_post_message`. +pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { + trait_id: 196, + request_id: 8, + response_id: 9, }; -/// Wire discriminants for `statement_store_create_proof_authorized`. -pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { - request_id: 132, - response_id: 133, +/// Wire discriminants for `chat_action_subscribe`. +pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 196, + start_id: 10, + stop_id: 11, + interrupt_id: 12, + receive_id: 13, }; -/// Wire discriminants for `notifications_cancel_push_notification`. -pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { - request_id: 134, - response_id: 135, +/// Wire discriminants for `chat_custom_message_render`. +pub const CHAT_CUSTOM_MESSAGE_RENDER: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 196, + start_id: 14, + stop_id: 15, + interrupt_id: 16, + receive_id: 17, }; /// Wire discriminants for `coin_payment_create_purse`. pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { - request_id: 136, - response_id: 137, + trait_id: 197, + request_id: 0, + response_id: 1, }; /// Wire discriminants for `coin_payment_query_purse`. pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { - request_id: 138, - response_id: 139, + trait_id: 197, + request_id: 2, + response_id: 3, }; /// Wire discriminants for `coin_payment_rebalance_purse`. pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 140, - stop_id: 141, - interrupt_id: 142, - receive_id: 143, + trait_id: 197, + start_id: 4, + stop_id: 5, + interrupt_id: 6, + receive_id: 7, }; /// Wire discriminants for `coin_payment_delete_purse`. pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 144, - stop_id: 145, - interrupt_id: 146, - receive_id: 147, + trait_id: 197, + start_id: 8, + stop_id: 9, + interrupt_id: 10, + receive_id: 11, }; /// Wire discriminants for `coin_payment_create_receivable`. pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { - request_id: 148, - response_id: 149, + trait_id: 197, + request_id: 12, + response_id: 13, }; /// Wire discriminants for `coin_payment_create_cheque`. pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { - request_id: 150, - response_id: 151, + trait_id: 197, + request_id: 14, + response_id: 15, }; /// Wire discriminants for `coin_payment_deposit`. pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 152, - stop_id: 153, - interrupt_id: 154, - receive_id: 155, + trait_id: 197, + start_id: 16, + stop_id: 17, + interrupt_id: 18, + receive_id: 19, }; /// Wire discriminants for `coin_payment_refund`. pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 156, - stop_id: 157, - interrupt_id: 158, - receive_id: 159, + trait_id: 197, + start_id: 20, + stop_id: 21, + interrupt_id: 22, + receive_id: 23, }; /// Wire discriminants for `coin_payment_listen_for_payment`. pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 160, - stop_id: 161, - interrupt_id: 162, - receive_id: 163, + trait_id: 197, + start_id: 24, + stop_id: 25, + interrupt_id: 26, + receive_id: 27, }; -/// Wire discriminants for `account_sign_vrf`. -pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { - request_id: 164, - response_id: 165, +/// Wire discriminants for `entropy_derive`. +pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { + trait_id: 198, + request_id: 0, + response_id: 1, }; -/// Wire discriminants for `chain_get_chain_info`. -pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { - request_id: 166, - response_id: 167, +/// Wire discriminants for `local_storage_read`. +pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { + trait_id: 199, + request_id: 0, + response_id: 1, }; -/// Wire discriminants for `account_register_ring_vrf_key`. -pub const ACCOUNT_REGISTER_RING_VRF_KEY: RequestFrameIds = RequestFrameIds { - request_id: 168, - response_id: 169, +/// Wire discriminants for `local_storage_write`. +pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { + trait_id: 199, + request_id: 2, + response_id: 3, }; -/// Wire discriminants for `account_list_ring_vrf_keys`. -pub const ACCOUNT_LIST_RING_VRF_KEYS: RequestFrameIds = RequestFrameIds { - request_id: 170, - response_id: 171, +/// Wire discriminants for `local_storage_clear`. +pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { + trait_id: 199, + request_id: 4, + response_id: 5, }; -/// Wire discriminants for `account_ring_vrf_sign`. -pub const ACCOUNT_RING_VRF_SIGN: RequestFrameIds = RequestFrameIds { - request_id: 172, - response_id: 173, +/// Wire discriminants for `notifications_send_push_notification`. +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + trait_id: 200, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `notifications_cancel_push_notification`. +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + trait_id: 200, + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `payment_balance_subscribe`. +pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 201, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, +}; + +/// Wire discriminants for `payment_top_up`. +pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { + trait_id: 201, + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `payment_request`. +pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { + trait_id: 201, + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `payment_status_subscribe`. +pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 201, + start_id: 8, + stop_id: 9, + interrupt_id: 10, + receive_id: 11, +}; + +/// Wire discriminants for `permissions_request_device_permission`. +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { + trait_id: 202, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `permissions_request_remote_permission`. +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { + trait_id: 202, + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `preimage_lookup_subscribe`. +pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 203, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, +}; + +/// Wire discriminants for `preimage_submit`. +pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + trait_id: 203, + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `resource_allocation_request`. +pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { + trait_id: 204, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `signing_create_transaction`. +pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `signing_create_transaction_with_legacy_account`. +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `signing_sign_raw_with_legacy_account`. +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `signing_sign_payload_with_legacy_account`. +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `signing_sign_raw`. +pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `signing_sign_payload`. +pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 10, + response_id: 11, +}; + +/// Wire discriminants for `statement_store_subscribe`. +pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 206, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, +}; + +/// Wire discriminants for `statement_store_create_proof`. +pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { + trait_id: 206, + request_id: 4, + response_id: 5, }; -/// Wire discriminants for `system_get_product_context`. -pub const SYSTEM_GET_PRODUCT_CONTEXT: RequestFrameIds = RequestFrameIds { - request_id: 190, - response_id: 191, +/// Wire discriminants for `statement_store_submit`. +pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { + trait_id: 206, + request_id: 6, + response_id: 7, }; -/// Wire discriminants for `system_host_info`. -pub const SYSTEM_HOST_INFO: RequestFrameIds = RequestFrameIds { - request_id: 192, - response_id: 193, +/// Wire discriminants for `statement_store_create_proof_authorized`. +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { + trait_id: 206, + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `theme_subscribe`. +pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 207, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; /// Wire discriminants for `locale_subscribe`. pub const LOCALE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 194, - stop_id: 195, - interrupt_id: 196, - receive_id: 197, + trait_id: 208, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; -/// The full wire table. Ordering is part of the wire protocol; -/// only ever append. Removed methods leave their slot empty. +/// The full wire table. Trait ids and per-trait method ordering are +/// part of the wire protocol; only ever append within a trait. +/// Removed methods leave their slot empty. pub const WIRE_TABLE: &[WireEntry] = &[ WireEntry { method: "system_handshake", @@ -521,33 +600,17 @@ pub const WIRE_TABLE: &[WireEntry] = &[ method: "system_feature_supported", kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), }, - WireEntry { - method: "notifications_send_push_notification", - kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), - }, WireEntry { method: "system_navigate_to", kind: WireKind::Request(SYSTEM_NAVIGATE_TO), }, WireEntry { - method: "permissions_request_device_permission", - kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), - }, - WireEntry { - method: "permissions_request_remote_permission", - kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), - }, - WireEntry { - method: "local_storage_read", - kind: WireKind::Request(LOCAL_STORAGE_READ), - }, - WireEntry { - method: "local_storage_write", - kind: WireKind::Request(LOCAL_STORAGE_WRITE), + method: "system_host_info", + kind: WireKind::Request(SYSTEM_HOST_INFO), }, WireEntry { - method: "local_storage_clear", - kind: WireKind::Request(LOCAL_STORAGE_CLEAR), + method: "system_get_product_context", + kind: WireKind::Request(SYSTEM_GET_PRODUCT_CONTEXT), }, WireEntry { method: "account_connection_status_subscribe", @@ -570,64 +633,28 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), }, WireEntry { - method: "signing_create_transaction", - kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), - }, - WireEntry { - method: "signing_create_transaction_with_legacy_account", - kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_raw_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_payload_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "chat_create_room", - kind: WireKind::Request(CHAT_CREATE_ROOM), - }, - WireEntry { - method: "chat_register_bot", - kind: WireKind::Request(CHAT_REGISTER_BOT), - }, - WireEntry { - method: "chat_list_subscribe", - kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), - }, - WireEntry { - method: "chat_post_message", - kind: WireKind::Request(CHAT_POST_MESSAGE), - }, - WireEntry { - method: "chat_action_subscribe", - kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), - }, - WireEntry { - method: "chat_custom_message_render", - kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER), + method: "account_get_user_id", + kind: WireKind::Request(ACCOUNT_GET_USER_ID), }, WireEntry { - method: "statement_store_subscribe", - kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + method: "account_request_login", + kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), }, WireEntry { - method: "statement_store_create_proof", - kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + method: "account_sign_vrf", + kind: WireKind::Request(ACCOUNT_SIGN_VRF), }, WireEntry { - method: "statement_store_submit", - kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + method: "account_register_ring_vrf_key", + kind: WireKind::Request(ACCOUNT_REGISTER_RING_VRF_KEY), }, WireEntry { - method: "preimage_lookup_subscribe", - kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + method: "account_list_ring_vrf_keys", + kind: WireKind::Request(ACCOUNT_LIST_RING_VRF_KEYS), }, WireEntry { - method: "preimage_submit", - kind: WireKind::Request(PREIMAGE_SUBMIT), + method: "account_ring_vrf_sign", + kind: WireKind::Request(ACCOUNT_RING_VRF_SIGN), }, WireEntry { method: "chain_follow_head_subscribe", @@ -682,56 +709,32 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Request(CHAIN_STOP_TRANSACTION), }, WireEntry { - method: "theme_subscribe", - kind: WireKind::Subscription(THEME_SUBSCRIBE), - }, - WireEntry { - method: "entropy_derive", - kind: WireKind::Request(ENTROPY_DERIVE), - }, - WireEntry { - method: "account_get_user_id", - kind: WireKind::Request(ACCOUNT_GET_USER_ID), - }, - WireEntry { - method: "account_request_login", - kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), - }, - WireEntry { - method: "signing_sign_raw", - kind: WireKind::Request(SIGNING_SIGN_RAW), - }, - WireEntry { - method: "signing_sign_payload", - kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), - }, - WireEntry { - method: "payment_balance_subscribe", - kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + method: "chain_get_chain_info", + kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), }, WireEntry { - method: "payment_top_up", - kind: WireKind::Request(PAYMENT_TOP_UP), + method: "chat_create_room", + kind: WireKind::Request(CHAT_CREATE_ROOM), }, WireEntry { - method: "payment_request", - kind: WireKind::Request(PAYMENT_REQUEST), + method: "chat_register_bot", + kind: WireKind::Request(CHAT_REGISTER_BOT), }, WireEntry { - method: "payment_status_subscribe", - kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + method: "chat_list_subscribe", + kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), }, WireEntry { - method: "resource_allocation_request", - kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + method: "chat_post_message", + kind: WireKind::Request(CHAT_POST_MESSAGE), }, WireEntry { - method: "statement_store_create_proof_authorized", - kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + method: "chat_action_subscribe", + kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), }, WireEntry { - method: "notifications_cancel_push_notification", - kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), + method: "chat_custom_message_render", + kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER), }, WireEntry { method: "coin_payment_create_purse", @@ -770,32 +773,108 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), }, WireEntry { - method: "account_sign_vrf", - kind: WireKind::Request(ACCOUNT_SIGN_VRF), + method: "entropy_derive", + kind: WireKind::Request(ENTROPY_DERIVE), }, WireEntry { - method: "chain_get_chain_info", - kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), + method: "local_storage_read", + kind: WireKind::Request(LOCAL_STORAGE_READ), }, WireEntry { - method: "account_register_ring_vrf_key", - kind: WireKind::Request(ACCOUNT_REGISTER_RING_VRF_KEY), + method: "local_storage_write", + kind: WireKind::Request(LOCAL_STORAGE_WRITE), }, WireEntry { - method: "account_list_ring_vrf_keys", - kind: WireKind::Request(ACCOUNT_LIST_RING_VRF_KEYS), + method: "local_storage_clear", + kind: WireKind::Request(LOCAL_STORAGE_CLEAR), }, WireEntry { - method: "account_ring_vrf_sign", - kind: WireKind::Request(ACCOUNT_RING_VRF_SIGN), + method: "notifications_send_push_notification", + kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), }, WireEntry { - method: "system_get_product_context", - kind: WireKind::Request(SYSTEM_GET_PRODUCT_CONTEXT), + method: "notifications_cancel_push_notification", + kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), }, WireEntry { - method: "system_host_info", - kind: WireKind::Request(SYSTEM_HOST_INFO), + method: "payment_balance_subscribe", + kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request(PAYMENT_TOP_UP), + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request(PAYMENT_REQUEST), + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request(PREIMAGE_SUBMIT), + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request(SIGNING_SIGN_RAW), + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription(THEME_SUBSCRIBE), }, WireEntry { method: "locale_subscribe", diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs index 3a3783b85..891fce3c1 100644 --- a/rust/crates/truapi-server/src/generated/wire_table.rs +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -2,31 +2,37 @@ //! //! Auto-generated by truapi-codegen. Do not edit. //! -//! Each method reserves either two ids (request/response) or four -//! (start/stop/interrupt/receive). The ids for each method are exposed -//! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the -//! generated dispatcher both reference those consts so the numbers live -//! in exactly one place. The table is sorted by request/start id. +//! Every frame carries a `(trait, method)` discriminant pair. Each +//! method reserves either two method ids (request/response) or four +//! (start/stop/interrupt/receive) within its trait. The ids for each +//! method are exposed as a named const (`PREIMAGE_SUBMIT`, ...); +//! [`WIRE_TABLE`] and the generated dispatcher both reference those +//! consts so the numbers live in exactly one place. The table is +//! sorted by (trait id, request/start id). /// Request method wire discriminants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RequestFrameIds { - /// Discriminant for the request frame. + /// Trait discriminant carried by both frames. + pub trait_id: u8, + /// Method discriminant for the request frame. pub request_id: u8, - /// Discriminant for the response frame. + /// Method discriminant for the response frame. pub response_id: u8, } /// Subscription method wire discriminants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SubscriptionFrameIds { - /// Discriminant for the start frame. + /// Trait discriminant carried by all four frames. + pub trait_id: u8, + /// Method discriminant for the start frame. pub start_id: u8, - /// Discriminant for the stop frame. + /// Method discriminant for the stop frame. pub stop_id: u8, - /// Discriminant for the interrupt frame (server-initiated termination). + /// Method discriminant for the interrupt frame (server-initiated termination). pub interrupt_id: u8, - /// Discriminant for each receive frame (a streamed item). + /// Method discriminant for each receive frame (a streamed item). pub receive_id: u8, } @@ -48,470 +54,543 @@ pub enum WireKind { /// Wire discriminants for `system_handshake`. pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 0, response_id: 1, }; /// Wire discriminants for `system_feature_supported`. pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 2, response_id: 3, }; -/// Wire discriminants for `notifications_send_push_notification`. -pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `system_navigate_to`. +pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 4, response_id: 5, }; -/// Wire discriminants for `system_navigate_to`. -pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `system_host_info`. +pub const SYSTEM_HOST_INFO: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 6, response_id: 7, }; -/// Wire discriminants for `permissions_request_device_permission`. -pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `system_get_product_context`. +pub const SYSTEM_GET_PRODUCT_CONTEXT: RequestFrameIds = RequestFrameIds { + trait_id: 193, request_id: 8, response_id: 9, }; -/// Wire discriminants for `permissions_request_remote_permission`. -pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { - request_id: 10, - response_id: 11, -}; - -/// Wire discriminants for `local_storage_read`. -pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { - request_id: 12, - response_id: 13, -}; - -/// Wire discriminants for `local_storage_write`. -pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { - request_id: 14, - response_id: 15, -}; - -/// Wire discriminants for `local_storage_clear`. -pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { - request_id: 16, - response_id: 17, -}; - /// Wire discriminants for `account_connection_status_subscribe`. pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 18, - stop_id: 19, - interrupt_id: 20, - receive_id: 21, + trait_id: 194, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; /// Wire discriminants for `account_get_account`. pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 22, - response_id: 23, + trait_id: 194, + request_id: 4, + response_id: 5, }; /// Wire discriminants for `account_get_account_alias`. pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { - request_id: 24, - response_id: 25, + trait_id: 194, + request_id: 6, + response_id: 7, }; /// Wire discriminants for `account_create_account_proof`. pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { - request_id: 26, - response_id: 27, + trait_id: 194, + request_id: 8, + response_id: 9, }; /// Wire discriminants for `account_get_legacy_accounts`. pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { - request_id: 28, - response_id: 29, -}; - -/// Wire discriminants for `signing_create_transaction`. -pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { - request_id: 30, - response_id: 31, -}; - -/// Wire discriminants for `signing_create_transaction_with_legacy_account`. -pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 32, - response_id: 33, -}; - -/// Wire discriminants for `signing_sign_raw_with_legacy_account`. -pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 34, - response_id: 35, -}; - -/// Wire discriminants for `signing_sign_payload_with_legacy_account`. -pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { - request_id: 36, - response_id: 37, -}; - -/// Wire discriminants for `chat_create_room`. -pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { - request_id: 38, - response_id: 39, -}; - -/// Wire discriminants for `chat_register_bot`. -pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { - request_id: 40, - response_id: 41, -}; - -/// Wire discriminants for `chat_list_subscribe`. -pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 42, - stop_id: 43, - interrupt_id: 44, - receive_id: 45, -}; - -/// Wire discriminants for `chat_post_message`. -pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { - request_id: 46, - response_id: 47, -}; - -/// Wire discriminants for `chat_action_subscribe`. -pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 48, - stop_id: 49, - interrupt_id: 50, - receive_id: 51, + trait_id: 194, + request_id: 10, + response_id: 11, }; -/// Wire discriminants for `chat_custom_message_render`. -pub const CHAT_CUSTOM_MESSAGE_RENDER: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 52, - stop_id: 53, - interrupt_id: 54, - receive_id: 55, +/// Wire discriminants for `account_get_user_id`. +pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 12, + response_id: 13, }; -/// Wire discriminants for `statement_store_subscribe`. -pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 56, - stop_id: 57, - interrupt_id: 58, - receive_id: 59, +/// Wire discriminants for `account_request_login`. +pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 14, + response_id: 15, }; -/// Wire discriminants for `statement_store_create_proof`. -pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { - request_id: 60, - response_id: 61, +/// Wire discriminants for `account_sign_vrf`. +pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 16, + response_id: 17, }; -/// Wire discriminants for `statement_store_submit`. -pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { - request_id: 62, - response_id: 63, +/// Wire discriminants for `account_register_ring_vrf_key`. +pub const ACCOUNT_REGISTER_RING_VRF_KEY: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 168, + response_id: 169, }; -/// Wire discriminants for `preimage_lookup_subscribe`. -pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 64, - stop_id: 65, - interrupt_id: 66, - receive_id: 67, +/// Wire discriminants for `account_list_ring_vrf_keys`. +pub const ACCOUNT_LIST_RING_VRF_KEYS: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 170, + response_id: 171, }; -/// Wire discriminants for `preimage_submit`. -pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { - request_id: 68, - response_id: 69, +/// Wire discriminants for `account_ring_vrf_sign`. +pub const ACCOUNT_RING_VRF_SIGN: RequestFrameIds = RequestFrameIds { + trait_id: 194, + request_id: 172, + response_id: 173, }; /// Wire discriminants for `chain_follow_head_subscribe`. pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 76, - stop_id: 77, - interrupt_id: 78, - receive_id: 79, + trait_id: 195, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; /// Wire discriminants for `chain_get_head_header`. pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { - request_id: 80, - response_id: 81, + trait_id: 195, + request_id: 4, + response_id: 5, }; /// Wire discriminants for `chain_get_head_body`. pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { - request_id: 82, - response_id: 83, + trait_id: 195, + request_id: 6, + response_id: 7, }; /// Wire discriminants for `chain_get_head_storage`. pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { - request_id: 84, - response_id: 85, + trait_id: 195, + request_id: 8, + response_id: 9, }; /// Wire discriminants for `chain_call_head`. pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { - request_id: 86, - response_id: 87, + trait_id: 195, + request_id: 10, + response_id: 11, }; /// Wire discriminants for `chain_unpin_head`. pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { - request_id: 88, - response_id: 89, + trait_id: 195, + request_id: 12, + response_id: 13, }; /// Wire discriminants for `chain_continue_head`. pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { - request_id: 90, - response_id: 91, + trait_id: 195, + request_id: 14, + response_id: 15, }; /// Wire discriminants for `chain_stop_head_operation`. pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { - request_id: 92, - response_id: 93, + trait_id: 195, + request_id: 16, + response_id: 17, }; /// Wire discriminants for `chain_get_spec_genesis_hash`. pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { - request_id: 94, - response_id: 95, + trait_id: 195, + request_id: 18, + response_id: 19, }; /// Wire discriminants for `chain_get_spec_chain_name`. pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { - request_id: 96, - response_id: 97, + trait_id: 195, + request_id: 20, + response_id: 21, }; /// Wire discriminants for `chain_get_spec_properties`. pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { - request_id: 98, - response_id: 99, + trait_id: 195, + request_id: 22, + response_id: 23, }; /// Wire discriminants for `chain_broadcast_transaction`. pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { - request_id: 100, - response_id: 101, + trait_id: 195, + request_id: 24, + response_id: 25, }; /// Wire discriminants for `chain_stop_transaction`. pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { - request_id: 102, - response_id: 103, -}; - -/// Wire discriminants for `theme_subscribe`. -pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 104, - stop_id: 105, - interrupt_id: 106, - receive_id: 107, -}; - -/// Wire discriminants for `entropy_derive`. -pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { - request_id: 108, - response_id: 109, -}; - -/// Wire discriminants for `account_get_user_id`. -pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { - request_id: 110, - response_id: 111, -}; - -/// Wire discriminants for `account_request_login`. -pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { - request_id: 112, - response_id: 113, -}; - -/// Wire discriminants for `signing_sign_raw`. -pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { - request_id: 114, - response_id: 115, -}; - -/// Wire discriminants for `signing_sign_payload`. -pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { - request_id: 116, - response_id: 117, + trait_id: 195, + request_id: 26, + response_id: 27, }; -/// Wire discriminants for `payment_balance_subscribe`. -pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 118, - stop_id: 119, - interrupt_id: 120, - receive_id: 121, +/// Wire discriminants for `chain_get_chain_info`. +pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { + trait_id: 195, + request_id: 166, + response_id: 167, }; -/// Wire discriminants for `payment_top_up`. -pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { - request_id: 122, - response_id: 123, +/// Wire discriminants for `chat_create_room`. +pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { + trait_id: 196, + request_id: 0, + response_id: 1, }; -/// Wire discriminants for `payment_request`. -pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { - request_id: 124, - response_id: 125, +/// Wire discriminants for `chat_register_bot`. +pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { + trait_id: 196, + request_id: 2, + response_id: 3, }; -/// Wire discriminants for `payment_status_subscribe`. -pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 126, - stop_id: 127, - interrupt_id: 128, - receive_id: 129, +/// Wire discriminants for `chat_list_subscribe`. +pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 196, + start_id: 4, + stop_id: 5, + interrupt_id: 6, + receive_id: 7, }; -/// Wire discriminants for `resource_allocation_request`. -pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { - request_id: 130, - response_id: 131, +/// Wire discriminants for `chat_post_message`. +pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { + trait_id: 196, + request_id: 8, + response_id: 9, }; -/// Wire discriminants for `statement_store_create_proof_authorized`. -pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { - request_id: 132, - response_id: 133, +/// Wire discriminants for `chat_action_subscribe`. +pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 196, + start_id: 10, + stop_id: 11, + interrupt_id: 12, + receive_id: 13, }; -/// Wire discriminants for `notifications_cancel_push_notification`. -pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { - request_id: 134, - response_id: 135, +/// Wire discriminants for `chat_custom_message_render`. +pub const CHAT_CUSTOM_MESSAGE_RENDER: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 196, + start_id: 14, + stop_id: 15, + interrupt_id: 16, + receive_id: 17, }; /// Wire discriminants for `coin_payment_create_purse`. pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { - request_id: 136, - response_id: 137, + trait_id: 197, + request_id: 0, + response_id: 1, }; /// Wire discriminants for `coin_payment_query_purse`. pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { - request_id: 138, - response_id: 139, + trait_id: 197, + request_id: 2, + response_id: 3, }; /// Wire discriminants for `coin_payment_rebalance_purse`. pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 140, - stop_id: 141, - interrupt_id: 142, - receive_id: 143, + trait_id: 197, + start_id: 4, + stop_id: 5, + interrupt_id: 6, + receive_id: 7, }; /// Wire discriminants for `coin_payment_delete_purse`. pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 144, - stop_id: 145, - interrupt_id: 146, - receive_id: 147, + trait_id: 197, + start_id: 8, + stop_id: 9, + interrupt_id: 10, + receive_id: 11, }; /// Wire discriminants for `coin_payment_create_receivable`. pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { - request_id: 148, - response_id: 149, + trait_id: 197, + request_id: 12, + response_id: 13, }; /// Wire discriminants for `coin_payment_create_cheque`. pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { - request_id: 150, - response_id: 151, + trait_id: 197, + request_id: 14, + response_id: 15, }; /// Wire discriminants for `coin_payment_deposit`. pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 152, - stop_id: 153, - interrupt_id: 154, - receive_id: 155, + trait_id: 197, + start_id: 16, + stop_id: 17, + interrupt_id: 18, + receive_id: 19, }; /// Wire discriminants for `coin_payment_refund`. pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 156, - stop_id: 157, - interrupt_id: 158, - receive_id: 159, + trait_id: 197, + start_id: 20, + stop_id: 21, + interrupt_id: 22, + receive_id: 23, }; /// Wire discriminants for `coin_payment_listen_for_payment`. pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 160, - stop_id: 161, - interrupt_id: 162, - receive_id: 163, + trait_id: 197, + start_id: 24, + stop_id: 25, + interrupt_id: 26, + receive_id: 27, }; -/// Wire discriminants for `account_sign_vrf`. -pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { - request_id: 164, - response_id: 165, +/// Wire discriminants for `entropy_derive`. +pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { + trait_id: 198, + request_id: 0, + response_id: 1, }; -/// Wire discriminants for `chain_get_chain_info`. -pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { - request_id: 166, - response_id: 167, +/// Wire discriminants for `local_storage_read`. +pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { + trait_id: 199, + request_id: 0, + response_id: 1, }; -/// Wire discriminants for `account_register_ring_vrf_key`. -pub const ACCOUNT_REGISTER_RING_VRF_KEY: RequestFrameIds = RequestFrameIds { - request_id: 168, - response_id: 169, +/// Wire discriminants for `local_storage_write`. +pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { + trait_id: 199, + request_id: 2, + response_id: 3, }; -/// Wire discriminants for `account_list_ring_vrf_keys`. -pub const ACCOUNT_LIST_RING_VRF_KEYS: RequestFrameIds = RequestFrameIds { - request_id: 170, - response_id: 171, +/// Wire discriminants for `local_storage_clear`. +pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { + trait_id: 199, + request_id: 4, + response_id: 5, }; -/// Wire discriminants for `account_ring_vrf_sign`. -pub const ACCOUNT_RING_VRF_SIGN: RequestFrameIds = RequestFrameIds { - request_id: 172, - response_id: 173, +/// Wire discriminants for `notifications_send_push_notification`. +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + trait_id: 200, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `notifications_cancel_push_notification`. +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + trait_id: 200, + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `payment_balance_subscribe`. +pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 201, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, +}; + +/// Wire discriminants for `payment_top_up`. +pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { + trait_id: 201, + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `payment_request`. +pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { + trait_id: 201, + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `payment_status_subscribe`. +pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 201, + start_id: 8, + stop_id: 9, + interrupt_id: 10, + receive_id: 11, +}; + +/// Wire discriminants for `permissions_request_device_permission`. +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { + trait_id: 202, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `permissions_request_remote_permission`. +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { + trait_id: 202, + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `preimage_lookup_subscribe`. +pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 203, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, +}; + +/// Wire discriminants for `preimage_submit`. +pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + trait_id: 203, + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `resource_allocation_request`. +pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { + trait_id: 204, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `signing_create_transaction`. +pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `signing_create_transaction_with_legacy_account`. +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `signing_sign_raw_with_legacy_account`. +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `signing_sign_payload_with_legacy_account`. +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `signing_sign_raw`. +pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `signing_sign_payload`. +pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { + trait_id: 205, + request_id: 10, + response_id: 11, +}; + +/// Wire discriminants for `statement_store_subscribe`. +pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 206, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, +}; + +/// Wire discriminants for `statement_store_create_proof`. +pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { + trait_id: 206, + request_id: 4, + response_id: 5, }; -/// Wire discriminants for `system_get_product_context`. -pub const SYSTEM_GET_PRODUCT_CONTEXT: RequestFrameIds = RequestFrameIds { - request_id: 190, - response_id: 191, +/// Wire discriminants for `statement_store_submit`. +pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { + trait_id: 206, + request_id: 6, + response_id: 7, }; -/// Wire discriminants for `system_host_info`. -pub const SYSTEM_HOST_INFO: RequestFrameIds = RequestFrameIds { - request_id: 192, - response_id: 193, +/// Wire discriminants for `statement_store_create_proof_authorized`. +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { + trait_id: 206, + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `theme_subscribe`. +pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + trait_id: 207, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; /// Wire discriminants for `locale_subscribe`. pub const LOCALE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { - start_id: 194, - stop_id: 195, - interrupt_id: 196, - receive_id: 197, + trait_id: 208, + start_id: 0, + stop_id: 1, + interrupt_id: 2, + receive_id: 3, }; -/// The full wire table. Ordering is part of the wire protocol; -/// only ever append. Removed methods leave their slot empty. +/// The full wire table. Trait ids and per-trait method ordering are +/// part of the wire protocol; only ever append within a trait. +/// Removed methods leave their slot empty. pub const WIRE_TABLE: &[WireEntry] = &[ WireEntry { method: "system_handshake", @@ -521,33 +600,17 @@ pub const WIRE_TABLE: &[WireEntry] = &[ method: "system_feature_supported", kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), }, - WireEntry { - method: "notifications_send_push_notification", - kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), - }, WireEntry { method: "system_navigate_to", kind: WireKind::Request(SYSTEM_NAVIGATE_TO), }, WireEntry { - method: "permissions_request_device_permission", - kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), - }, - WireEntry { - method: "permissions_request_remote_permission", - kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), - }, - WireEntry { - method: "local_storage_read", - kind: WireKind::Request(LOCAL_STORAGE_READ), - }, - WireEntry { - method: "local_storage_write", - kind: WireKind::Request(LOCAL_STORAGE_WRITE), + method: "system_host_info", + kind: WireKind::Request(SYSTEM_HOST_INFO), }, WireEntry { - method: "local_storage_clear", - kind: WireKind::Request(LOCAL_STORAGE_CLEAR), + method: "system_get_product_context", + kind: WireKind::Request(SYSTEM_GET_PRODUCT_CONTEXT), }, WireEntry { method: "account_connection_status_subscribe", @@ -570,64 +633,28 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), }, WireEntry { - method: "signing_create_transaction", - kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), - }, - WireEntry { - method: "signing_create_transaction_with_legacy_account", - kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_raw_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_payload_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "chat_create_room", - kind: WireKind::Request(CHAT_CREATE_ROOM), - }, - WireEntry { - method: "chat_register_bot", - kind: WireKind::Request(CHAT_REGISTER_BOT), - }, - WireEntry { - method: "chat_list_subscribe", - kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), - }, - WireEntry { - method: "chat_post_message", - kind: WireKind::Request(CHAT_POST_MESSAGE), - }, - WireEntry { - method: "chat_action_subscribe", - kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), - }, - WireEntry { - method: "chat_custom_message_render", - kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER), + method: "account_get_user_id", + kind: WireKind::Request(ACCOUNT_GET_USER_ID), }, WireEntry { - method: "statement_store_subscribe", - kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + method: "account_request_login", + kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), }, WireEntry { - method: "statement_store_create_proof", - kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + method: "account_sign_vrf", + kind: WireKind::Request(ACCOUNT_SIGN_VRF), }, WireEntry { - method: "statement_store_submit", - kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + method: "account_register_ring_vrf_key", + kind: WireKind::Request(ACCOUNT_REGISTER_RING_VRF_KEY), }, WireEntry { - method: "preimage_lookup_subscribe", - kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + method: "account_list_ring_vrf_keys", + kind: WireKind::Request(ACCOUNT_LIST_RING_VRF_KEYS), }, WireEntry { - method: "preimage_submit", - kind: WireKind::Request(PREIMAGE_SUBMIT), + method: "account_ring_vrf_sign", + kind: WireKind::Request(ACCOUNT_RING_VRF_SIGN), }, WireEntry { method: "chain_follow_head_subscribe", @@ -682,56 +709,32 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Request(CHAIN_STOP_TRANSACTION), }, WireEntry { - method: "theme_subscribe", - kind: WireKind::Subscription(THEME_SUBSCRIBE), - }, - WireEntry { - method: "entropy_derive", - kind: WireKind::Request(ENTROPY_DERIVE), - }, - WireEntry { - method: "account_get_user_id", - kind: WireKind::Request(ACCOUNT_GET_USER_ID), - }, - WireEntry { - method: "account_request_login", - kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), - }, - WireEntry { - method: "signing_sign_raw", - kind: WireKind::Request(SIGNING_SIGN_RAW), - }, - WireEntry { - method: "signing_sign_payload", - kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), - }, - WireEntry { - method: "payment_balance_subscribe", - kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + method: "chain_get_chain_info", + kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), }, WireEntry { - method: "payment_top_up", - kind: WireKind::Request(PAYMENT_TOP_UP), + method: "chat_create_room", + kind: WireKind::Request(CHAT_CREATE_ROOM), }, WireEntry { - method: "payment_request", - kind: WireKind::Request(PAYMENT_REQUEST), + method: "chat_register_bot", + kind: WireKind::Request(CHAT_REGISTER_BOT), }, WireEntry { - method: "payment_status_subscribe", - kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + method: "chat_list_subscribe", + kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), }, WireEntry { - method: "resource_allocation_request", - kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + method: "chat_post_message", + kind: WireKind::Request(CHAT_POST_MESSAGE), }, WireEntry { - method: "statement_store_create_proof_authorized", - kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + method: "chat_action_subscribe", + kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), }, WireEntry { - method: "notifications_cancel_push_notification", - kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), + method: "chat_custom_message_render", + kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER), }, WireEntry { method: "coin_payment_create_purse", @@ -770,32 +773,108 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), }, WireEntry { - method: "account_sign_vrf", - kind: WireKind::Request(ACCOUNT_SIGN_VRF), + method: "entropy_derive", + kind: WireKind::Request(ENTROPY_DERIVE), }, WireEntry { - method: "chain_get_chain_info", - kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), + method: "local_storage_read", + kind: WireKind::Request(LOCAL_STORAGE_READ), }, WireEntry { - method: "account_register_ring_vrf_key", - kind: WireKind::Request(ACCOUNT_REGISTER_RING_VRF_KEY), + method: "local_storage_write", + kind: WireKind::Request(LOCAL_STORAGE_WRITE), }, WireEntry { - method: "account_list_ring_vrf_keys", - kind: WireKind::Request(ACCOUNT_LIST_RING_VRF_KEYS), + method: "local_storage_clear", + kind: WireKind::Request(LOCAL_STORAGE_CLEAR), }, WireEntry { - method: "account_ring_vrf_sign", - kind: WireKind::Request(ACCOUNT_RING_VRF_SIGN), + method: "notifications_send_push_notification", + kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), }, WireEntry { - method: "system_get_product_context", - kind: WireKind::Request(SYSTEM_GET_PRODUCT_CONTEXT), + method: "notifications_cancel_push_notification", + kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), }, WireEntry { - method: "system_host_info", - kind: WireKind::Request(SYSTEM_HOST_INFO), + method: "payment_balance_subscribe", + kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request(PAYMENT_TOP_UP), + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request(PAYMENT_REQUEST), + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request(PREIMAGE_SUBMIT), + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request(SIGNING_SIGN_RAW), + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription(THEME_SUBSCRIBE), }, WireEntry { method: "locale_subscribe", diff --git a/rust/crates/truapi/src/api/locale.rs b/rust/crates/truapi/src/api/locale.rs index 3dc938dc2..4051bb45e 100644 --- a/rust/crates/truapi/src/api/locale.rs +++ b/rust/crates/truapi/src/api/locale.rs @@ -1,10 +1,11 @@ //! Unified [`Locale`] trait. use crate::versioned::locale::HostLocaleSubscribeItem; -use crate::wire; use crate::{CallContext, Subscription}; +use crate::{wire, wire_trait}; /// Host locale subscription. +#[wire_trait(id = 208)] #[crate::async_trait] pub trait Locale: Send + Sync { /// Subscribe to the host's selected locale. @@ -17,7 +18,7 @@ pub trait Locale: Send + Sync { /// ); /// console.log("locale received:", locale.languageTag); /// ``` - #[wire(start_id = 194)] + #[wire(start_id = 0)] async fn subscribe(&self, _cx: &CallContext) -> Subscription { Subscription::empty() } From f190cdb19d9241493c495ee6add66684aad3880b Mon Sep 17 00:00:00 2001 From: Nidish Date: Tue, 1 Sep 2026 14:42:32 +0530 Subject: [PATCH 11/16] feat(wire): fold direction into the versioned payload (RFC 0028) --- rust/crates/truapi-codegen/src/rust.rs | 157 +- .../truapi-codegen/src/rust/dispatcher.rs | 653 +++- .../truapi-codegen/src/rust/wire_table.rs | 251 +- rust/crates/truapi-codegen/src/rustdoc.rs | 55 +- rust/crates/truapi-codegen/src/ts.rs | 701 ++-- .../truapi-codegen/tests/golden/dispatcher.rs | 3151 +++++++++++------ .../truapi-codegen/tests/golden/wire_table.rs | 444 +-- .../truapi-codegen/tests/golden_rust_emit.rs | 49 + rust/crates/truapi-macros/src/lib.rs | 80 +- rust/crates/truapi/src/api/account.rs | 22 +- rust/crates/truapi/src/api/chain.rs | 28 +- rust/crates/truapi/src/api/chat.rs | 12 +- rust/crates/truapi/src/api/coin_payment.rs | 18 +- rust/crates/truapi/src/api/entropy.rs | 2 +- rust/crates/truapi/src/api/local_storage.rs | 6 +- rust/crates/truapi/src/api/locale.rs | 2 +- rust/crates/truapi/src/api/notifications.rs | 4 +- rust/crates/truapi/src/api/payment.rs | 8 +- rust/crates/truapi/src/api/permissions.rs | 4 +- rust/crates/truapi/src/api/preimage.rs | 4 +- .../truapi/src/api/resource_allocation.rs | 2 +- rust/crates/truapi/src/api/signing.rs | 12 +- rust/crates/truapi/src/api/statement_store.rs | 8 +- rust/crates/truapi/src/api/system.rs | 10 +- rust/crates/truapi/src/api/theme.rs | 2 +- rust/crates/truapi/src/versioned.rs | 96 +- rust/crates/truapi/src/versioned/account.rs | 75 + rust/crates/truapi/src/versioned/chain.rs | 93 + rust/crates/truapi/src/versioned/chat.rs | 40 + .../truapi/src/versioned/coin_payment.rs | 64 + rust/crates/truapi/src/versioned/entropy.rs | 8 + .../truapi/src/versioned/local_storage.rs | 20 + rust/crates/truapi/src/versioned/locale.rs | 11 + .../truapi/src/versioned/notifications.rs | 16 + rust/crates/truapi/src/versioned/payment.rs | 29 + .../truapi/src/versioned/permissions.rs | 16 + rust/crates/truapi/src/versioned/preimage.rs | 16 + .../src/versioned/resource_allocation.rs | 9 + rust/crates/truapi/src/versioned/signing.rs | 42 + .../truapi/src/versioned/statement_store.rs | 30 + rust/crates/truapi/src/versioned/system.rs | 34 + rust/crates/truapi/src/versioned/theme.rs | 8 + 42 files changed, 4135 insertions(+), 2157 deletions(-) diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index 9d21ff984..a0ef1f503 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -152,12 +152,7 @@ mod tests { }, wire: WireAttrs { host_initiated: false, - request_id: Some(request_id), - response_id: None, - start_id: None, - stop_id: None, - interrupt_id: None, - receive_id: None, + id: Some(request_id), }, docs: None, } @@ -174,12 +169,7 @@ mod tests { }), wire: WireAttrs { host_initiated: false, - request_id: None, - response_id: None, - start_id: Some(start_id), - stop_id: None, - interrupt_id: None, - receive_id: None, + id: Some(start_id), }, docs: None, } @@ -210,12 +200,12 @@ mod tests { } fn parse_entries(src: &str) -> Vec<(u8, String)> { - // Each method's ids are emitted as a named const, e.g. - // pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { - // request_id: 68, - // response_id: 69, + // Each method's id is emitted as a named const, e.g. + // pub const PREIMAGE_SUBMIT: MethodIds = MethodIds { + // trait_id: 203, + // method_id: 68, // }; - // Reconstruct the `(id, "{method}_{suffix}")` pairs the assertions use. + // Reconstruct the `(method_id, method_name)` pairs the assertions use. let mut out = Vec::new(); let mut lines = src.lines(); while let Some(line) = lines.next() { @@ -225,9 +215,8 @@ mod tests { let Some(colon) = rest.find(':') else { continue; }; - let is_sub = rest.contains("SubscriptionFrameIds"); // Skip non-id consts (e.g. `WIRE_TABLE: &[WireEntry]`). - if !is_sub && !rest.contains("RequestFrameIds") { + if !rest.contains("MethodIds") { continue; } let method = rest[..colon].trim().to_ascii_lowercase(); @@ -244,28 +233,15 @@ mod tests { } } - let suffixes: &[(&str, &str)] = if is_sub { - &[ - ("start_id", "start"), - ("stop_id", "stop"), - ("interrupt_id", "interrupt"), - ("receive_id", "receive"), - ] - } else { - &[("request_id", "request"), ("response_id", "response")] - }; - for (field, suffix) in suffixes { - out.push((ids[field], format!("{method}_{suffix}"))); - } + out.push((ids["method_id"], method)); } out } - /// A single subscription method must reserve four consecutive wire - /// ids (start/stop/interrupt/receive) even when no sibling methods - /// exist to mask off-by-one errors. + /// A single subscription method reserves exactly one wire id, same as a + /// request method — direction lives in the payload, not the address. #[test] - fn wire_table_subscribe_method_reserves_four_ids() { + fn wire_table_subscribe_method_reserves_one_id() { let api = ApiDefinition { traits: vec![TraitDef { name: "Account".to_string(), @@ -282,12 +258,7 @@ mod tests { let entries = parse_entries(&src); assert_eq!( entries, - vec![ - (18, "account_connection_status_subscribe_start".into()), - (19, "account_connection_status_subscribe_stop".into()), - (20, "account_connection_status_subscribe_interrupt".into()), - (21, "account_connection_status_subscribe_receive".into()), - ], + vec![(18, "account_connection_status_subscribe".into())], ); } @@ -333,13 +304,11 @@ mod tests { assert!( entries .iter() - .any(|(_, tag)| tag == "statement_store_submit_request"), + .any(|(_, tag)| tag == "statement_store_submit"), "wire_table missing prefixed StatementStore tag:\n{table}" ); assert!( - entries - .iter() - .any(|(_, tag)| tag == "preimage_submit_request"), + entries.iter().any(|(_, tag)| tag == "preimage_submit"), "wire_table missing prefixed Preimage tag:\n{table}" ); } @@ -411,10 +380,8 @@ mod tests { assert_eq!(table_a, table_b); } - /// Methods with a `#[wire(request_id = N)]` annotation get a 2-id - /// slot (request/response). Methods with `#[wire(start_id = N)]` - /// get a 4-id slot (start/stop/interrupt/receive). The emitter - /// must enforce that, and reject collisions. + /// Every method, request or subscription, gets exactly one wire id. + /// The emitter must reject collisions between them. #[test] fn wire_table_rejects_collisions() { let api = ApiDefinition { @@ -591,8 +558,7 @@ mod tests { /// envelope, and refusing it would silently cost every trait its last slot. #[test] fn wire_table_allows_method_id_255_outside_the_reserved_trait() { - let mut method = make_request_method("explicit_request", 255); - method.wire.response_id = Some(1); + let method = make_request_method("explicit_request", 255); let api = ApiDefinition { traits: vec![TraitDef { name: "Example".to_string(), @@ -638,62 +604,13 @@ mod tests { assert_eq!(module_for_trait("Account"), "account"); } - /// A request-kind method must not carry subscription wire ids. The - /// emitter rejects `start_id` / `stop_id` / `interrupt_id` / `receive_id` - /// on a `MethodKind::Request`. - #[test] - fn wire_table_request_with_subscription_id_errors() { - let mut method = make_request_method("alpha", 10); - method.wire.start_id = Some(99); - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Permissions".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(197), - methods: vec![method], - docs: None, - }], - public_trait_order: vec!["Permissions".to_string()], - types: vec![], - }; - let err = generate_wire_table(&api).expect_err("request kind + start_id must error"); - let msg = format!("{err}"); - assert!( - msg.contains("must not use subscription wire ids"), - "unexpected error message: {msg}", - ); - } - - /// A subscription-kind method must not carry request wire ids. - #[test] - fn wire_table_subscription_with_request_id_errors() { - let mut method = make_subscription_method("connection_status_subscribe", 18); - method.wire.request_id = Some(99); - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Account".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(193), - methods: vec![method], - docs: None, - }], - public_trait_order: vec!["Account".to_string()], - types: vec![], - }; - let err = generate_wire_table(&api).expect_err("subscription kind + request_id must error"); - let msg = format!("{err}"); - assert!( - msg.contains("must not use request wire ids"), - "unexpected error message: {msg}", - ); - } - - /// A request-kind method missing the mandatory `request_id` annotation - /// must fail emission, not silently default to 0. + /// A method missing the mandatory `#[wire(id = N)]` annotation must fail + /// emission, not silently default to 0 — true for both request and + /// subscription kinds, which now share the same single-id path. #[test] - fn wire_table_missing_request_id_errors() { + fn wire_table_missing_id_errors() { let mut method = make_request_method("alpha", 10); - method.wire.request_id = None; + method.wire.id = None; let api = ApiDefinition { traits: vec![TraitDef { name: "Permissions".to_string(), @@ -705,34 +622,10 @@ mod tests { public_trait_order: vec!["Permissions".to_string()], types: vec![], }; - let err = generate_wire_table(&api).expect_err("missing request_id annotation must error"); - let msg = format!("{err}"); - assert!( - msg.contains("missing #[wire(request_id"), - "unexpected error message: {msg}", - ); - } - - /// Subscription-kind method missing `start_id` is similarly rejected. - #[test] - fn wire_table_missing_start_id_errors() { - let mut method = make_subscription_method("connection_status_subscribe", 18); - method.wire.start_id = None; - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Account".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(193), - methods: vec![method], - docs: None, - }], - public_trait_order: vec!["Account".to_string()], - types: vec![], - }; - let err = generate_wire_table(&api).expect_err("missing start_id annotation must error"); + let err = generate_wire_table(&api).expect_err("missing id annotation must error"); let msg = format!("{err}"); assert!( - msg.contains("missing #[wire(start_id"), + msg.contains("missing #[wire(id"), "unexpected error message: {msg}", ); } diff --git a/rust/crates/truapi-codegen/src/rust/dispatcher.rs b/rust/crates/truapi-codegen/src/rust/dispatcher.rs index 41ac4505f..8856346f3 100644 --- a/rust/crates/truapi-codegen/src/rust/dispatcher.rs +++ b/rust/crates/truapi-codegen/src/rust/dispatcher.rs @@ -47,10 +47,12 @@ pub fn generate_dispatcher(api: &ApiDefinition) -> Result { let mut modules = Vec::with_capacity(traits.len()); let mut uses_raw_err_payload = false; let mut uses_raw_unit_ok_payload = false; + let mut uses_legacy_versioned_helpers = false; for trait_def in &traits { let module = build_module(api, trait_def)?; uses_raw_err_payload |= module.uses_raw_err_payload; uses_raw_unit_ok_payload |= module.uses_raw_unit_ok_payload; + uses_legacy_versioned_helpers |= module.uses_legacy_versioned_helpers; modules.push(module.code); } @@ -61,6 +63,7 @@ pub fn generate_dispatcher(api: &ApiDefinition) -> Result { &traits, uses_raw_err_payload, uses_raw_unit_ok_payload, + uses_legacy_versioned_helpers, ); writeln!(out).unwrap(); write_top_register(&mut out, &traits); @@ -99,6 +102,7 @@ struct ModuleEmission { code: String, uses_raw_err_payload: bool, uses_raw_unit_ok_payload: bool, + uses_legacy_versioned_helpers: bool, } /// Emit the `register_{module}` function for a single trait. @@ -122,6 +126,12 @@ fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result>>()? + .into_iter() + .any(|uses| uses); let fn_name = format!("register_{module}"); let trait_name = &trait_def.name; @@ -139,7 +149,7 @@ fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result Result { + let version_variant = single_variant(api, &version_type)?; + let request_variant = single_variant(api, request)?; + let envelope_path = format!("versioned::{module}::{version_type}"); + let bind = envelope_bind_name(request_variant); + let version_number: u8 = version_variant + .name + .strip_prefix('V') + .and_then(|n| n.parse().ok()) + .ok_or_else(|| { + anyhow::anyhow!( + "Host-initiated method `{}`: envelope variant `{}` is not named `V`", + method.name, + version_variant.name + ) + })?; + formatdoc! {r#" + let envelope = match request {{ + {request_pat} => {envelope_path}::{ev_name}(truapi::versioned::Subscription::Start({bind})), + }}; + subscriptions.start( + wire_table::{ids}, + {version_number}, + parity_scale_codec::Encode::encode(&envelope), + transport, + ) + "#, + request_pat = variant_expr(&request_path, request_variant, bind), + ev_name = version_variant.name, + } + } + None => formatdoc! {r#" + subscriptions.start( + wire_table::{ids}, + 1, + parity_scale_codec::Encode::encode(&request), + transport, + ) + "# + }, + }; + writedoc!( out, r#" @@ -193,19 +249,15 @@ fn write_host_initiated_callers( pub(crate) fn {wire_name}( subscriptions: &HostInitiatedSubscriptionManager, transport: Arc, - request: versioned::{module}::{request}, + request: {request_path}, ) -> truapi::Subscription< Result, > {{ - subscriptions.start( - wire_table::{ids}, - parity_scale_codec::Encode::encode(&request), - transport, - ) - }} "# ) .unwrap(); + write_indented(out, 4, &start_body); + writeln!(out, "}}").unwrap(); } } Ok(()) @@ -317,26 +369,75 @@ impl MethodEmission { }) } - fn write(&self, out: &mut String, host_expr: &str) -> Result<()> { + fn write(&self, out: &mut String, api: &ApiDefinition, host_expr: &str) -> Result<()> { match self.kind { - MethodKind::Request => self.write_request(out, host_expr), + MethodKind::Request => self.write_request(out, api, host_expr), MethodKind::Subscription | MethodKind::ResultSubscription => { - self.write_subscription(out, host_expr) + self.write_subscription(out, api, host_expr) } } } + /// The merged `{Method}Version` wire-envelope type this method uses, if + /// one exists. Derived from the request or item wrapper's name (stripping + /// its `Request`/`Item` suffix, per the authoring convention every real + /// method follows); returns `None` when no such name can be derived + /// (synthetic/test methods whose wrapper names don't follow the + /// convention), in which case the method falls back to the legacy + /// (pre-nested-envelope) wire shape entirely unchanged. When a name *can* + /// be derived but doesn't resolve to a real single-version wrapper, that + /// is treated as an authoring bug and fails loudly instead of silently + /// falling back. + fn envelope<'a>(&self, api: &'a ApiDefinition) -> Result>> { + let request_name = match &self.request_payload { + Some(WirePayload::Versioned(name)) => Some(name.as_str()), + _ => None, + }; + let Some(type_name) = envelope_type_name(request_name, self.item_wrapper.as_deref()) else { + return Ok(None); + }; + let variant = single_variant(api, &type_name)?; + Ok(Some(EnvelopeInfo { type_name, variant })) + } + fn uses_raw_err_payload(&self) -> bool { matches!(self.request_payload, Some(WirePayload::Raw(_))) || self.uses_raw_unit_ok_payload() } + /// Whether this method's *legacy* (non-nested-envelope) codegen path + /// calls any of `encode_versioned_{ok,err,unit_ok,interrupt}_payload`. + /// Every real method resolves to the nested envelope (see [`Self::envelope`]), + /// so these helpers end up unused there; this lets the emitted `use`s stay + /// conditional on some method actually needing them. + fn uses_legacy_versioned_helpers(&self, api: &ApiDefinition) -> Result { + if self.envelope(api)?.is_some() { + return Ok(false); + } + Ok(match self.kind { + MethodKind::Request => { + self.response_wrapper.is_some() + || matches!(self.error_payload, WirePayload::Versioned(_)) + } + MethodKind::Subscription | MethodKind::ResultSubscription => { + matches!(self.error_payload, WirePayload::Versioned(_)) + } + }) + } + fn uses_raw_unit_ok_payload(&self) -> bool { matches!(self.kind, MethodKind::Request) && self.response_wrapper.is_none() && matches!(self.error_payload, WirePayload::Raw(_)) } - fn write_request(&self, out: &mut String, host_expr: &str) -> Result<()> { + fn write_request(&self, out: &mut String, api: &ApiDefinition, host_expr: &str) -> Result<()> { + match self.envelope(api)? { + Some(env) => self.write_request_envelope(out, api, host_expr, &env), + None => self.write_request_legacy(out, host_expr), + } + } + + fn write_request_legacy(&self, out: &mut String, host_expr: &str) -> Result<()> { let module = &self.module; let method = &self.name; let ids = const_name(&self.wire_name); @@ -510,7 +611,19 @@ impl MethodEmission { Ok(()) } - fn write_subscription(&self, out: &mut String, host_expr: &str) -> Result<()> { + fn write_subscription( + &self, + out: &mut String, + api: &ApiDefinition, + host_expr: &str, + ) -> Result<()> { + match self.envelope(api)? { + Some(env) => self.write_subscription_envelope(out, api, host_expr, &env), + None => self.write_subscription_legacy(out, host_expr), + } + } + + fn write_subscription_legacy(&self, out: &mut String, host_expr: &str) -> Result<()> { let module = &self.module; let method = &self.name; let ids = const_name(&self.wire_name); @@ -643,7 +756,7 @@ impl MethodEmission { } writeln!( out, - " Ok(subscription_stream::(stream))" + " Ok(({target_version_expr}, subscription_stream::(stream)))" ) .unwrap(); write_indented( @@ -660,6 +773,373 @@ impl MethodEmission { Ok(()) } + /// Generates a request/response handler that decodes and encodes through + /// the nested wire envelope: incoming bytes are the merged + /// `{Method}Version` type, matched for the `Request` direction; outgoing + /// bytes are constructed as that same type's `Response` direction, + /// wrapping `Result>`. The trait method itself is + /// unchanged — it still takes/returns the original bare versioned + /// request/response/error types; only the wire shape nests differently. + fn write_request_envelope( + &self, + out: &mut String, + api: &ApiDefinition, + host_expr: &str, + env: &EnvelopeInfo<'_>, + ) -> Result<()> { + let module = &self.module; + let method = &self.name; + let ids = const_name(&self.wire_name); + let envelope_path = format!("versioned::{module}::{}", env.type_name); + let version_variant = &env.variant.name; + + let Some(WirePayload::Versioned(request_name)) = &self.request_payload else { + bail!("Method `{method}`: nested envelope requires a versioned request"); + }; + let Some(error_name) = self.error_payload.versioned_name() else { + bail!("Method `{method}`: nested envelope requires a versioned error"); + }; + let request_variant = single_variant(api, request_name)?; + let error_variant = single_variant(api, error_name)?; + let error_bare_ty = variant_bare_type(error_variant)?; + let request_path = format!("versioned::{module}::{request_name}"); + let error_path = format!("versioned::{module}::{error_name}"); + + let wrap_response = |inner: &str| { + format!( + "{envelope_path}::{version_variant}(truapi::versioned::Request::Response({inner}))" + ) + }; + + writeln!(out, " {{").unwrap(); + self.write_execution_binding(out); + write_indented( + out, + 8, + &formatdoc! { + r#" + let host = {host_expr}; + dispatcher.on_request(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ + let host = host.clone(); + Box::pin(async move {{ + "# + }, + ); + + write_indented( + out, + 16, + &formatdoc! { + r#" + let envelope: {envelope_path} = match Decode::decode(&mut &bytes[..]) {{ + Ok(envelope) => envelope, + Err(err) => {{ + let error: truapi::CallError<{error_bare_ty}> = + truapi::CallError::MalformedFrame {{ reason: err.to_string() }}; + return Ok({wrap_err}.encode()); + }} + }}; + let request: {request_path} = match envelope {{ + {envelope_path}::{version_variant}(truapi::versioned::Request::Request({bind})) => {request_ctor}, + _ => {{ + let error: truapi::CallError<{error_bare_ty}> = + truapi::CallError::MalformedFrame {{ + reason: "expected a request-direction frame".to_string(), + }}; + return Ok({wrap_err}.encode()); + }} + }}; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + "#, + bind = envelope_bind_name(request_variant), + request_ctor = variant_expr(&request_path, request_variant, envelope_bind_name(request_variant)), + wrap_err = wrap_response("Err(error)"), + }, + ); + + if self.required_execution.is_some() { + write_indented( + out, + 16, + &formatdoc! { + r#" + if !execution_allowed {{ + let error: truapi::CallError<{error_bare_ty}> = truapi::CallError::Denied; + return Ok({wrap_err}.encode()); + }} + "#, + wrap_err = wrap_response("Err(error)"), + }, + ); + } + + let unwrap_call_error = rewrap_call_error(&error_path, error_variant, "downgraded"); + + match &self.response_wrapper { + Some(response_name) => { + let response_variant = single_variant(api, response_name)?; + let response_path = format!("versioned::{module}::{response_name}"); + let ok_extract = bare_ident_or_unit(response_variant); + write_indented( + out, + 16, + &formatdoc! { + r#" + let response: {response_path} = match host.{method}(&cx, request).await {{ + Ok(value) => value, + Err(err) => {{ + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError<{error_bare_ty}> = {unwrap_call_error}; + return Ok({wrap_err}.encode()); + }} + }}; + let response = <{response_path} as truapi::versioned::FromLatest>::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); + // Downgraded to the caller's version: a handler answers in + // latest terms, and a peer that asked in an older version + // cannot decode a newer variant. + Ok(match response {{ + {response_pat} => {wrap_ok}, + }}.encode()) + "#, + wrap_err = wrap_response("Err(error)"), + response_pat = variant_expr(&response_path, response_variant, "bare"), + wrap_ok = wrap_response(&format!("Ok({ok_extract})")), + }, + ); + } + None => { + write_indented( + out, + 16, + &formatdoc! { + r#" + match host.{method}(&cx, request).await {{ + Ok(()) => Ok({wrap_unit_ok}.encode()), + Err(err) => {{ + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError<{error_bare_ty}> = {unwrap_call_error}; + Ok({wrap_err}.encode()) + }} + }} + "#, + wrap_unit_ok = wrap_response("Ok(())"), + wrap_err = wrap_response("Err(error)"), + }, + ); + } + } + + write_indented( + out, + 4, + indoc! { + r#" + }) + }); + } + "# + }, + ); + Ok(()) + } + + /// Generates a subscription handler through the nested wire envelope: + /// incoming bytes are the merged `{Method}Version` type, matched for the + /// `Start` direction (the framework intercepts `Stop` frames before they + /// ever reach a registered handler, so only `Start` is handled here); + /// outgoing item frames are constructed as that type's `Receive` + /// direction. Natural stream completion (`Interrupt(None)`) is encoded + /// generically by the runtime with no per-method type knowledge needed, + /// so it isn't generated here. + fn write_subscription_envelope( + &self, + out: &mut String, + api: &ApiDefinition, + host_expr: &str, + env: &EnvelopeInfo<'_>, + ) -> Result<()> { + let module = &self.module; + let method = &self.name; + let ids = const_name(&self.wire_name); + let envelope_path = format!("versioned::{module}::{}", env.type_name); + let version_variant = &env.variant.name; + // The nested envelope currently supports exactly one version per + // method (see `single_variant`), so the version number is this + // literal, not something decoded per frame. + let version_number: u8 = version_variant + .strip_prefix('V') + .and_then(|n| n.parse().ok()) + .ok_or_else(|| { + anyhow::anyhow!( + "Method `{method}`: envelope variant `{version_variant}` is not named `V`" + ) + })?; + + let Some(item_name) = self.item_wrapper.as_deref() else { + bail!("Method `{method}`: subscription methods must have an item wrapper"); + }; + let item_variant = single_variant(api, item_name)?; + let item_path = format!("versioned::{module}::{item_name}"); + + let is_result_sub = matches!(self.kind, MethodKind::ResultSubscription); + let has_request = matches!(self.request_payload, Some(WirePayload::Versioned(_))); + + let (start_ty, start_ctor, start_bind) = match &self.request_payload { + Some(WirePayload::Versioned(request_name)) => { + let request_variant = single_variant(api, request_name)?; + let request_path = format!("versioned::{module}::{request_name}"); + let bind = envelope_bind_name(request_variant); + ( + request_path.clone(), + variant_expr(&request_path, request_variant, bind), + bind, + ) + } + _ => ("()".to_string(), "()".to_string(), "_bare"), + }; + + let error_bare_ty = if is_result_sub { + let Some(error_name) = self.error_payload.versioned_name() else { + bail!( + "Method `{method}`: result subscription methods must have a versioned error wrapper" + ); + }; + variant_bare_type(single_variant(api, error_name)?)? + } else { + "truapi::latest::GenericError".to_string() + }; + + let wrap_start_err = format!( + "{envelope_path}::{version_variant}(truapi::versioned::Subscription::Interrupt(Some(error)))" + ); + // A unit-typed binding trips clippy's `let_unit_value` lint, so a + // subscription with no `Start` payload names it `_request` instead + // of relying on a follow-up `let _ = request;` to silence it. + let request_binding = if has_request { "request" } else { "_request" }; + + writeln!(out, " {{").unwrap(); + self.write_execution_binding(out); + write_indented( + out, + 8, + &formatdoc! { + r#" + let host = {host_expr}; + dispatcher.on_subscription(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ + let host = host.clone(); + Box::pin(async move {{ + "# + }, + ); + + write_indented( + out, + 16, + &formatdoc! { + r#" + let envelope: {envelope_path} = match Decode::decode(&mut &bytes[..]) {{ + Ok(envelope) => envelope, + Err(err) => {{ + let error: truapi::CallError<{error_bare_ty}> = + truapi::CallError::MalformedFrame {{ reason: err.to_string() }}; + return Err({wrap_start_err}.encode()); + }} + }}; + let {request_binding}: {start_ty} = match envelope {{ + {envelope_path}::{version_variant}(truapi::versioned::Subscription::Start({start_bind})) => {start_ctor}, + _ => {{ + let error: truapi::CallError<{error_bare_ty}> = + truapi::CallError::MalformedFrame {{ + reason: "expected a start-direction frame".to_string(), + }}; + return Err({wrap_start_err}.encode()); + }} + }}; + let cx = CallContext::with_request_id(request_id.clone()); + "# + }, + ); + let call_args = if has_request { "&cx, request" } else { "&cx" }; + + if self.required_execution.is_some() { + write_indented( + out, + 16, + &formatdoc! { + r#" + if !execution_allowed {{ + let error: truapi::CallError<{error_bare_ty}> = truapi::CallError::Denied; + return Err({wrap_start_err}.encode()); + }} + "# + }, + ); + } + + if is_result_sub { + let error_name = self.error_payload.versioned_name().expect("checked above"); + let error_variant = single_variant(api, error_name)?; + let error_path = format!("versioned::{module}::{error_name}"); + let unwrap_call_error = rewrap_call_error(&error_path, error_variant, "downgraded"); + write_indented( + out, + 16, + &formatdoc! { + r#" + let stream = match host.{method}({call_args}).await {{ + Ok(sub) => sub, + Err(err) => {{ + let downgraded = downgrade_call_error(err, {version_number}); + let error: truapi::CallError<{error_bare_ty}> = {unwrap_call_error}; + return Err({wrap_start_err}.encode()); + }} + }}; + "# + }, + ); + } else { + writeln!( + out, + " let stream = host.{method}({call_args}).await;" + ) + .unwrap(); + } + + write_indented( + out, + 16, + &formatdoc! { + r#" + let stream = futures::StreamExt::map(stream, |item: {item_path}| match item {{ + {item_pat} => {envelope_path}::{version_variant}( + truapi::versioned::Subscription::Receive({item_extract}), + ), + }}); + Ok(({version_number}, subscription_stream::<{envelope_path}, _>(stream))) + "#, + item_pat = variant_expr(&item_path, item_variant, "bare"), + item_extract = bare_ident_or_unit(item_variant), + }, + ); + + write_indented( + out, + 4, + indoc! { + r#" + }) + }); + } + "# + }, + ); + Ok(()) + } + fn write_execution_binding(&self, out: &mut String) { if let Some(required) = self.required_execution.as_ref() { writeln!( @@ -716,6 +1196,127 @@ impl MethodEmission { } } +/// The merged wire-envelope type this method's frames nest into +/// (`{Method}Version`), and its single declared version variant. Only +/// single-version envelopes are currently generated; see [`single_variant`]. +struct EnvelopeInfo<'a> { + type_name: String, + variant: &'a VariantDef, +} + +/// Derive the merged `{Method}Version` wire-envelope type name from a +/// method's request or item wrapper name, stripping its `Request`/`Item` +/// suffix — the naming convention every hand-authored envelope type follows. +/// Returns `None` when neither name is present or neither ends in the +/// expected suffix (synthetic/test methods opt out of the nested envelope +/// entirely this way, falling back to the legacy wire shape). +fn envelope_type_name(request: Option<&str>, item: Option<&str>) -> Option { + if let Some(base) = request.and_then(|name| name.strip_suffix("Request")) { + return Some(format!("{base}Version")); + } + if let Some(base) = item.and_then(|name| name.strip_suffix("Item")) { + return Some(format!("{base}Version")); + } + None +} + +/// Look up a versioned wrapper type's single declared variant. The nested +/// wire envelope currently supports exactly one version per method; a type +/// with more is a hard error rather than a silent partial implementation. +fn single_variant<'a>(api: &'a ApiDefinition, name: &str) -> Result<&'a VariantDef> { + let type_def = api + .types + .iter() + .find(|type_def| type_def.name == name) + .ok_or_else(|| { + anyhow::anyhow!("versioned wrapper type `{name}` not found in extracted API") + })?; + let TypeDefKind::Enum(variants) = &type_def.kind else { + bail!("versioned wrapper type `{name}` is not an enum"); + }; + match variants.as_slice() { + [only] => Ok(only), + other => bail!( + "versioned wrapper `{name}` has {} versions; the nested wire envelope \ + currently supports exactly one version per method", + other.len() + ), + } +} + +/// Rust expression naming `variant` of `type_path`: either constructing it +/// from `bare_ident`, or (identical syntax) pattern-matching it and binding +/// `bare_ident` — unit variants take neither parens nor `bare_ident`. +fn variant_expr(type_path: &str, variant: &VariantDef, bare_ident: &str) -> String { + match &variant.fields { + VariantFields::Unit => format!("{type_path}::{}", variant.name), + VariantFields::Unnamed(_) => format!("{type_path}::{}({bare_ident})", variant.name), + VariantFields::Named(_) => { + unreachable!("versioned wrapper variants are unit or single-field tuples") + } + } +} + +/// The identifier (or unit literal) a matched [`variant_expr`] binds: +/// `"bare"` for a single-field tuple variant, `"()"` for a unit variant. +fn bare_ident_or_unit(variant: &VariantDef) -> &'static str { + match &variant.fields { + VariantFields::Unit => "()", + VariantFields::Unnamed(_) => "bare", + VariantFields::Named(_) => { + unreachable!("versioned wrapper variants are unit or single-field tuples") + } + } +} + +/// Identifier to bind an envelope direction tag's inner value as (`Request`, +/// `Start`, ...), which is always structurally present even when the +/// destination variant it reconstructs is a unit that discards it. Binding +/// as `"bare"` when the destination will reference it and `"_bare"` when it +/// won't avoids an unused-variable warning on the otherwise-always-present +/// envelope binding. +fn envelope_bind_name(destination_variant: &VariantDef) -> &'static str { + match &destination_variant.fields { + VariantFields::Unit => "_bare", + VariantFields::Unnamed(_) => "bare", + VariantFields::Named(_) => { + unreachable!("versioned wrapper variants are unit or single-field tuples") + } + } +} + +/// The Rust type of `variant`'s bare payload (`()` for a unit variant). +fn variant_bare_type(variant: &VariantDef) -> Result { + match &variant.fields { + VariantFields::Unit => Ok("()".to_string()), + VariantFields::Unnamed(types) if types.len() == 1 => rust_type_ref(&types[0]), + _ => bail!( + "versioned wrapper variant `{}` must be unit or a single-field tuple", + variant.name + ), + } +} + +/// Emit a match expression that rewraps a `truapi::CallError<{old versioned +/// error}>` value (`scrutinee`) into `truapi::CallError<{bare domain +/// error}>` — unwrapping the domain payload's own (now-redundant) version +/// tag, since the nested envelope's outer version tag already carries that +/// information. Framework variants (`Denied`/`Unsupported`/`MalformedFrame`/ +/// `HostFailure`) carry no domain payload and pass through unchanged. +fn rewrap_call_error(error_path: &str, error_variant: &VariantDef, scrutinee: &str) -> String { + let domain_pattern = variant_expr(error_path, error_variant, "bare"); + let domain_bare = bare_ident_or_unit(error_variant); + formatdoc! {r#" + match {scrutinee} {{ + truapi::CallError::Domain({domain_pattern}) => truapi::CallError::Domain({domain_bare}), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame {{ reason }} => truapi::CallError::MalformedFrame {{ reason }}, + truapi::CallError::HostFailure {{ reason }} => truapi::CallError::HostFailure {{ reason }}, + }}"# + } +} + impl WirePayload { fn versioned_name(&self) -> Option<&str> { match self { @@ -883,13 +1484,14 @@ fn write_imports( traits: &[&TraitDef], uses_raw_err_payload: bool, uses_raw_unit_ok_payload: bool, + uses_legacy_versioned_helpers: bool, ) { writedoc!( out, r#" use std::sync::Arc; - use parity_scale_codec::Decode; + use parity_scale_codec::{{Decode, Encode}}; use truapi::CallContext; use truapi::api::{{ @@ -907,11 +1509,7 @@ fn write_imports( use truapi_platform::ProductExecutionKind; use crate::dispatcher::Dispatcher; - use crate::frame::encode_versioned_err_payload; - use crate::frame::encode_versioned_interrupt_payload; use crate::frame::downgrade_call_error; - use crate::frame::encode_versioned_ok_payload; - use crate::frame::encode_versioned_unit_ok_payload; use crate::generated::wire_table; use crate::subscription::{{HostInitiatedSubscriptionManager, subscription_stream}}; use crate::transport::Transport; @@ -924,6 +1522,23 @@ fn write_imports( if uses_raw_unit_ok_payload { writeln!(out, "use crate::frame::encode_raw_unit_ok_payload;").unwrap(); } + // Every real method resolves to the nested wire envelope (see + // `MethodEmission::envelope`), which encodes by constructing and + // encoding its envelope value directly rather than through these + // helpers. They remain for the legacy (non-nested-envelope) codegen + // path, imported only when some method actually falls back to it. + if uses_legacy_versioned_helpers { + writedoc!( + out, + r#" + use crate::frame::encode_versioned_err_payload; + use crate::frame::encode_versioned_interrupt_payload; + use crate::frame::encode_versioned_ok_payload; + use crate::frame::encode_versioned_unit_ok_payload; + "# + ) + .unwrap(); + } } fn write_top_register(out: &mut String, traits: &[&TraitDef]) { diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs index afa6e537c..6798de788 100644 --- a/rust/crates/truapi-codegen/src/rust/wire_table.rs +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -1,12 +1,12 @@ //! Emits `wire_table.rs`: the (trait, method) discriminant lookup table the -//! server uses to pair incoming wire frames with their request, response, or -//! subscription role. +//! server uses to pair incoming wire frames with their registered handler. //! //! A trait-level `#[wire_trait(id = N)]` annotation assigns the trait -//! discriminant; per-method `#[wire(...)]` annotations decide method-id -//! assignment within the trait: -//! - request methods reserve `(request_id, response_id)`. -//! - subscription methods reserve `(start_id, stop_id, interrupt_id, receive_id)`. +//! discriminant; a per-method `#[wire(id = N)]` annotation assigns the method +//! discriminant. One id addresses a method regardless of its shape — +//! direction (request/response, or a subscription's start/stop/interrupt/ +//! receive) is carried inside the method's versioned payload, not by a +//! separate id. //! //! Missing annotations and collisions (per trait) both hard-fail codegen. @@ -21,26 +21,26 @@ use crate::rustdoc::*; use super::{const_name, wire_method_name}; use crate::RESERVED_PROTOCOL_ERROR_TRAIT_ID; +/// Wire discriminants for one method: the pair every frame it ever sends or +/// receives carries. Direction and version are carried inside the payload. #[derive(Debug, Clone, Copy)] -struct WireEntry { +struct MethodIds { trait_id: u8, - request_id: u8, - response_id: u8, + method_id: u8, } #[derive(Debug, Clone, Copy)] -struct SubEntry { - trait_id: u8, - start_id: u8, - stop_id: u8, - interrupt_id: u8, - receive_id: u8, +enum MethodEntry { + Request(MethodIds), + Subscription(MethodIds), } -#[derive(Debug, Clone, Copy)] -enum MethodEntry { - Request(WireEntry), - Subscription(SubEntry), +impl MethodEntry { + fn ids(self) -> MethodIds { + match self { + MethodEntry::Request(ids) | MethodEntry::Subscription(ids) => ids, + } + } } /// Emit the contents of `wire_table.rs`. @@ -89,15 +89,12 @@ pub fn generate_wire_table(api: &ApiDefinition) -> Result { } } - method_entries.sort_by_key(|(_, entry)| match entry { - MethodEntry::Request(WireEntry { + method_entries.sort_by_key(|(_, entry)| { + let MethodIds { trait_id, - request_id, - .. - }) => (*trait_id, *request_id), - MethodEntry::Subscription(SubEntry { - trait_id, start_id, .. - }) => (*trait_id, *start_id), + method_id, + } = entry.ids(); + (trait_id, method_id) }); render(&method_entries) @@ -128,103 +125,36 @@ fn trait_wire_id(trait_def: &TraitDef) -> Result { } fn method_entry(trait_def: &TraitDef, trait_id: u8, method: &MethodDef) -> Result { - let wire = &method.wire; + let method_id = method.wire.id.ok_or_else(|| { + anyhow::anyhow!( + "method `{}::{}` is missing #[wire(id = N)] annotation", + trait_def.name, + method.name + ) + })?; + let ids = MethodIds { + trait_id, + method_id, + }; match method.kind { - MethodKind::Request => { - if wire.start_id.is_some() - || wire.stop_id.is_some() - || wire.interrupt_id.is_some() - || wire.receive_id.is_some() - { - bail!( - "method `{}::{}` is a request and must not use subscription wire ids", - trait_def.name, - method.name - ); - } - let request_id = wire.request_id.ok_or_else(|| { - anyhow::anyhow!( - "method `{}::{}` is missing #[wire(request_id = N)] annotation", - trait_def.name, - method.name - ) - })?; - let response_id = infer_id(wire.response_id, request_id, 1, &method.name)?; - Ok(MethodEntry::Request(WireEntry { - trait_id, - request_id, - response_id, - })) - } + MethodKind::Request => Ok(MethodEntry::Request(ids)), MethodKind::Subscription | MethodKind::ResultSubscription => { - if wire.request_id.is_some() || wire.response_id.is_some() { - bail!( - "method `{}::{}` is a subscription and must not use request wire ids", - trait_def.name, - method.name - ); - } - let start_id = wire.start_id.ok_or_else(|| { - anyhow::anyhow!( - "method `{}::{}` is missing #[wire(start_id = N)] annotation", - trait_def.name, - method.name - ) - })?; - let stop_id = infer_id(wire.stop_id, start_id, 1, &method.name)?; - let interrupt_id = infer_id(wire.interrupt_id, start_id, 2, &method.name)?; - let receive_id = infer_id(wire.receive_id, start_id, 3, &method.name)?; - Ok(MethodEntry::Subscription(SubEntry { - trait_id, - start_id, - stop_id, - interrupt_id, - receive_id, - })) + Ok(MethodEntry::Subscription(ids)) } } } -fn infer_id(explicit: Option, anchor: u8, offset: u8, method_name: &str) -> Result { - if let Some(id) = explicit { - return Ok(id); - } - anchor - .checked_add(offset) - .ok_or_else(|| anyhow::anyhow!("wire id overflow on `{method_name}` (base {anchor})")) -} - fn insert_entry( seen: &mut BTreeMap<(u8, u8), String>, method_name: &str, entry: MethodEntry, ) -> Result<()> { - let pairs: Vec<(u8, u8, String)> = match entry { - MethodEntry::Request(WireEntry { - trait_id, - request_id, - response_id, - }) => vec![ - (trait_id, request_id, format!("{method_name}_request")), - (trait_id, response_id, format!("{method_name}_response")), - ], - MethodEntry::Subscription(SubEntry { - trait_id, - start_id, - stop_id, - interrupt_id, - receive_id, - }) => vec![ - (trait_id, start_id, format!("{method_name}_start")), - (trait_id, stop_id, format!("{method_name}_stop")), - (trait_id, interrupt_id, format!("{method_name}_interrupt")), - (trait_id, receive_id, format!("{method_name}_receive")), - ], - }; - for (trait_id, id, tag) in pairs { - if let Some(existing) = seen.insert((trait_id, id), tag.clone()) { - bail!("wire id ({trait_id}, {id}) reused: `{existing}` and `{tag}` collide"); - } + let MethodIds { + trait_id, + method_id, + } = entry.ids(); + if let Some(existing) = seen.insert((trait_id, method_id), method_name.to_string()) { + bail!("wire id ({trait_id}, {method_id}) reused: `{existing}` and `{method_name}` collide"); } Ok(()) } @@ -238,38 +168,23 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { //! //! Auto-generated by truapi-codegen. Do not edit. //! - //! Every frame carries a `(trait, method)` discriminant pair. Each - //! method reserves either two method ids (request/response) or four - //! (start/stop/interrupt/receive) within its trait. The ids for each - //! method are exposed as a named const (`PREIMAGE_SUBMIT`, ...); - //! [`WIRE_TABLE`] and the generated dispatcher both reference those - //! consts so the numbers live in exactly one place. The table is - //! sorted by (trait id, request/start id). - - /// Request method wire discriminants. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct RequestFrameIds {{ - /// Trait discriminant carried by both frames. - pub trait_id: u8, - /// Method discriminant for the request frame. - pub request_id: u8, - /// Method discriminant for the response frame. - pub response_id: u8, - }} + //! Every frame carries a `(trait, method)` discriminant pair; one + //! method id addresses every frame a method ever sends or receives, + //! regardless of shape. Direction (request/response, or a + //! subscription's start/stop/interrupt/receive) and version are + //! carried inside the payload. The ids for each method are exposed as + //! a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the + //! generated dispatcher both reference those consts so the numbers + //! live in exactly one place. The table is sorted by (trait id, + //! method id). - /// Subscription method wire discriminants. + /// Wire discriminants for one method. #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct SubscriptionFrameIds {{ - /// Trait discriminant carried by all four frames. + pub struct MethodIds {{ + /// Trait discriminant carried by every frame of this method. pub trait_id: u8, - /// Method discriminant for the start frame. - pub start_id: u8, - /// Method discriminant for the stop frame. - pub stop_id: u8, - /// Method discriminant for the interrupt frame (server-initiated termination). - pub interrupt_id: u8, - /// Method discriminant for each receive frame (a streamed item). - pub receive_id: u8, + /// Method discriminant carried by every frame of this method. + pub method_id: u8, }} /// A single wire-table row. @@ -280,12 +195,13 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { pub kind: WireKind, }} - /// Wire-slot shape: request/response pair or subscription quartet. + /// Wire-slot shape: request/response or a subscription's + /// start/stop/interrupt/receive quartet. pub enum WireKind {{ /// Request/response method. - Request(RequestFrameIds), + Request(MethodIds), /// Subscription method. - Subscription(SubscriptionFrameIds), + Subscription(MethodIds), }} "# ) @@ -294,39 +210,18 @@ fn render(methods: &[(String, MethodEntry)]) -> Result { // Per-method consts: the single source of truth for each method's ids. for (name, entry) in methods { let konst = const_name(name); - let block = match entry { - MethodEntry::Request(WireEntry { - trait_id, - request_id, - response_id, - }) => formatdoc! { - r#" - /// Wire discriminants for `{name}`. - pub const {konst}: RequestFrameIds = RequestFrameIds {{ - trait_id: {trait_id}, - request_id: {request_id}, - response_id: {response_id}, - }}; - "# - }, - MethodEntry::Subscription(SubEntry { - trait_id, - start_id, - stop_id, - interrupt_id, - receive_id, - }) => formatdoc! { - r#" - /// Wire discriminants for `{name}`. - pub const {konst}: SubscriptionFrameIds = SubscriptionFrameIds {{ - trait_id: {trait_id}, - start_id: {start_id}, - stop_id: {stop_id}, - interrupt_id: {interrupt_id}, - receive_id: {receive_id}, - }}; - "# - }, + let MethodIds { + trait_id, + method_id, + } = entry.ids(); + let block = formatdoc! { + r#" + /// Wire discriminants for `{name}`. + pub const {konst}: MethodIds = MethodIds {{ + trait_id: {trait_id}, + method_id: {method_id}, + }}; + "# }; out.push('\n'); out.push_str(&block); diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index 3a232e01a..ab6453ce2 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -109,23 +109,16 @@ pub struct MethodDef { pub docs: Option, } -/// Raw wire ids extracted from `#[wire(...)]`. -#[derive(Debug, Default, Clone, PartialEq, Eq)] +/// Raw wire id extracted from `#[wire(...)]`. One id addresses the method +/// regardless of shape; direction (request/response, or a subscription's +/// start/stop/interrupt/receive) is carried inside the method's versioned +/// payload, not by a separate id. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct WireAttrs { /// This subscription is started by the host and served by the product. pub host_initiated: bool, - /// Request frame discriminant. - pub request_id: Option, - /// Response frame discriminant. - pub response_id: Option, - /// Subscription start frame discriminant. - pub start_id: Option, - /// Subscription stop frame discriminant. - pub stop_id: Option, - /// Subscription interrupt frame discriminant. - pub interrupt_id: Option, - /// Subscription item frame discriminant. - pub receive_id: Option, + /// Method frame discriminant. + pub id: Option, } /// Wire-shape classification of a trait method. @@ -444,6 +437,7 @@ fn should_skip_type_name(name: &str) -> bool { matches!( name, "Subscription" + | "Request" | "CallContext" | "CallError" | "CancellationFuture" @@ -853,9 +847,10 @@ fn extract_wire_trait_id(trait_name: &str, docs: &str) -> Result> { Ok(found) } -/// Extracts `@wire__id=N` markers from a doc comment block. Annotated -/// methods carry these markers via the `#[wire(...)]` proc-macro, which appends -/// hidden doc strings so they propagate through rustdoc JSON. +/// Extracts the `@wire_id=N` marker (and `@wire_host_initiated`) from a doc +/// comment block. Annotated methods carry these markers via the `#[wire(...)]` +/// proc-macro, which appends hidden doc strings so they propagate through +/// rustdoc JSON. fn extract_wire_attrs(docs: &str) -> WireAttrs { let mut attrs = WireAttrs::default(); for line in docs.lines() { @@ -863,23 +858,15 @@ fn extract_wire_attrs(docs: &str) -> WireAttrs { if line.starts_with("@wire_host_initiated") { attrs.host_initiated = true; } - for (needle, target) in [ - ("@wire_request_id=", &mut attrs.request_id), - ("@wire_response_id=", &mut attrs.response_id), - ("@wire_start_id=", &mut attrs.start_id), - ("@wire_stop_id=", &mut attrs.stop_id), - ("@wire_interrupt_id=", &mut attrs.interrupt_id), - ("@wire_receive_id=", &mut attrs.receive_id), - ] { - let Some(start) = line.find(needle).map(|index| index + needle.len()) else { - continue; - }; - let end = line[start..] - .find(|c: char| !c.is_ascii_digit()) - .map_or(line.len(), |offset| start + offset); - if let Ok(id) = line[start..end].parse::() { - *target = Some(id); - } + const NEEDLE: &str = "@wire_id="; + let Some(start) = line.find(NEEDLE).map(|index| index + NEEDLE.len()) else { + continue; + }; + let end = line[start..] + .find(|c: char| !c.is_ascii_digit()) + .map_or(line.len(), |offset| start + offset); + if let Ok(id) = line[start..end].parse::() { + attrs.id = Some(id); } } attrs diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index 9618e17a3..24bf5ca0a 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -386,6 +386,47 @@ fn versioned_wrapper_for<'a>( None } +/// Name of a method's merged wire-envelope type (RFC 0028), derived the same +/// way `truapi-codegen`'s Rust dispatcher emitter derives it: strip the +/// `Request`/`Item` suffix off the request (or subscription item) wrapper's +/// own name and append `Version`. Mirrors `envelope_type_name` in +/// `rust/dispatcher.rs` so both languages name the same Rust type the same +/// way. +fn envelope_type_name(request: Option<&str>, item: Option<&str>) -> Option { + if let Some(base) = request.and_then(|name| name.strip_suffix("Request")) { + return Some(format!("{base}Version")); + } + if let Some(base) = item.and_then(|name| name.strip_suffix("Item")) { + return Some(format!("{base}Version")); + } + None +} + +/// Look up an extracted type definition by name. +fn find_type<'a>(api: &'a ApiDefinition, name: &str) -> Option<&'a TypeDef> { + api.types.iter().find(|type_def| type_def.name == name) +} + +/// Resolve a method's merged wire-envelope type name, if one was extracted +/// for it. `None` when the method's request/item wrapper doesn't follow the +/// `{Base}Request`/`{Base}Item` naming convention (synthetic test fixtures +/// only; every real method resolves), or when the derived name isn't +/// actually present in the extracted API (an authoring bug: the wrapper +/// exists but its merged envelope type doesn't). +fn method_envelope_name( + api: &ApiDefinition, + request_wrapper: Option<&str>, + item_wrapper: Option<&str>, +) -> Result> { + let Some(name) = envelope_type_name(request_wrapper, item_wrapper) else { + return Ok(None); + }; + if find_type(api, &name).is_none() { + return Ok(None); + } + Ok(Some(name)) +} + /// Emits a JSDoc block for `docs` at the given indent. No-op when `docs` is /// `None` so callers can pipe rust doc strings through unconditionally. /// @@ -497,52 +538,6 @@ fn generate_index() -> String { "export * from './types.js';\nexport * from './client.js';\n".to_string() } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ExpandedWireIds { - Request { - request_id: u8, - response_id: u8, - }, - Subscription { - start_id: u8, - stop_id: u8, - interrupt_id: u8, - receive_id: u8, - }, -} - -impl ExpandedWireIds { - fn sort_id(self) -> u8 { - match self { - ExpandedWireIds::Request { request_id, .. } => request_id, - ExpandedWireIds::Subscription { start_id, .. } => start_id, - } - } - - fn entries(self, method_name: &str) -> Vec<(u8, String)> { - match self { - ExpandedWireIds::Request { - request_id, - response_id, - } => vec![ - (request_id, format!("{method_name}_request")), - (response_id, format!("{method_name}_response")), - ], - ExpandedWireIds::Subscription { - start_id, - stop_id, - interrupt_id, - receive_id, - } => vec![ - (start_id, format!("{method_name}_start")), - (stop_id, format!("{method_name}_stop")), - (interrupt_id, format!("{method_name}_interrupt")), - (receive_id, format!("{method_name}_receive")), - ], - } - } -} - fn trim_doc_lines(lines: &[&str]) -> Option { let mut start = 0; let mut end = lines.len(); @@ -575,11 +570,7 @@ fn wire_const_name(trait_name: &str, method_name: &str) -> String { /// Sort key for stable, wire-id-ordered method emission shared by the /// playground and examples submodules. fn method_wire_sort_id(method: &MethodDef) -> u8 { - method - .wire - .request_id - .or(method.wire.start_id) - .unwrap_or(u8::MAX) + method.wire.id.unwrap_or(u8::MAX) } fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result { @@ -592,7 +583,7 @@ fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result = Vec::new(); + let mut constants: Vec<(String, u8, u8)> = Vec::new(); for trait_def in &api.traits { // Method-less traits (e.g. the `TrUApi` umbrella trait) own no wire @@ -608,11 +599,12 @@ fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result Result Result { - out.push('\n'); - out.push_str(&formatdoc! {" - export const {name} = {{ - trait: {trait_id}, - request: {request_id}, - response: {response_id}, - }} as const satisfies RequestFrameIds; - "}); - } - ExpandedWireIds::Subscription { - start_id, - stop_id, - interrupt_id, - receive_id, - } => { - out.push('\n'); - out.push_str(&formatdoc! {" - export const {name} = {{ - trait: {trait_id}, - start: {start_id}, - stop: {stop_id}, - interrupt: {interrupt_id}, - receive: {receive_id}, - }} as const satisfies SubscriptionFrameIds; - "}); - } - } + for (name, trait_id, method_id) in constants { + out.push('\n'); + out.push_str(&formatdoc! {" + export const {name} = {{ + trait: {trait_id}, + method: {method_id}, + }} as const satisfies MethodIds; + "}); } Ok(out) @@ -710,7 +679,7 @@ fn method_is_included( wrappers: &HashMap, target_version: u32, ) -> Result { - wire_ids_for_method(trait_def, method)?; + wire_id_for_method(trait_def, method)?; let wrapper_names = method_versioned_wrappers(method, wrappers); Ok( @@ -719,82 +688,14 @@ fn method_is_included( ) } -fn wire_ids_for_method(trait_def: &TraitDef, method: &MethodDef) -> Result { - let wire = &method.wire; - match method.kind { - MethodKind::Request => { - if wire.start_id.is_some() - || wire.stop_id.is_some() - || wire.interrupt_id.is_some() - || wire.receive_id.is_some() - { - bail!( - "method `{}::{}` is a request and must not use subscription wire ids", - trait_def.name, - method.name - ); - } - let request_id = wire.request_id.ok_or_else(|| { - anyhow::anyhow!( - "method `{}::{}` is missing #[wire(request_id = N)] annotation", - trait_def.name, - method.name - ) - })?; - let response_id = - infer_wire_id(wire.response_id, request_id, 1, &method.name, "response_id")?; - Ok(ExpandedWireIds::Request { - request_id, - response_id, - }) - } - MethodKind::Subscription | MethodKind::ResultSubscription => { - if wire.request_id.is_some() || wire.response_id.is_some() { - bail!( - "method `{}::{}` is a subscription and must not use request wire ids", - trait_def.name, - method.name - ); - } - let start_id = wire.start_id.ok_or_else(|| { - anyhow::anyhow!( - "method `{}::{}` is missing #[wire(start_id = N)] annotation", - trait_def.name, - method.name - ) - })?; - let stop_id = infer_wire_id(wire.stop_id, start_id, 1, &method.name, "stop_id")?; - let interrupt_id = - infer_wire_id(wire.interrupt_id, start_id, 2, &method.name, "interrupt_id")?; - let receive_id = - infer_wire_id(wire.receive_id, start_id, 3, &method.name, "receive_id")?; - Ok(ExpandedWireIds::Subscription { - start_id, - stop_id, - interrupt_id, - receive_id, - }) - } - } -} - -fn infer_wire_id( - explicit: Option, - anchor_id: u8, - offset: u8, - method_name: &str, - field_name: &str, -) -> Result { - explicit.map_or_else( - || { - anchor_id.checked_add(offset).ok_or_else(|| { - anyhow::anyhow!( - "wire id overflow on `{method_name}` while inferring `{field_name}` from {anchor_id}" - ) - }) - }, - Ok, - ) +fn wire_id_for_method(trait_def: &TraitDef, method: &MethodDef) -> Result { + method.wire.id.ok_or_else(|| { + anyhow::anyhow!( + "method `{}::{}` is missing #[wire(id = N)] annotation", + trait_def.name, + method.name + ) + }) } /// Picks the wrapper variant the generated client emits on the wire for a @@ -942,6 +843,7 @@ fn generate_types(api: &ApiDefinition, target_version: u32) -> Result { // Auto-generated by truapi-codegen. Do not edit. import * as S from '../scale.js'; + import {{ Request, Result, Subscription }} from '../scale.js'; import type {{ HexString }} from '../scale.js'; "# @@ -988,7 +890,7 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) import * as S from '../scale.js'; import type {{ HexString }} from '../scale.js'; import {{ SubscriptionError }} from '../transport.js'; - import type {{ HostInitiatedSubscriptionRegistration, ObservableLike, ObservableSource, Observer, Subscription, SubscriptionFrameIds, TrUApiTransport }} from '../transport.js'; + import type {{ HostInitiatedSubscriptionRegistration, MethodIds, ObservableLike, ObservableSource, Observer, Subscription, TrUApiTransport }} from '../transport.js'; import * as T from './types.js'; import * as W from './wire-table.js'; @@ -1003,9 +905,12 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) return new SubscriptionError(cause.message, {{ cause }}); }} - // `_interrupt` payload sent when a host-initiated request arrives with no - // registered handler: SCALE `Result::Err` discriminant, declining the start. - const HOST_INITIATED_DECLINE_PAYLOAD = new Uint8Array([0]); + // Interrupt payload sent when a host-initiated render arrives with no + // registered handler, declining the start: `[version=V1, direction= + // Interrupt, Option::None]`. The host only inspects the direction + // byte for this flow, so this fixed frame (matching a clean, + // error-free interrupt) is valid for every method. + const HOST_INITIATED_DECLINE_PAYLOAD = new Uint8Array([0, 2, 0]); // Items buffered per host-initiated stream while the product's handler // observable has no subscriber yet. const HOST_INITIATED_BUFFER_CAPACITY = 64; @@ -1048,6 +953,7 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) { emit_host_initiated_registration( &mut out, + api, trait_def, method, &wrappers, @@ -1058,7 +964,15 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) writeln!(out, " }}\n").unwrap(); for method in methods { - emit_method(&mut out, trait_def, method, &wrappers, &ctx, target_version)?; + emit_method( + &mut out, + api, + trait_def, + method, + &wrappers, + &ctx, + target_version, + )?; writeln!(out).unwrap(); } @@ -1148,10 +1062,14 @@ fn write_observable_helper(out: &mut String) { onSubscribe, }}: {{ transport: TrUApiTransport; - ids: SubscriptionFrameIds; + ids: MethodIds; payload: Uint8Array; decodeItem: (payload: Uint8Array) => Item; - decodeInterrupt?: (payload: Uint8Array) => Reason; + // `undefined` signals a clean, error-free completion (the wire + // envelope's `Interrupt(None)`), distinct from not being able to + // observe a typed reason at all (this method has no domain error, + // and `decodeInterrupt` itself is omitted). + decodeInterrupt?: (payload: Uint8Array) => Reason | undefined; onSubscribe?: (subscription: Subscription) => {{ unsubscribe(): void }}; }}): ObservableLike {{ const observable: ObservableLike = {{ @@ -1198,6 +1116,12 @@ fn write_observable_helper(out: &mut String) { fail(error, false); return; }} + if (reason === undefined) {{ + closed = true; + stopForwarding(); + observer.complete?.(); + return; + }} fail(new SubscriptionError("Subscription interrupted", {{ reason }}), false); return; }} @@ -1370,6 +1294,13 @@ struct ResponseEmission { wire_type_ts: String, wire_codec_expr: String, inner_codec_expr: String, + /// `Some(version)` when `inner_type_ts` is a domain error's own versioned + /// wrapper (`S.CallErrorValue`) rather than its bare + /// type. The merged wire envelope (RFC 0028) always carries the bare + /// domain error under one shared version tag, so a caller decoding + /// through it must re-wrap a `Domain` outcome in `{ tag: "V{version}", + /// value }` to keep presenting this same public shape. + wrap_version: Option, } fn versioned_value_cast(wire_type: &str, inner_type: &str, version: u32) -> String { @@ -1408,12 +1339,14 @@ fn emit_response( wire_type_ts: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), wire_codec_expr: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), inner_codec_expr: "S._void".to_string(), + wrap_version: None, }), VersionedKind::Tuple(inner) => Ok(ResponseEmission { inner_type_ts: ts_type_qualified(inner)?, wire_type_ts: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), wire_codec_expr: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), inner_codec_expr: codec_expr(inner, true, ctx)?, + wrap_version: None, }), }; } @@ -1423,6 +1356,7 @@ fn emit_response( wire_type_ts: ts_type_qualified(ty)?, wire_codec_expr: codec_expr(ty, true, ctx)?, inner_codec_expr: codec_expr(ty, true, ctx)?, + wrap_version: None, }) } @@ -1449,6 +1383,7 @@ fn emit_error_response( wire_type_ts: format!("{{ tag: \"V{version}\"; value: {inner_type_ts} }}"), wire_codec_expr, inner_codec_expr, + wrap_version: Some(version), }); } @@ -1465,6 +1400,7 @@ fn emit_error_response( wire_type_ts: inner_type_ts, wire_codec_expr: inner_codec_expr.clone(), inner_codec_expr, + wrap_version: None, }) } @@ -1503,8 +1439,22 @@ fn versioned_result_codec_expr(version: u32, ok_codec: &str, err_codec: &str) -> indexed_versioned_codec_expr([(version, format!("S.Result({ok_codec}, {err_codec})"))]) } +/// The request wrapper name for a method's single param, if its param shape +/// is a recognized versioned wrapper (payloadless and multi/raw-param +/// methods have no such wrapper to name). +fn request_wrapper_name<'a>( + method: &'a MethodDef, + wrappers: &'a HashMap, +) -> Option<&'a str> { + if method.params.len() != 1 { + return None; + } + versioned_wrapper_for(&method.params[0].type_ref, wrappers).map(|(name, _)| name) +} + fn emit_method( out: &mut String, + api: &ApiDefinition, trait_def: &TraitDef, method: &MethodDef, wrappers: &HashMap, @@ -1526,17 +1476,7 @@ fn emit_method( let is_handshake = trait_def.name == "System" && method.name == "handshake"; let response = emit_response(ok, wrappers, ctx, wire_version)?; let error = emit_error_response(err, wrappers, ctx, wire_version)?; - let response_codec = match wire_version { - Some(version) => versioned_result_codec_expr( - version, - &response.inner_codec_expr, - &error.inner_codec_expr, - )?, - None => format!( - "S.Result({}, {})", - response.wire_codec_expr, error.wire_codec_expr - ), - }; + let envelope = method_envelope_name(api, request_wrapper_name(method, wrappers), None)?; let arg_decl = if is_handshake || payload.param_list.is_empty() { String::new() @@ -1544,9 +1484,9 @@ fn emit_method( format!("request: {}", payload.inner_type_ts) }; let request_expr = if is_handshake { - "{ codecVersion: this.transport.codecVersion }" + "{ codecVersion: this.transport.codecVersion }".to_string() } else { - &payload.value_expr + payload.value_expr.clone() }; writedoc!( @@ -1560,19 +1500,77 @@ fn emit_method( err_type = error.inner_type_ts ) .unwrap(); - write_payload_field( - out, - " ", - &payload.wire_codec_expr, - payload.wire_version, - request_expr, - ); - let value_suffix = if wire_version.is_some() { ".value" } else { "" }; - writeln!( - out, - " decodeResponse: (payload) => {response_codec}.dec(payload){value_suffix}," - ) - .unwrap(); + + match envelope { + Some(envelope_name) => { + let version = wire_version.ok_or_else(|| { + anyhow::anyhow!( + "method `{}` resolved wire envelope `{envelope_name}` with no selected wire version", + method.name + ) + })?; + let method_name = &method.name; + writedoc!( + out, + " + payload: T.{envelope_name}.enc({{ tag: \"V{version}\", value: {{ tag: \"Request\", value: {request_expr} }} }}), + decodeResponse: (payload) => {{ + const envelope = T.{envelope_name}.dec(payload); + if (envelope.value.tag !== \"Response\") {{ + throw new Error(`{method_name}: expected Response direction, got ${{envelope.value.tag}}`); + }} + const result = envelope.value.value; + " + ) + .unwrap(); + match error.wrap_version { + Some(err_version) => writedoc!( + out, + " + if (!result.success) {{ + return {{ + success: false, + value: result.value.tag === \"Domain\" + ? {{ tag: \"Domain\" as const, value: {{ tag: \"V{err_version}\", value: result.value.value }} }} + : result.value, + }}; + }} + return result; + " + ) + .unwrap(), + None => writeln!(out, " return result;").unwrap(), + } + writeln!(out, " }},").unwrap(); + } + None => { + let response_codec = match wire_version { + Some(version) => versioned_result_codec_expr( + version, + &response.inner_codec_expr, + &error.inner_codec_expr, + )?, + None => format!( + "S.Result({}, {})", + response.wire_codec_expr, error.wire_codec_expr + ), + }; + write_payload_field( + out, + " ", + &payload.wire_codec_expr, + payload.wire_version, + &request_expr, + ); + let value_suffix = if wire_version.is_some() { ".value" } else { "" }; + writeln!( + out, + " decodeResponse: (payload) => {response_codec}.dec(payload){value_suffix}," + ) + .unwrap(); + } + } + writedoc!( out, " @@ -1584,6 +1582,11 @@ fn emit_method( } (MethodKind::Subscription, ReturnType::Subscription(ty)) => { let response = emit_response(ty, wrappers, ctx, wire_version)?; + let envelope = method_envelope_name( + api, + request_wrapper_name(method, wrappers), + versioned_wrapper_for(ty, wrappers).map(|(name, _)| name), + )?; emit_subscribe_method( out, &ts_method_name, @@ -1593,11 +1596,17 @@ fn emit_method( response.inner_type_ts.clone(), None, wire_version, + envelope, )?; } (MethodKind::ResultSubscription, ReturnType::ResultSubscription { item, err }) => { let response = emit_response(item, wrappers, ctx, wire_version)?; let error = emit_error_response(err, wrappers, ctx, wire_version)?; + let envelope = method_envelope_name( + api, + request_wrapper_name(method, wrappers), + versioned_wrapper_for(item, wrappers).map(|(name, _)| name), + )?; emit_subscribe_method( out, &ts_method_name, @@ -1607,6 +1616,7 @@ fn emit_method( response.inner_type_ts.clone(), Some(error), wire_version, + envelope, )?; } (kind, return_type) => { @@ -1667,6 +1677,7 @@ fn emit_host_initiated_field( fn emit_host_initiated_registration( out: &mut String, + api: &ApiDefinition, trait_def: &TraitDef, method: &MethodDef, wrappers: &HashMap, @@ -1676,22 +1687,61 @@ fn emit_host_initiated_registration( let (payload, response, version) = emit_host_initiated_types(method, wrappers, ctx, target_version)?; let wire_const = wire_const_name(&trait_def.name, &method.name); - writedoc!( - out, - " - this.{field} = transport.registerHostInitiatedSubscription({{ - ids: W.{wire_const}, - decodeRequest: (payload) => {request_codec}.dec(payload).value, - encodeItem: (item) => {item_codec}.enc({{ tag: \"V{version}\", value: item }}), - interruptPayload: HOST_INITIATED_DECLINE_PAYLOAD, - bufferCapacity: HOST_INITIATED_BUFFER_CAPACITY, - }}); - ", - field = host_registration_field(method), - request_codec = payload.wire_codec_expr, - item_codec = response.wire_codec_expr, - ) - .unwrap(); + let ReturnType::Subscription(item_ty) = &method.return_type else { + bail!( + "host-initiated method `{}` must return Subscription", + method.name + ); + }; + let envelope = method_envelope_name( + api, + request_wrapper_name(method, wrappers), + versioned_wrapper_for(item_ty, wrappers).map(|(name, _)| name), + )?; + + match envelope { + Some(envelope_name) => { + let method_name = &method.name; + writedoc!( + out, + " + this.{field} = transport.registerHostInitiatedSubscription({{ + ids: W.{wire_const}, + decodeRequest: (payload) => {{ + const envelope = T.{envelope_name}.dec(payload); + if (envelope.value.tag !== \"Start\") {{ + throw new Error(`{method_name}: expected Start direction, got ${{envelope.value.tag}}`); + }} + return envelope.value.value; + }}, + encodeItem: (item) => T.{envelope_name}.enc({{ tag: \"V{version}\", value: {{ tag: \"Receive\", value: item }} }}), + interruptPayload: HOST_INITIATED_DECLINE_PAYLOAD, + bufferCapacity: HOST_INITIATED_BUFFER_CAPACITY, + }}); + ", + field = host_registration_field(method), + ) + .unwrap(); + } + None => { + writedoc!( + out, + " + this.{field} = transport.registerHostInitiatedSubscription({{ + ids: W.{wire_const}, + decodeRequest: (payload) => {request_codec}.dec(payload).value, + encodeItem: (item) => {item_codec}.enc({{ tag: \"V{version}\", value: item }}), + interruptPayload: HOST_INITIATED_DECLINE_PAYLOAD, + bufferCapacity: HOST_INITIATED_BUFFER_CAPACITY, + }}); + ", + field = host_registration_field(method), + request_codec = payload.wire_codec_expr, + item_codec = response.wire_codec_expr, + ) + .unwrap(); + } + } Ok(()) } @@ -1741,6 +1791,7 @@ fn emit_subscribe_method( item_type_ts: String, err: Option, wire_version: Option, + envelope: Option, ) -> Result<()> { let observable_args = match err.as_ref() { Some(err) => format!("{item_type_ts}, {}", err.inner_type_ts), @@ -1768,37 +1819,93 @@ fn emit_subscribe_method( " ) .unwrap(); - write_payload_field( - out, - " ", - &payload.wire_codec_expr, - payload.wire_version, - &payload.value_expr, - ); - let item_value = if let Some(version) = wire_version { - versioned_value_expr( - &format!("{}.dec(payload)", response.wire_codec_expr), - &response.wire_type_ts, - &item_type_ts, - version, - ) - } else { - format!("{}.dec(payload)", response.wire_codec_expr) - }; - writeln!(out, " decodeItem: (payload) => {item_value},").unwrap(); - if let Some(err) = err { - let err_value = if let Some(version) = wire_version { - versioned_value_expr( - &format!("{}.dec(payload)", err.wire_codec_expr), - &err.wire_type_ts, - &err.inner_type_ts, - version, + + match envelope { + Some(envelope_name) => { + let version = wire_version.ok_or_else(|| { + anyhow::anyhow!( + "method `{ts_method_name}` resolved wire envelope `{envelope_name}` with no selected wire version" + ) + })?; + writedoc!( + out, + " + payload: T.{envelope_name}.enc({{ tag: \"V{version}\", value: {{ tag: \"Start\", value: {value_expr} }} }}), + decodeItem: (payload) => {{ + const envelope = T.{envelope_name}.dec(payload); + if (envelope.value.tag !== \"Receive\") {{ + throw new Error(`{ts_method_name}: expected Receive direction, got ${{envelope.value.tag}}`); + }} + return envelope.value.value; + }}, + ", + value_expr = payload.value_expr, ) - } else { - format!("{}.dec(payload)", err.wire_codec_expr) - }; - writeln!(out, " decodeInterrupt: (payload) => {err_value},").unwrap(); + .unwrap(); + if let Some(err) = &err { + writedoc!( + out, + " + decodeInterrupt: (payload) => {{ + const envelope = T.{envelope_name}.dec(payload); + if (envelope.value.tag !== \"Interrupt\") {{ + throw new Error(`{ts_method_name}: expected Interrupt direction, got ${{envelope.value.tag}}`); + }} + const reason = envelope.value.value; + if (reason === undefined) return undefined; + " + ) + .unwrap(); + match err.wrap_version { + Some(err_version) => writedoc!( + out, + " + return reason.tag === \"Domain\" + ? {{ tag: \"Domain\" as const, value: {{ tag: \"V{err_version}\", value: reason.value }} }} + : reason; + }}, + " + ) + .unwrap(), + None => writeln!(out, " return reason;\n }},").unwrap(), + } + } + } + None => { + write_payload_field( + out, + " ", + &payload.wire_codec_expr, + payload.wire_version, + &payload.value_expr, + ); + let item_value = if let Some(version) = wire_version { + versioned_value_expr( + &format!("{}.dec(payload)", response.wire_codec_expr), + &response.wire_type_ts, + &item_type_ts, + version, + ) + } else { + format!("{}.dec(payload)", response.wire_codec_expr) + }; + writeln!(out, " decodeItem: (payload) => {item_value},").unwrap(); + if let Some(err) = err { + let err_value = if let Some(version) = wire_version { + versioned_value_expr( + &format!("{}.dec(payload)", err.wire_codec_expr), + &err.wire_type_ts, + &err.inner_type_ts, + version, + ) + } else { + format!("{}.dec(payload)", err.wire_codec_expr) + }; + writeln!(out, " decodeInterrupt: (payload) => {err_value},").unwrap(); + } + } } + writedoc!( out, " @@ -2566,16 +2673,9 @@ fn to_camel_case(s: &str) -> String { mod tests { use super::*; - fn request_wire(request_id: Option) -> WireAttrs { + fn wire_attrs(id: Option) -> WireAttrs { WireAttrs { - request_id, - ..WireAttrs::default() - } - } - - fn subscription_wire(start_id: Option) -> WireAttrs { - WireAttrs { - start_id, + id, ..WireAttrs::default() } } @@ -2610,7 +2710,7 @@ mod tests { ok: TypeRef::Unit, err: TypeRef::Unit, }, - wire: request_wire(wire_id), + wire: wire_attrs(wire_id), docs: None, } } @@ -2621,7 +2721,7 @@ mod tests { kind: MethodKind::Subscription, params: Vec::new(), return_type: ReturnType::Subscription(TypeRef::Unit), - wire: subscription_wire(wire_id), + wire: wire_attrs(wire_id), docs: None, } } @@ -2665,7 +2765,7 @@ mod tests { ok: named_type(response), err: named_type(error), }, - wire: request_wire(wire_id), + wire: wire_attrs(wire_id), docs: None, } } @@ -2676,7 +2776,7 @@ mod tests { kind: MethodKind::Subscription, params: Vec::new(), return_type: ReturnType::Subscription(named_type(item)), - wire: subscription_wire(wire_id), + wire: wire_attrs(wire_id), docs: None, } } @@ -2861,10 +2961,9 @@ mod tests { assert!(source.contains("export const EXAMPLE_STREAM = {")); assert!(source.contains(" trait: 200,")); - assert!(source.contains(" start: 2,")); - assert!(source.contains(" receive: 5,")); + assert!(source.contains(" method: 2,")); assert!(source.contains("export const EXAMPLE_LATER = {")); - assert!(source.contains(" request: 10,")); + assert!(source.contains(" method: 10,")); assert!( source .find("export const EXAMPLE_STREAM") @@ -2880,13 +2979,13 @@ mod tests { let err = generate_wire_table( &api(vec![ request_method("first", Some(2)), - subscription_method("second", Some(3)), + subscription_method("second", Some(2)), ]), 2, ) .expect_err("duplicate ids must error"); - assert!(err.to_string().contains("wire id (200, 3) reused")); + assert!(err.to_string().contains("wire id (200, 2) reused")); } /// Trait 255 is reserved for protocol errors, so no API trait may declare @@ -2913,8 +3012,7 @@ mod tests { /// would quietly cost every trait its last method slot. #[test] fn generate_wire_table_allows_method_id_255_outside_the_reserved_trait() { - let mut method = request_method("explicit_request", Some(255)); - method.wire.response_id = Some(1); + let method = request_method("explicit_request", Some(255)); generate_wire_table(&api(vec![method]), 2).expect("(200, 255) is an ordinary address"); } @@ -2925,14 +3023,13 @@ mod tests { /// considered, which is what makes that hold. #[test] fn generate_wire_table_rejects_the_reserved_trait_id_for_filtered_methods() { - let mut future = request_method_with_wrappers( + let future = request_method_with_wrappers( "future", Some(0), "FutureRequest", "FutureResponse", "FutureError", ); - future.wire.response_id = Some(1); let api = ApiDefinition { traits: vec![TraitDef { name: "Example".to_string(), @@ -2963,54 +3060,20 @@ mod tests { let err = generate_wire_table(&api(vec![request_method("missing", None)]), 2) .expect_err("missing wire id must error"); - assert!(err.to_string().contains("missing #[wire(request_id = N)]")); + assert!(err.to_string().contains("missing #[wire(id = N)]")); } #[test] - fn generate_wire_table_uses_explicit_overrides() { - let mut request = request_method("custom_request", Some(2)); - request.wire.response_id = Some(9); - let mut subscription = subscription_method("custom_stream", Some(20)); - subscription.wire.stop_id = Some(30); - subscription.wire.interrupt_id = Some(31); - subscription.wire.receive_id = Some(32); + fn generate_wire_table_emits_one_id_per_method_regardless_of_kind() { + let request = request_method("custom_request", Some(2)); + let subscription = subscription_method("custom_stream", Some(20)); let source = generate_wire_table(&api(vec![request, subscription]), 2).expect("wire table"); assert!(source.contains("export const EXAMPLE_CUSTOM_REQUEST = {")); - assert!(source.contains(" request: 2,")); - assert!(source.contains(" response: 9,")); + assert!(source.contains(" method: 2,")); assert!(source.contains("export const EXAMPLE_CUSTOM_STREAM = {")); - assert!(source.contains(" start: 20,")); - assert!(source.contains(" stop: 30,")); - assert!(source.contains(" interrupt: 31,")); - assert!(source.contains(" receive: 32,")); - } - - #[test] - fn generate_wire_table_rejects_invalid_attrs_by_method_kind() { - let mut request = request_method("bad_request", Some(2)); - request.wire.start_id = Some(4); - let err = generate_wire_table(&api(vec![request]), 2) - .expect_err("request with start id must error"); - assert!( - err.to_string() - .contains("must not use subscription wire ids") - ); - - let mut subscription = subscription_method("bad_stream", Some(10)); - subscription.wire.request_id = Some(12); - let err = generate_wire_table(&api(vec![subscription]), 2) - .expect_err("subscription with request id must error"); - assert!(err.to_string().contains("must not use request wire ids")); - } - - #[test] - fn generate_wire_table_rejects_inferred_overflow() { - let err = generate_wire_table(&api(vec![subscription_method("overflow", Some(253))]), 2) - .expect_err("overflow must error"); - - assert!(err.to_string().contains("wire id overflow")); + assert!(source.contains(" method: 20,")); } /// Method ids are scoped per trait: two traits may both use method id 0. @@ -3133,7 +3196,7 @@ mod tests { let source = generate_wire_table(&api, 1).expect("generate wire table"); assert!(source.contains("export const EXAMPLE_LEGACY = {")); - assert!(source.contains(" request: 2,")); + assert!(source.contains(" method: 2,")); assert!(!source.contains("FUTURE")); assert!(!source.contains("FUTURE_STREAM")); } @@ -3214,7 +3277,7 @@ mod tests { }, err: TypeRef::Unit, }, - wire: request_wire(Some(2)), + wire: wire_attrs(Some(2)), docs: None, }], docs: None, @@ -3259,7 +3322,7 @@ mod tests { ok: TypeRef::Unit, err: named_type("ExampleError"), }, - wire: request_wire(Some(2)), + wire: wire_attrs(Some(2)), docs: None, }, MethodDef { @@ -3273,7 +3336,7 @@ mod tests { ok: TypeRef::Unit, err: named_type("ExampleError"), }, - wire: request_wire(Some(4)), + wire: wire_attrs(Some(4)), docs: None, }, ], @@ -3332,7 +3395,7 @@ mod tests { }, err: TypeRef::Unit, }, - wire: request_wire(Some(2)), + wire: wire_attrs(Some(2)), docs: None, }], docs: None, @@ -3379,7 +3442,7 @@ mod tests { }, err: TypeRef::Unit, }, - wire: request_wire(Some(2)), + wire: wire_attrs(Some(2)), docs: None, }], docs: None, diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs index 7b059b295..9c93a1d91 100644 --- a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -9,7 +9,7 @@ use std::sync::Arc; -use parity_scale_codec::Decode; +use parity_scale_codec::{Decode, Encode}; use truapi::CallContext; use truapi::api::{ @@ -34,11 +34,7 @@ use truapi::versioned::{self, Versioned}; use truapi_platform::ProductExecutionKind; use crate::dispatcher::Dispatcher; -use crate::frame::encode_versioned_err_payload; -use crate::frame::encode_versioned_interrupt_payload; use crate::frame::downgrade_call_error; -use crate::frame::encode_versioned_ok_payload; -use crate::frame::encode_versioned_unit_ok_payload; use crate::generated::wire_table; use crate::subscription::{HostInitiatedSubscriptionManager, subscription_stream}; use crate::transport::Transport; @@ -74,9 +70,13 @@ pub(crate) fn chat_custom_message_render( ) -> truapi::Subscription< Result, > { + let envelope = match request { + versioned::chat::ProductChatCustomMessageRenderRequest::V1(bare) => versioned::chat::ProductChatCustomMessageRenderVersion::V1(truapi::versioned::Subscription::Start(bare)), + }; subscriptions.start( wire_table::CHAT_CUSTOM_MESSAGE_RENDER, - parity_scale_codec::Encode::encode(&request), + 1, + parity_scale_codec::Encode::encode(&envelope), transport, ) } @@ -90,10 +90,32 @@ where dispatcher.on_subscription(wire_table::ACCOUNT_CONNECTION_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::account::HostAccountConnectionStatusSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::account::HostAccountConnectionStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let _request: () = match envelope { + versioned::account::HostAccountConnectionStatusSubscribeVersion::V1(truapi::versioned::Subscription::Start(_bare)) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::account::HostAccountConnectionStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.connection_status_subscribe(&cx).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::account::HostAccountConnectionStatusSubscribeItem| match item { + versioned::account::HostAccountConnectionStatusSubscribeItem::V1(bare) => versioned::account::HostAccountConnectionStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -102,15 +124,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountGetRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountGetVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountGetRequest = match envelope { + versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountGetRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -118,21 +147,27 @@ where let response: versioned::account::HostAccountGetResponse = match host.get_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountGetError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountGetResponse::V1(bare) => versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -141,15 +176,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT_ALIAS, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountGetAliasRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountGetAliasVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountGetAliasRequest = match envelope { + versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountGetAliasRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -157,21 +199,27 @@ where let response: versioned::account::HostAccountGetAliasResponse = match host.get_account_alias(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountGetAliasError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountGetAliasResponse::V1(bare) => versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -180,15 +228,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_CREATE_ACCOUNT_PROOF, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountCreateProofRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountCreateProofVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountCreateProofRequest = match envelope { + versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountCreateProofRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -196,21 +251,27 @@ where let response: versioned::account::HostAccountCreateProofResponse = match host.create_account_proof(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountCreateProofError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountCreateProofResponse::V1(bare) => versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -219,15 +280,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_SIGN_VRF, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountSignVrfRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountSignVrfVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountSignVrfRequest = match envelope { + versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountSignVrfRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -235,21 +303,27 @@ where let response: versioned::account::HostAccountSignVrfResponse = match host.sign_vrf(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountSignVrfError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountSignVrfResponse::V1(bare) => versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -258,15 +332,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_REGISTER_RING_VRF_KEY, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountRegisterRingVrfKeyRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountRegisterRingVrfKeyVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountRegisterRingVrfKeyRequest = match envelope { + versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountRegisterRingVrfKeyRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -274,21 +355,27 @@ where let response: versioned::account::HostAccountRegisterRingVrfKeyResponse = match host.register_ring_vrf_key(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountRegisterRingVrfKeyError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountRegisterRingVrfKeyResponse::V1(bare) => versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -297,15 +384,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_LIST_RING_VRF_KEYS, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountListRingVrfKeysRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountListRingVrfKeysVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountListRingVrfKeysRequest = match envelope { + versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountListRingVrfKeysRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -313,21 +407,27 @@ where let response: versioned::account::HostAccountListRingVrfKeysResponse = match host.list_ring_vrf_keys(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountListRingVrfKeysError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountListRingVrfKeysResponse::V1(bare) => versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -336,15 +436,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_RING_VRF_SIGN, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountRingVrfSignRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountRingVrfSignVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountRingVrfSignRequest = match envelope { + versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountRingVrfSignRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -352,21 +459,27 @@ where let response: versioned::account::HostAccountRingVrfSignResponse = match host.ring_vrf_sign(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountRingVrfSignError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountRingVrfSignResponse::V1(bare) => versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -375,15 +488,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_LEGACY_ACCOUNTS, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostGetLegacyAccountsRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostGetLegacyAccountsVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostGetLegacyAccountsRequest = match envelope { + versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::account::HostGetLegacyAccountsRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -391,21 +511,27 @@ where let response: versioned::account::HostGetLegacyAccountsResponse = match host.get_legacy_accounts(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostGetLegacyAccountsError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostGetLegacyAccountsResponse::V1(bare) => versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -414,15 +540,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_USER_ID, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostGetUserIdRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostGetUserIdVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostGetUserIdRequest = match envelope { + versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::account::HostGetUserIdRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -430,21 +563,27 @@ where let response: versioned::account::HostGetUserIdResponse = match host.get_user_id(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostGetUserIdError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostGetUserIdResponse::V1(bare) => versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -453,15 +592,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_REQUEST_LOGIN, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostRequestLoginRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostRequestLoginVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostRequestLoginRequest = match envelope { + versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostRequestLoginRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -469,21 +615,27 @@ where let response: versioned::account::HostRequestLoginResponse = match host.request_login(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostRequestLoginError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostRequestLoginResponse::V1(bare) => versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -498,13 +650,32 @@ where dispatcher.on_subscription(wire_table::CHAIN_FOLLOW_HEAD_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadFollowRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(_) => return Err(Vec::new()), + let envelope: versioned::chain::RemoteChainHeadFollowVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::chain::RemoteChainHeadFollowVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadFollowRequest = match envelope { + versioned::chain::RemoteChainHeadFollowVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::chain::RemoteChainHeadFollowRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::chain::RemoteChainHeadFollowVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.follow_head_subscribe(&cx, request).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::chain::RemoteChainHeadFollowItem| match item { + versioned::chain::RemoteChainHeadFollowItem::V1(bare) => versioned::chain::RemoteChainHeadFollowVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -513,15 +684,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_HEAD_HEADER, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadHeaderRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadHeaderVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadHeaderRequest = match envelope { + versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadHeaderRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -529,21 +707,27 @@ where let response: versioned::chain::RemoteChainHeadHeaderResponse = match host.get_head_header(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadHeaderError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadHeaderResponse::V1(bare) => versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -552,15 +736,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_HEAD_BODY, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadBodyRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadBodyVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadBodyRequest = match envelope { + versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadBodyRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -568,21 +759,27 @@ where let response: versioned::chain::RemoteChainHeadBodyResponse = match host.get_head_body(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadBodyError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadBodyResponse::V1(bare) => versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -591,15 +788,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_HEAD_STORAGE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadStorageRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadStorageVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadStorageRequest = match envelope { + versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadStorageRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -607,21 +811,27 @@ where let response: versioned::chain::RemoteChainHeadStorageResponse = match host.get_head_storage(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadStorageError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadStorageResponse::V1(bare) => versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -630,15 +840,22 @@ where dispatcher.on_request(wire_table::CHAIN_CALL_HEAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadCallRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadCallVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadCallRequest = match envelope { + versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadCallRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -646,21 +863,27 @@ where let response: versioned::chain::RemoteChainHeadCallResponse = match host.call_head(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadCallError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadCallResponse::V1(bare) => versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -669,15 +892,22 @@ where dispatcher.on_request(wire_table::CHAIN_UNPIN_HEAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadUnpinRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadUnpinVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadUnpinRequest = match envelope { + versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadUnpinRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -685,21 +915,27 @@ where let response: versioned::chain::RemoteChainHeadUnpinResponse = match host.unpin_head(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadUnpinError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadUnpinResponse::V1 => versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -708,15 +944,22 @@ where dispatcher.on_request(wire_table::CHAIN_CONTINUE_HEAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadContinueRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadContinueVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadContinueRequest = match envelope { + versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadContinueRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -724,21 +967,27 @@ where let response: versioned::chain::RemoteChainHeadContinueResponse = match host.continue_head(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadContinueError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadContinueResponse::V1 => versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -747,15 +996,22 @@ where dispatcher.on_request(wire_table::CHAIN_STOP_HEAD_OPERATION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadStopOperationRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadStopOperationVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadStopOperationRequest = match envelope { + versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadStopOperationRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -763,21 +1019,27 @@ where let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadStopOperationError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadStopOperationResponse::V1 => versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -786,15 +1048,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_SPEC_GENESIS_HASH, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainSpecGenesisHashVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match envelope { + versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainSpecGenesisHashRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -802,21 +1071,27 @@ where let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainSpecGenesisHashError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainSpecGenesisHashResponse::V1(bare) => versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -825,15 +1100,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_SPEC_CHAIN_NAME, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainSpecChainNameRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainSpecChainNameVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainSpecChainNameRequest = match envelope { + versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainSpecChainNameRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -841,21 +1123,27 @@ where let response: versioned::chain::RemoteChainSpecChainNameResponse = match host.get_spec_chain_name(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainSpecChainNameError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainSpecChainNameResponse::V1(bare) => versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -864,15 +1152,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_SPEC_PROPERTIES, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainSpecPropertiesRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainSpecPropertiesVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainSpecPropertiesRequest = match envelope { + versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainSpecPropertiesRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -880,21 +1175,27 @@ where let response: versioned::chain::RemoteChainSpecPropertiesResponse = match host.get_spec_properties(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainSpecPropertiesError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainSpecPropertiesResponse::V1(bare) => versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -903,15 +1204,22 @@ where dispatcher.on_request(wire_table::CHAIN_BROADCAST_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainTransactionBroadcastVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match envelope { + versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainTransactionBroadcastRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -919,21 +1227,27 @@ where let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainTransactionBroadcastError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainTransactionBroadcastResponse::V1(bare) => versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -942,15 +1256,22 @@ where dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainTransactionStopRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainTransactionStopVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainTransactionStopRequest = match envelope { + versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainTransactionStopRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -958,21 +1279,27 @@ where let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainTransactionStopError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainTransactionStopResponse::V1 => versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -981,15 +1308,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_CHAIN_INFO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainInfoRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainInfoVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainInfoRequest = match envelope { + versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainInfoRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -997,21 +1331,27 @@ where let response: versioned::chain::RemoteChainInfoResponse = match host.get_chain_info(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainInfoError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainInfoResponse::V1(bare) => versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1027,42 +1367,54 @@ where dispatcher.on_request(wire_table::CHAT_CREATE_ROOM, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chat::HostChatCreateRoomRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chat::HostChatCreateRoomVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chat::HostChatCreateRoomRequest = match envelope { + versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chat::HostChatCreateRoomRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, target_version)); + let error: truapi::CallError = truapi::CallError::Denied; + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } let response: versioned::chat::HostChatCreateRoomResponse = match host.create_room(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chat::HostChatCreateRoomError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chat::HostChatCreateRoomResponse::V1(bare) => versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1072,42 +1424,54 @@ where dispatcher.on_request(wire_table::CHAT_REGISTER_BOT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chat::HostChatRegisterBotRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chat::HostChatRegisterBotVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chat::HostChatRegisterBotRequest = match envelope { + versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chat::HostChatRegisterBotRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, target_version)); + let error: truapi::CallError = truapi::CallError::Denied; + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } let response: versioned::chat::HostChatRegisterBotResponse = match host.register_bot(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chat::HostChatRegisterBotError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chat::HostChatRegisterBotResponse::V1(bare) => versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1117,11 +1481,36 @@ where dispatcher.on_subscription(wire_table::CHAT_LIST_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::chat::HostChatListSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::chat::HostChatListSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let _request: () = match envelope { + versioned::chat::HostChatListSubscribeVersion::V1(truapi::versioned::Subscription::Start(_bare)) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::chat::HostChatListSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); - if !execution_allowed { return Err(Vec::new()); } + if !execution_allowed { + let error: truapi::CallError = truapi::CallError::Denied; + return Err(versioned::chat::HostChatListSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } let stream = host.list_subscribe(&cx).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::chat::HostChatListSubscribeItem| match item { + versioned::chat::HostChatListSubscribeItem::V1(bare) => versioned::chat::HostChatListSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1131,42 +1520,54 @@ where dispatcher.on_request(wire_table::CHAT_POST_MESSAGE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chat::HostChatPostMessageRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chat::HostChatPostMessageVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chat::HostChatPostMessageRequest = match envelope { + versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chat::HostChatPostMessageRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, target_version)); + let error: truapi::CallError = truapi::CallError::Denied; + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } let response: versioned::chat::HostChatPostMessageResponse = match host.post_message(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chat::HostChatPostMessageError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chat::HostChatPostMessageResponse::V1(bare) => versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1176,11 +1577,36 @@ where dispatcher.on_subscription(wire_table::CHAT_ACTION_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::chat::HostChatActionSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::chat::HostChatActionSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let _request: () = match envelope { + versioned::chat::HostChatActionSubscribeVersion::V1(truapi::versioned::Subscription::Start(_bare)) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::chat::HostChatActionSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); - if !execution_allowed { return Err(Vec::new()); } + if !execution_allowed { + let error: truapi::CallError = truapi::CallError::Denied; + return Err(versioned::chat::HostChatActionSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } let stream = host.action_subscribe(&cx).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::chat::HostChatActionSubscribeItem| match item { + versioned::chat::HostChatActionSubscribeItem::V1(bare) => versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1195,15 +1621,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentCreatePurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentCreatePurseRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1211,21 +1644,27 @@ where let response: versioned::coin_payment::HostCoinPaymentCreatePurseResponse = match host.create_purse(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentCreatePurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentCreatePurseResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1234,15 +1673,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_QUERY_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentQueryPurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentQueryPurseRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1250,21 +1696,27 @@ where let response: versioned::coin_payment::HostCoinPaymentQueryPurseResponse = match host.query_purse(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentQueryPurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentQueryPurseResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1273,28 +1725,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_REBALANCE_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentRebalancePurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentRebalancePurseRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.rebalance_purse(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentRebalancePurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentRebalancePurseItem| match item { + versioned::coin_payment::HostCoinPaymentRebalancePurseItem::V1(bare) => versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1303,28 +1772,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_DELETE_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentDeletePurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentDeletePurseRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.delete_purse(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentDeletePurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentDeletePurseItem| match item { + versioned::coin_payment::HostCoinPaymentDeletePurseItem::V1(bare) => versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1333,15 +1819,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentCreateReceivableVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match envelope { + versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentCreateReceivableRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1349,21 +1842,27 @@ where let response: versioned::coin_payment::HostCoinPaymentCreateReceivableResponse = match host.create_receivable(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentCreateReceivableError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentCreateReceivableResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1372,15 +1871,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_CHEQUE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentCreateChequeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match envelope { + versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentCreateChequeRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1388,21 +1894,27 @@ where let response: versioned::coin_payment::HostCoinPaymentCreateChequeResponse = match host.create_cheque(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentCreateChequeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentCreateChequeResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1411,28 +1923,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_DEPOSIT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentDepositRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentDepositVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentDepositVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentDepositRequest = match envelope { + versioned::coin_payment::HostCoinPaymentDepositVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentDepositRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentDepositVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.deposit(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentDepositError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentDepositVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentDepositItem| match item { + versioned::coin_payment::HostCoinPaymentDepositItem::V1(bare) => versioned::coin_payment::HostCoinPaymentDepositVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1441,28 +1970,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_REFUND, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentRefundRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentRefundVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentRefundVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentRefundRequest = match envelope { + versioned::coin_payment::HostCoinPaymentRefundVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentRefundRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentRefundVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.refund(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentRefundError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentRefundVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentRefundItem| match item { + versioned::coin_payment::HostCoinPaymentRefundItem::V1(bare) => versioned::coin_payment::HostCoinPaymentRefundVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1471,28 +2017,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentListenForRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentListenForVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentListenForVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentListenForRequest = match envelope { + versioned::coin_payment::HostCoinPaymentListenForVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentListenForRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentListenForVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.listen_for_payment(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentListenForError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentListenForVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentListenForItem| match item { + versioned::coin_payment::HostCoinPaymentListenForItem::V1(bare) => versioned::coin_payment::HostCoinPaymentListenForVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1507,15 +2070,22 @@ where dispatcher.on_request(wire_table::ENTROPY_DERIVE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::entropy::HostDeriveEntropyRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::entropy::HostDeriveEntropyVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::entropy::HostDeriveEntropyRequest = match envelope { + versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::entropy::HostDeriveEntropyRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1523,21 +2093,27 @@ where let response: versioned::entropy::HostDeriveEntropyResponse = match host.derive(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::entropy::HostDeriveEntropyError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::entropy::HostDeriveEntropyResponse::V1(bare) => versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1552,15 +2128,22 @@ where dispatcher.on_request(wire_table::LOCAL_STORAGE_READ, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageReadRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::local_storage::HostLocalStorageReadVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::local_storage::HostLocalStorageReadRequest = match envelope { + versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::local_storage::HostLocalStorageReadRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1568,21 +2151,27 @@ where let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::local_storage::HostLocalStorageReadError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::local_storage::HostLocalStorageReadResponse::V1(bare) => versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1591,15 +2180,22 @@ where dispatcher.on_request(wire_table::LOCAL_STORAGE_WRITE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageWriteRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::local_storage::HostLocalStorageWriteVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::local_storage::HostLocalStorageWriteRequest = match envelope { + versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::local_storage::HostLocalStorageWriteRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1607,21 +2203,27 @@ where let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::local_storage::HostLocalStorageWriteError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::local_storage::HostLocalStorageWriteResponse::V1 => versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1630,15 +2232,22 @@ where dispatcher.on_request(wire_table::LOCAL_STORAGE_CLEAR, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageClearRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::local_storage::HostLocalStorageClearVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::local_storage::HostLocalStorageClearRequest = match envelope { + versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::local_storage::HostLocalStorageClearRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1646,21 +2255,27 @@ where let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::local_storage::HostLocalStorageClearError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::local_storage::HostLocalStorageClearResponse::V1 => versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1675,10 +2290,32 @@ where dispatcher.on_subscription(wire_table::LOCALE_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::locale::HostLocaleSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::locale::HostLocaleSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let _request: () = match envelope { + versioned::locale::HostLocaleSubscribeVersion::V1(truapi::versioned::Subscription::Start(_bare)) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::locale::HostLocaleSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe(&cx).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::locale::HostLocaleSubscribeItem| match item { + versioned::locale::HostLocaleSubscribeItem::V1(bare) => versioned::locale::HostLocaleSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1693,15 +2330,22 @@ where dispatcher.on_request(wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::notifications::HostPushNotificationRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::notifications::HostPushNotificationVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::notifications::HostPushNotificationRequest = match envelope { + versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::notifications::HostPushNotificationRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1709,21 +2353,27 @@ where let response: versioned::notifications::HostPushNotificationResponse = match host.send_push_notification(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::notifications::HostPushNotificationError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::notifications::HostPushNotificationResponse::V1(bare) => versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1732,15 +2382,22 @@ where dispatcher.on_request(wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::notifications::HostPushNotificationCancelRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::notifications::HostPushNotificationCancelVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::notifications::HostPushNotificationCancelRequest = match envelope { + versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::notifications::HostPushNotificationCancelRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1748,21 +2405,27 @@ where let response: versioned::notifications::HostPushNotificationCancelResponse = match host.cancel_push_notification(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::notifications::HostPushNotificationCancelError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::notifications::HostPushNotificationCancelResponse::V1 => versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1777,28 +2440,45 @@ where dispatcher.on_subscription(wire_table::PAYMENT_BALANCE_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::payment::HostPaymentBalanceSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::payment::HostPaymentBalanceSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::payment::HostPaymentBalanceSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = match envelope { + versioned::payment::HostPaymentBalanceSubscribeVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::payment::HostPaymentBalanceSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::payment::HostPaymentBalanceSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.balance_subscribe(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::payment::HostPaymentBalanceSubscribeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::payment::HostPaymentBalanceSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::payment::HostPaymentBalanceSubscribeItem| match item { + versioned::payment::HostPaymentBalanceSubscribeItem::V1(bare) => versioned::payment::HostPaymentBalanceSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1807,15 +2487,22 @@ where dispatcher.on_request(wire_table::PAYMENT_REQUEST, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::payment::HostPaymentRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::payment::HostPaymentVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::payment::HostPaymentRequest = match envelope { + versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::payment::HostPaymentRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1823,21 +2510,27 @@ where let response: versioned::payment::HostPaymentResponse = match host.request(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::payment::HostPaymentError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::payment::HostPaymentResponse::V1(bare) => versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1846,28 +2539,45 @@ where dispatcher.on_subscription(wire_table::PAYMENT_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::payment::HostPaymentStatusSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::payment::HostPaymentStatusSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::payment::HostPaymentStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::payment::HostPaymentStatusSubscribeRequest = match envelope { + versioned::payment::HostPaymentStatusSubscribeVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::payment::HostPaymentStatusSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::payment::HostPaymentStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.status_subscribe(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::payment::HostPaymentStatusSubscribeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::payment::HostPaymentStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::payment::HostPaymentStatusSubscribeItem| match item { + versioned::payment::HostPaymentStatusSubscribeItem::V1(bare) => versioned::payment::HostPaymentStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1876,15 +2586,22 @@ where dispatcher.on_request(wire_table::PAYMENT_TOP_UP, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::payment::HostPaymentTopUpRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::payment::HostPaymentTopUpVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::payment::HostPaymentTopUpRequest = match envelope { + versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::payment::HostPaymentTopUpRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1892,21 +2609,27 @@ where let response: versioned::payment::HostPaymentTopUpResponse = match host.top_up(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::payment::HostPaymentTopUpError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::payment::HostPaymentTopUpResponse::V1 => versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1921,15 +2644,22 @@ where dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::permissions::HostDevicePermissionRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::permissions::HostDevicePermissionVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::permissions::HostDevicePermissionRequest = match envelope { + versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::permissions::HostDevicePermissionRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1937,21 +2667,27 @@ where let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::permissions::HostDevicePermissionError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::permissions::HostDevicePermissionResponse::V1(bare) => versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1960,15 +2696,22 @@ where dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_REMOTE_PERMISSION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::permissions::RemotePermissionRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::permissions::RemotePermissionVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::permissions::RemotePermissionRequest = match envelope { + versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::permissions::RemotePermissionRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1976,21 +2719,27 @@ where let response: versioned::permissions::RemotePermissionResponse = match host.request_remote_permission(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::permissions::RemotePermissionError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::permissions::RemotePermissionResponse::V1(bare) => versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2005,13 +2754,32 @@ where dispatcher.on_subscription(wire_table::PREIMAGE_LOOKUP_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(_) => return Err(Vec::new()), + let envelope: versioned::preimage::RemotePreimageLookupSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::preimage::RemotePreimageLookupSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = match envelope { + versioned::preimage::RemotePreimageLookupSubscribeVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::preimage::RemotePreimageLookupSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::preimage::RemotePreimageLookupSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.lookup_subscribe(&cx, request).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::preimage::RemotePreimageLookupSubscribeItem| match item { + versioned::preimage::RemotePreimageLookupSubscribeItem::V1(bare) => versioned::preimage::RemotePreimageLookupSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -2020,15 +2788,22 @@ where dispatcher.on_request(wire_table::PREIMAGE_SUBMIT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::preimage::RemotePreimageSubmitRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::preimage::RemotePreimageSubmitVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::preimage::RemotePreimageSubmitRequest = match envelope { + versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::preimage::RemotePreimageSubmitRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2036,21 +2811,27 @@ where let response: versioned::preimage::RemotePreimageSubmitResponse = match host.submit(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::preimage::RemotePreimageSubmitError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::preimage::RemotePreimageSubmitResponse::V1(bare) => versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2065,15 +2846,22 @@ where dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::resource_allocation::HostRequestResourceAllocationVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match envelope { + versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::resource_allocation::HostRequestResourceAllocationRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2081,21 +2869,27 @@ where let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::resource_allocation::HostRequestResourceAllocationError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::resource_allocation::HostRequestResourceAllocationResponse::V1(bare) => versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2110,15 +2904,22 @@ where dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostCreateTransactionRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostCreateTransactionVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostCreateTransactionRequest = match envelope { + versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostCreateTransactionRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2126,21 +2927,27 @@ where let response: versioned::signing::HostCreateTransactionResponse = match host.create_transaction(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostCreateTransactionError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostCreateTransactionResponse::V1(bare) => versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2149,15 +2956,22 @@ where dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostCreateTransactionWithLegacyAccountVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match envelope { + versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostCreateTransactionWithLegacyAccountRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2165,21 +2979,27 @@ where let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostCreateTransactionWithLegacyAccountError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostCreateTransactionWithLegacyAccountResponse::V1(bare) => versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2188,15 +3008,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignRawWithLegacyAccountVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match envelope { + versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignRawWithLegacyAccountRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2204,21 +3031,27 @@ where let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignRawWithLegacyAccountError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignRawWithLegacyAccountResponse::V1(bare) => versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2227,15 +3060,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignPayloadWithLegacyAccountVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match envelope { + versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignPayloadWithLegacyAccountRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2243,21 +3083,27 @@ where let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignPayloadWithLegacyAccountError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignPayloadWithLegacyAccountResponse::V1(bare) => versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2266,15 +3112,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_RAW, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignRawRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignRawVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignRawRequest = match envelope { + versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignRawRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2282,21 +3135,27 @@ where let response: versioned::signing::HostSignRawResponse = match host.sign_raw(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignRawError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignRawResponse::V1(bare) => versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2305,15 +3164,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignPayloadRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignPayloadVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignPayloadRequest = match envelope { + versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignPayloadRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2321,21 +3187,27 @@ where let response: versioned::signing::HostSignPayloadResponse = match host.sign_payload(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignPayloadError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignPayloadResponse::V1(bare) => versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2350,28 +3222,45 @@ where dispatcher.on_subscription(wire_table::STATEMENT_STORE_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match envelope { + versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::statement_store::RemoteStatementStoreSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.subscribe(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreSubscribeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::statement_store::RemoteStatementStoreSubscribeItem| match item { + versioned::statement_store::RemoteStatementStoreSubscribeItem::V1(bare) => versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -2380,15 +3269,22 @@ where dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreCreateProofVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match envelope { + versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::statement_store::RemoteStatementStoreCreateProofRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2396,21 +3292,27 @@ where let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreCreateProofError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::statement_store::RemoteStatementStoreCreateProofResponse::V1(bare) => versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2419,15 +3321,22 @@ where dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF_AUTHORIZED, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match envelope { + versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2435,21 +3344,27 @@ where let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse::V1(bare) => versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2458,23 +3373,38 @@ where dispatcher.on_request(wire_table::STATEMENT_STORE_SUBMIT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreSubmitVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match envelope { + versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::statement_store::RemoteStatementStoreSubmitRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); match host.submit(&cx, request).await { - Ok(()) => Ok(encode_versioned_unit_ok_payload(target_version)), + Ok(()) => Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Ok(()))).encode()), Err(err) => { - Ok(encode_versioned_err_payload(err, target_version)) + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreSubmitError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()) } } }) @@ -2491,15 +3421,22 @@ where dispatcher.on_request(wire_table::SYSTEM_HANDSHAKE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostHandshakeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostHandshakeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostHandshakeRequest = match envelope { + versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::system::HostHandshakeRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2507,21 +3444,27 @@ where let response: versioned::system::HostHandshakeResponse = match host.handshake(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostHandshakeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostHandshakeResponse::V1 => versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -2530,15 +3473,22 @@ where dispatcher.on_request(wire_table::SYSTEM_FEATURE_SUPPORTED, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostFeatureSupportedRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostFeatureSupportedVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostFeatureSupportedRequest = match envelope { + versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::system::HostFeatureSupportedRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2546,21 +3496,27 @@ where let response: versioned::system::HostFeatureSupportedResponse = match host.feature_supported(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostFeatureSupportedError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostFeatureSupportedResponse::V1(bare) => versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2569,15 +3525,22 @@ where dispatcher.on_request(wire_table::SYSTEM_NAVIGATE_TO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostNavigateToRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostNavigateToVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostNavigateToRequest = match envelope { + versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::system::HostNavigateToRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2585,21 +3548,27 @@ where let response: versioned::system::HostNavigateToResponse = match host.navigate_to(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostNavigateToError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostNavigateToResponse::V1 => versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -2608,15 +3577,22 @@ where dispatcher.on_request(wire_table::SYSTEM_HOST_INFO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostInfoRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostInfoVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostInfoRequest = match envelope { + versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::system::HostInfoRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2624,21 +3600,27 @@ where let response: versioned::system::HostInfoResponse = match host.host_info(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostInfoError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostInfoResponse::V1(bare) => versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2647,15 +3629,22 @@ where dispatcher.on_request(wire_table::SYSTEM_GET_PRODUCT_CONTEXT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostGetProductContextRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostGetProductContextVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostGetProductContextRequest = match envelope { + versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::system::HostGetProductContextRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2663,21 +3652,27 @@ where let response: versioned::system::HostGetProductContextResponse = match host.get_product_context(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostGetProductContextError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostGetProductContextResponse::V1(bare) => versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2692,10 +3687,32 @@ where dispatcher.on_subscription(wire_table::THEME_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::theme::HostThemeSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::theme::HostThemeSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let _request: () = match envelope { + versioned::theme::HostThemeSubscribeVersion::V1(truapi::versioned::Subscription::Start(_bare)) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::theme::HostThemeSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe(&cx).await; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::theme::HostThemeSubscribeItem| match item { + versioned::theme::HostThemeSubscribeItem::V1(bare) => versioned::theme::HostThemeSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs index 891fce3c1..c3d06f400 100644 --- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -2,38 +2,23 @@ //! //! Auto-generated by truapi-codegen. Do not edit. //! -//! Every frame carries a `(trait, method)` discriminant pair. Each -//! method reserves either two method ids (request/response) or four -//! (start/stop/interrupt/receive) within its trait. The ids for each -//! method are exposed as a named const (`PREIMAGE_SUBMIT`, ...); -//! [`WIRE_TABLE`] and the generated dispatcher both reference those -//! consts so the numbers live in exactly one place. The table is -//! sorted by (trait id, request/start id). - -/// Request method wire discriminants. +//! Every frame carries a `(trait, method)` discriminant pair; one +//! method id addresses every frame a method ever sends or receives, +//! regardless of shape. Direction (request/response, or a +//! subscription's start/stop/interrupt/receive) and version are +//! carried inside the payload. The ids for each method are exposed as +//! a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the +//! generated dispatcher both reference those consts so the numbers +//! live in exactly one place. The table is sorted by (trait id, +//! method id). + +/// Wire discriminants for one method. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RequestFrameIds { - /// Trait discriminant carried by both frames. +pub struct MethodIds { + /// Trait discriminant carried by every frame of this method. pub trait_id: u8, - /// Method discriminant for the request frame. - pub request_id: u8, - /// Method discriminant for the response frame. - pub response_id: u8, -} - -/// Subscription method wire discriminants. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SubscriptionFrameIds { - /// Trait discriminant carried by all four frames. - pub trait_id: u8, - /// Method discriminant for the start frame. - pub start_id: u8, - /// Method discriminant for the stop frame. - pub stop_id: u8, - /// Method discriminant for the interrupt frame (server-initiated termination). - pub interrupt_id: u8, - /// Method discriminant for each receive frame (a streamed item). - pub receive_id: u8, + /// Method discriminant carried by every frame of this method. + pub method_id: u8, } /// A single wire-table row. @@ -44,548 +29,445 @@ pub struct WireEntry { pub kind: WireKind, } -/// Wire-slot shape: request/response pair or subscription quartet. +/// Wire-slot shape: request/response or a subscription's +/// start/stop/interrupt/receive quartet. pub enum WireKind { /// Request/response method. - Request(RequestFrameIds), + Request(MethodIds), /// Subscription method. - Subscription(SubscriptionFrameIds), + Subscription(MethodIds), } /// Wire discriminants for `system_handshake`. -pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_HANDSHAKE: MethodIds = MethodIds { trait_id: 193, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `system_feature_supported`. -pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_FEATURE_SUPPORTED: MethodIds = MethodIds { trait_id: 193, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `system_navigate_to`. -pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_NAVIGATE_TO: MethodIds = MethodIds { trait_id: 193, - request_id: 4, - response_id: 5, + method_id: 2, }; /// Wire discriminants for `system_host_info`. -pub const SYSTEM_HOST_INFO: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_HOST_INFO: MethodIds = MethodIds { trait_id: 193, - request_id: 6, - response_id: 7, + method_id: 3, }; /// Wire discriminants for `system_get_product_context`. -pub const SYSTEM_GET_PRODUCT_CONTEXT: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_GET_PRODUCT_CONTEXT: MethodIds = MethodIds { trait_id: 193, - request_id: 8, - response_id: 9, + method_id: 4, }; /// Wire discriminants for `account_connection_status_subscribe`. -pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: MethodIds = MethodIds { trait_id: 194, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `account_get_account`. -pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_ACCOUNT: MethodIds = MethodIds { trait_id: 194, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `account_get_account_alias`. -pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_ACCOUNT_ALIAS: MethodIds = MethodIds { trait_id: 194, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `account_create_account_proof`. -pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_CREATE_ACCOUNT_PROOF: MethodIds = MethodIds { trait_id: 194, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `account_get_legacy_accounts`. -pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_LEGACY_ACCOUNTS: MethodIds = MethodIds { trait_id: 194, - request_id: 10, - response_id: 11, + method_id: 4, }; /// Wire discriminants for `account_get_user_id`. -pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_USER_ID: MethodIds = MethodIds { trait_id: 194, - request_id: 12, - response_id: 13, + method_id: 5, }; /// Wire discriminants for `account_request_login`. -pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_REQUEST_LOGIN: MethodIds = MethodIds { trait_id: 194, - request_id: 14, - response_id: 15, + method_id: 6, }; /// Wire discriminants for `account_sign_vrf`. -pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_SIGN_VRF: MethodIds = MethodIds { trait_id: 194, - request_id: 16, - response_id: 17, + method_id: 7, }; /// Wire discriminants for `account_register_ring_vrf_key`. -pub const ACCOUNT_REGISTER_RING_VRF_KEY: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_REGISTER_RING_VRF_KEY: MethodIds = MethodIds { trait_id: 194, - request_id: 168, - response_id: 169, + method_id: 8, }; /// Wire discriminants for `account_list_ring_vrf_keys`. -pub const ACCOUNT_LIST_RING_VRF_KEYS: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_LIST_RING_VRF_KEYS: MethodIds = MethodIds { trait_id: 194, - request_id: 170, - response_id: 171, + method_id: 9, }; /// Wire discriminants for `account_ring_vrf_sign`. -pub const ACCOUNT_RING_VRF_SIGN: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_RING_VRF_SIGN: MethodIds = MethodIds { trait_id: 194, - request_id: 172, - response_id: 173, + method_id: 10, }; /// Wire discriminants for `chain_follow_head_subscribe`. -pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: MethodIds = MethodIds { trait_id: 195, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `chain_get_head_header`. -pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_HEAD_HEADER: MethodIds = MethodIds { trait_id: 195, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `chain_get_head_body`. -pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_HEAD_BODY: MethodIds = MethodIds { trait_id: 195, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `chain_get_head_storage`. -pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_HEAD_STORAGE: MethodIds = MethodIds { trait_id: 195, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `chain_call_head`. -pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { +pub const CHAIN_CALL_HEAD: MethodIds = MethodIds { trait_id: 195, - request_id: 10, - response_id: 11, + method_id: 4, }; /// Wire discriminants for `chain_unpin_head`. -pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { +pub const CHAIN_UNPIN_HEAD: MethodIds = MethodIds { trait_id: 195, - request_id: 12, - response_id: 13, + method_id: 5, }; /// Wire discriminants for `chain_continue_head`. -pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { +pub const CHAIN_CONTINUE_HEAD: MethodIds = MethodIds { trait_id: 195, - request_id: 14, - response_id: 15, + method_id: 6, }; /// Wire discriminants for `chain_stop_head_operation`. -pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { +pub const CHAIN_STOP_HEAD_OPERATION: MethodIds = MethodIds { trait_id: 195, - request_id: 16, - response_id: 17, + method_id: 7, }; /// Wire discriminants for `chain_get_spec_genesis_hash`. -pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_SPEC_GENESIS_HASH: MethodIds = MethodIds { trait_id: 195, - request_id: 18, - response_id: 19, + method_id: 8, }; /// Wire discriminants for `chain_get_spec_chain_name`. -pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_SPEC_CHAIN_NAME: MethodIds = MethodIds { trait_id: 195, - request_id: 20, - response_id: 21, + method_id: 9, }; /// Wire discriminants for `chain_get_spec_properties`. -pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_SPEC_PROPERTIES: MethodIds = MethodIds { trait_id: 195, - request_id: 22, - response_id: 23, + method_id: 10, }; /// Wire discriminants for `chain_broadcast_transaction`. -pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { +pub const CHAIN_BROADCAST_TRANSACTION: MethodIds = MethodIds { trait_id: 195, - request_id: 24, - response_id: 25, + method_id: 11, }; /// Wire discriminants for `chain_stop_transaction`. -pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { +pub const CHAIN_STOP_TRANSACTION: MethodIds = MethodIds { trait_id: 195, - request_id: 26, - response_id: 27, + method_id: 12, }; /// Wire discriminants for `chain_get_chain_info`. -pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_CHAIN_INFO: MethodIds = MethodIds { trait_id: 195, - request_id: 166, - response_id: 167, + method_id: 13, }; /// Wire discriminants for `chat_create_room`. -pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { +pub const CHAT_CREATE_ROOM: MethodIds = MethodIds { trait_id: 196, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `chat_register_bot`. -pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { +pub const CHAT_REGISTER_BOT: MethodIds = MethodIds { trait_id: 196, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `chat_list_subscribe`. -pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAT_LIST_SUBSCRIBE: MethodIds = MethodIds { trait_id: 196, - start_id: 4, - stop_id: 5, - interrupt_id: 6, - receive_id: 7, + method_id: 2, }; /// Wire discriminants for `chat_post_message`. -pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { +pub const CHAT_POST_MESSAGE: MethodIds = MethodIds { trait_id: 196, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `chat_action_subscribe`. -pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAT_ACTION_SUBSCRIBE: MethodIds = MethodIds { trait_id: 196, - start_id: 10, - stop_id: 11, - interrupt_id: 12, - receive_id: 13, + method_id: 4, }; /// Wire discriminants for `chat_custom_message_render`. -pub const CHAT_CUSTOM_MESSAGE_RENDER: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAT_CUSTOM_MESSAGE_RENDER: MethodIds = MethodIds { trait_id: 196, - start_id: 14, - stop_id: 15, - interrupt_id: 16, - receive_id: 17, + method_id: 5, }; /// Wire discriminants for `coin_payment_create_purse`. -pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_CREATE_PURSE: MethodIds = MethodIds { trait_id: 197, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `coin_payment_query_purse`. -pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_QUERY_PURSE: MethodIds = MethodIds { trait_id: 197, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `coin_payment_rebalance_purse`. -pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_REBALANCE_PURSE: MethodIds = MethodIds { trait_id: 197, - start_id: 4, - stop_id: 5, - interrupt_id: 6, - receive_id: 7, + method_id: 2, }; /// Wire discriminants for `coin_payment_delete_purse`. -pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_DELETE_PURSE: MethodIds = MethodIds { trait_id: 197, - start_id: 8, - stop_id: 9, - interrupt_id: 10, - receive_id: 11, + method_id: 3, }; /// Wire discriminants for `coin_payment_create_receivable`. -pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_CREATE_RECEIVABLE: MethodIds = MethodIds { trait_id: 197, - request_id: 12, - response_id: 13, + method_id: 4, }; /// Wire discriminants for `coin_payment_create_cheque`. -pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_CREATE_CHEQUE: MethodIds = MethodIds { trait_id: 197, - request_id: 14, - response_id: 15, + method_id: 5, }; /// Wire discriminants for `coin_payment_deposit`. -pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_DEPOSIT: MethodIds = MethodIds { trait_id: 197, - start_id: 16, - stop_id: 17, - interrupt_id: 18, - receive_id: 19, + method_id: 6, }; /// Wire discriminants for `coin_payment_refund`. -pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_REFUND: MethodIds = MethodIds { trait_id: 197, - start_id: 20, - stop_id: 21, - interrupt_id: 22, - receive_id: 23, + method_id: 7, }; /// Wire discriminants for `coin_payment_listen_for_payment`. -pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: MethodIds = MethodIds { trait_id: 197, - start_id: 24, - stop_id: 25, - interrupt_id: 26, - receive_id: 27, + method_id: 8, }; /// Wire discriminants for `entropy_derive`. -pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { +pub const ENTROPY_DERIVE: MethodIds = MethodIds { trait_id: 198, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `local_storage_read`. -pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { +pub const LOCAL_STORAGE_READ: MethodIds = MethodIds { trait_id: 199, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `local_storage_write`. -pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { +pub const LOCAL_STORAGE_WRITE: MethodIds = MethodIds { trait_id: 199, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `local_storage_clear`. -pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { +pub const LOCAL_STORAGE_CLEAR: MethodIds = MethodIds { trait_id: 199, - request_id: 4, - response_id: 5, + method_id: 2, }; /// Wire discriminants for `notifications_send_push_notification`. -pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: MethodIds = MethodIds { trait_id: 200, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `notifications_cancel_push_notification`. -pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: MethodIds = MethodIds { trait_id: 200, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `payment_balance_subscribe`. -pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const PAYMENT_BALANCE_SUBSCRIBE: MethodIds = MethodIds { trait_id: 201, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `payment_top_up`. -pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { +pub const PAYMENT_TOP_UP: MethodIds = MethodIds { trait_id: 201, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `payment_request`. -pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { +pub const PAYMENT_REQUEST: MethodIds = MethodIds { trait_id: 201, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `payment_status_subscribe`. -pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const PAYMENT_STATUS_SUBSCRIBE: MethodIds = MethodIds { trait_id: 201, - start_id: 8, - stop_id: 9, - interrupt_id: 10, - receive_id: 11, + method_id: 3, }; /// Wire discriminants for `permissions_request_device_permission`. -pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: MethodIds = MethodIds { trait_id: 202, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `permissions_request_remote_permission`. -pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: MethodIds = MethodIds { trait_id: 202, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `preimage_lookup_subscribe`. -pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const PREIMAGE_LOOKUP_SUBSCRIBE: MethodIds = MethodIds { trait_id: 203, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `preimage_submit`. -pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { +pub const PREIMAGE_SUBMIT: MethodIds = MethodIds { trait_id: 203, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `resource_allocation_request`. -pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { +pub const RESOURCE_ALLOCATION_REQUEST: MethodIds = MethodIds { trait_id: 204, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `signing_create_transaction`. -pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { +pub const SIGNING_CREATE_TRANSACTION: MethodIds = MethodIds { trait_id: 205, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `signing_create_transaction_with_legacy_account`. -pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { trait_id: 205, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `signing_sign_raw_with_legacy_account`. -pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { trait_id: 205, - request_id: 4, - response_id: 5, + method_id: 2, }; /// Wire discriminants for `signing_sign_payload_with_legacy_account`. -pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { trait_id: 205, - request_id: 6, - response_id: 7, + method_id: 3, }; /// Wire discriminants for `signing_sign_raw`. -pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_RAW: MethodIds = MethodIds { trait_id: 205, - request_id: 8, - response_id: 9, + method_id: 4, }; /// Wire discriminants for `signing_sign_payload`. -pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_PAYLOAD: MethodIds = MethodIds { trait_id: 205, - request_id: 10, - response_id: 11, + method_id: 5, }; /// Wire discriminants for `statement_store_subscribe`. -pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const STATEMENT_STORE_SUBSCRIBE: MethodIds = MethodIds { trait_id: 206, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `statement_store_create_proof`. -pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { +pub const STATEMENT_STORE_CREATE_PROOF: MethodIds = MethodIds { trait_id: 206, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `statement_store_submit`. -pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { +pub const STATEMENT_STORE_SUBMIT: MethodIds = MethodIds { trait_id: 206, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `statement_store_create_proof_authorized`. -pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: MethodIds = MethodIds { trait_id: 206, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `theme_subscribe`. -pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const THEME_SUBSCRIBE: MethodIds = MethodIds { trait_id: 207, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `locale_subscribe`. -pub const LOCALE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const LOCALE_SUBSCRIBE: MethodIds = MethodIds { trait_id: 208, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// The full wire table. Trait ids and per-trait method ordering are diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs index 429d30346..ff38d1738 100644 --- a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -221,7 +221,56 @@ fn golden_dispatcher_and_wire_table() { dump.display() ); } + if output_name == "dispatcher.rs" { + // Every real method must resolve to the nested-envelope codegen + // path (RFC 0028): these markers belong only to the legacy + // (pre-RFC-0028) emission path, kept solely for truapi-codegen's + // own synthetic unit-test fixtures whose request/item wrapper + // names don't follow the `{Base}Request`/`{Base}Item` convention + // (see `envelope_type_name`). A real method's request or item + // wrapper always follows that convention, so a hit here means + // one silently fell through to the legacy path instead of + // failing loudly — most likely a renamed wrapper type no longer + // matching `envelope_type_name`'s expectations. + for marker in [ + "RequestFrameIds", + "SubscriptionFrameIds", + "encode_versioned_ok_payload", + "encode_versioned_err_payload", + "encode_versioned_unit_ok_payload", + "encode_versioned_interrupt_payload", + ] { + assert!( + !actual.contains(marker), + "generated dispatcher.rs contains `{marker}`, a legacy (pre-RFC-0028) \ + codegen marker; every real method should resolve to the nested-envelope \ + path instead — check which method's request/item wrapper name stopped \ + matching `envelope_type_name`'s `{{Base}}Request`/`{{Base}}Item` convention" + ); + } + } } + + // `ts.rs` re-implements the same envelope/legacy fork independently of + // `rust/dispatcher.rs` (its own `envelope_type_name`/`method_envelope_name`), + // so a real method regressing to the legacy path there would slip past the + // Rust-side check above undetected. `S.indexedTaggedUnion(` is the codec + // shape the legacy fallback inlines directly into a method body + // (`versioned_result_codec_expr`/`write_payload_field`); the nested-envelope + // path only ever references a pre-built `T.{Method}Version` codec by name, + // so this string never appears in `client.ts` for a real method — it does + // appear throughout `types.ts`, where versioned wrapper *type* definitions + // legitimately use the same combinator, which is why only `client.ts` is + // scanned here. + let client_ts = fs::read_to_string(tempdir.path().join("ts").join("client.ts")) + .unwrap_or_else(|e| panic!("read generated client.ts: {e}")); + assert!( + !client_ts.contains("indexedTaggedUnion"), + "generated client.ts contains `S.indexedTaggedUnion(`, a legacy (pre-RFC-0028) \ + codegen marker; every real method should resolve to the nested-envelope path \ + instead — check which method's request/item wrapper name stopped matching \ + `method_envelope_name`'s expectations on the TS side" + ); } /// Idempotence guard at the integration level: running the binary twice diff --git a/rust/crates/truapi-macros/src/lib.rs b/rust/crates/truapi-macros/src/lib.rs index fc5a591e9..562ebd388 100644 --- a/rust/crates/truapi-macros/src/lib.rs +++ b/rust/crates/truapi-macros/src/lib.rs @@ -5,11 +5,14 @@ //! `Versioned`/`IntoLatest`/`FromLatest` impls from `truapi::versioned`. //! //! The `wire` attribute marks a trait method with its wire-protocol -//! discriminant ids, and the `wire_trait` attribute marks an API trait with -//! its trait discriminant. Together they form the two-byte -//! `(trait, method)` discriminant pair in the +//! discriminant id, and the `wire_trait` attribute marks an API trait with +//! its trait discriminant. Together they form the two-byte `(trait, method)` +//! discriminant pair in the //! `Struct { request_id: str, payload: (trait, method, bytes) }` envelope; -//! trait and method ordering become part of the wire protocol. +//! trait and method ordering become part of the wire protocol. One id +//! addresses a method regardless of its shape — direction (request/response, +//! or a subscription's start/stop/interrupt/receive) is carried inside the +//! method's versioned payload, not by a separate id. //! //! At compile time the macro validates that every id literal is a `u8`. It emits //! a hidden doc line so the value survives into rustdoc JSON, where @@ -34,12 +37,7 @@ use syn::{ #[derive(Default)] struct WireArgs { host_initiated: bool, - request_id: Option, - response_id: Option, - start_id: Option, - stop_id: Option, - interrupt_id: Option, - receive_id: Option, + id: Option, } struct ServiceArgs { @@ -90,13 +88,18 @@ impl Parse for WireArgs { input.parse::()?; continue; } + if key != "id" { + return Err(syn::Error::new(key.span(), "expected `id = N`")); + } input.parse::()?; let lit: LitInt = input.parse()?; let value = lit.base10_parse().map_err(|err| { syn::Error::new(lit.span(), format!("wire id must fit in a u8: {err}")) })?; - set_id(&mut args, &key, value)?; + if args.id.replace(value).is_some() { + return Err(syn::Error::new(key.span(), "duplicate `id`")); + } if input.is_empty() { break; @@ -104,48 +107,24 @@ impl Parse for WireArgs { input.parse::()?; } - if args.request_id.is_none() && args.start_id.is_none() { - return Err(input.error("missing `request_id = N` or `start_id = N`")); + if args.id.is_none() { + return Err(input.error("missing `id = N`")); } Ok(args) } } -fn set_id(args: &mut WireArgs, key: &Ident, value: u8) -> syn::Result<()> { - let target = if key == "request_id" { - &mut args.request_id - } else if key == "response_id" { - &mut args.response_id - } else if key == "start_id" { - &mut args.start_id - } else if key == "stop_id" { - &mut args.stop_id - } else if key == "interrupt_id" { - &mut args.interrupt_id - } else if key == "receive_id" { - &mut args.receive_id - } else { - return Err(syn::Error::new( - key.span(), - "expected one of `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, `receive_id`", - )); - }; - - if target.replace(value).is_some() { - return Err(syn::Error::new(key.span(), format!("duplicate `{key}`"))); - } - - Ok(()) -} - -/// Mark a TrUAPI trait method with its wire-protocol discriminant id. +/// Mark a TrUAPI trait method with its wire-protocol discriminant id. One id +/// addresses the method regardless of shape (request/response or +/// subscription) — direction is carried inside the method's versioned +/// payload, not by a separate wire id. /// /// ```ignore -/// #[wire(request_id = 4)] +/// #[wire(id = 4)] /// async fn host_account_get(...) -> ...; /// -/// #[wire(start_id = 42)] +/// #[wire(id = 42)] /// async fn host_account_connection_status_subscribe(...) -> ...; /// ``` /// @@ -232,17 +211,10 @@ pub fn wire_trait(args: TokenStream, item: TokenStream) -> TokenStream { } fn wire_tags(args: &WireArgs) -> Vec { - let mut tags = [ - ("request_id", args.request_id), - ("response_id", args.response_id), - ("start_id", args.start_id), - ("stop_id", args.stop_id), - ("interrupt_id", args.interrupt_id), - ("receive_id", args.receive_id), - ] - .into_iter() - .filter_map(|(name, value)| value.map(|id| format!("@wire_{name}={id}"))) - .collect::>(); + let mut tags = Vec::new(); + if let Some(id) = args.id { + tags.push(format!("@wire_id={id}")); + } if args.host_initiated { tags.push("@wire_host_initiated".to_string()); } diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index 0e23c4829..dc3ca934b 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -31,7 +31,7 @@ pub trait Account: Send + Sync { /// ); /// console.log("connection status:", status); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn connection_status_subscribe( &self, _cx: &CallContext, @@ -63,7 +63,7 @@ pub trait Account: Send + Sync { /// assert(otherProduct.isOk(), "cross-product getAccount was denied or failed:", otherProduct); /// console.log("other product account retrieved after approval:", otherProduct.value); /// ``` - #[wire(request_id = 4)] + #[wire(id = 1)] async fn get_account( &self, _cx: &CallContext, @@ -107,7 +107,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "getAccountAlias failed:", result); /// console.log("account alias:", result.value); /// ``` - #[wire(request_id = 6)] + #[wire(id = 2)] async fn get_account_alias( &self, _cx: &CallContext, @@ -152,7 +152,7 @@ pub trait Account: Send + Sync { /// ); /// console.log("foreign account proof refused without prompting"); /// ``` - #[wire(request_id = 8)] + #[wire(id = 3)] async fn create_account_proof( &self, _cx: &CallContext, @@ -186,7 +186,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "signVrf failed:", result); /// console.log("vrf signature:", result.value); /// ``` - #[wire(request_id = 16)] + #[wire(id = 7)] async fn sign_vrf( &self, _cx: &CallContext, @@ -215,7 +215,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "registerRingVrfKey failed:", result); /// console.log("ring VRF public key:", result.value); /// ``` - #[wire(request_id = 168)] + #[wire(id = 8)] async fn register_ring_vrf_key( &self, _cx: &CallContext, @@ -238,7 +238,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "listRingVrfKeys failed:", result); /// console.log("registered ring VRF keys:", result.value); /// ``` - #[wire(request_id = 170)] + #[wire(id = 9)] async fn list_ring_vrf_keys( &self, _cx: &CallContext, @@ -264,7 +264,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "ringVrfSign failed:", result); /// console.log("ring VRF signature:", result.value); /// ``` - #[wire(request_id = 172)] + #[wire(id = 10)] async fn ring_vrf_sign( &self, _cx: &CallContext, @@ -283,7 +283,7 @@ pub trait Account: Send + Sync { /// assert(result.value.accounts.length === 0, "unexpected legacy accounts:", result.value); /// console.log("legacy accounts:", result.value.accounts); /// ``` - #[wire(request_id = 10)] + #[wire(id = 4)] async fn get_legacy_accounts( &self, _cx: &CallContext, @@ -299,7 +299,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "getUserId failed:", result); /// console.log("user id:", result.value); /// ``` - #[wire(request_id = 12)] + #[wire(id = 5)] async fn get_user_id( &self, _cx: &CallContext, @@ -320,7 +320,7 @@ pub trait Account: Send + Sync { /// assert(result.isOk(), "requestLogin failed:", result); /// console.log("login completed:", result.value); /// ``` - #[wire(request_id = 14)] + #[wire(id = 6)] async fn request_login( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index f21bdaccf..4209eb7cf 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -46,7 +46,7 @@ pub trait Chain: Send + Sync { /// ); /// console.log("head follow event:", item); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn follow_head_subscribe( &self, _cx: &CallContext, @@ -75,7 +75,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getHeadHeader failed:", result); /// console.log("block header:", result.value); /// ``` - #[wire(request_id = 4)] + #[wire(id = 1)] async fn get_head_header( &self, _cx: &CallContext, @@ -104,7 +104,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getHeadBody failed:", result); /// console.log("block body:", result.value); /// ``` - #[wire(request_id = 6)] + #[wire(id = 2)] async fn get_head_body( &self, _cx: &CallContext, @@ -138,7 +138,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getHeadStorage failed:", result); /// console.log("storage value:", result.value); /// ``` - #[wire(request_id = 8)] + #[wire(id = 3)] async fn get_head_storage( &self, _cx: &CallContext, @@ -174,7 +174,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "callHead failed:", result); /// console.log("runtime call result:", result.value); /// ``` - #[wire(request_id = 10)] + #[wire(id = 4)] async fn call_head( &self, _cx: &CallContext, @@ -207,7 +207,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "unpinHead failed:", result); /// console.log("blocks unpinned"); /// ``` - #[wire(request_id = 12)] + #[wire(id = 5)] async fn unpin_head( &self, _cx: &CallContext, @@ -240,7 +240,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "continueHead failed:", result); /// console.log("operation continued"); /// ``` - #[wire(request_id = 14)] + #[wire(id = 6)] async fn continue_head( &self, _cx: &CallContext, @@ -273,7 +273,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "stopHeadOperation failed:", result); /// console.log("operation stopped"); /// ``` - #[wire(request_id = 16)] + #[wire(id = 7)] async fn stop_head_operation( &self, _cx: &CallContext, @@ -295,7 +295,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getSpecGenesisHash failed:", result); /// console.log("genesis hash:", result.value); /// ``` - #[wire(request_id = 18)] + #[wire(id = 8)] async fn get_spec_genesis_hash( &self, _cx: &CallContext, @@ -317,7 +317,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getSpecChainName failed:", result); /// console.log("chain name:", result.value); /// ``` - #[wire(request_id = 20)] + #[wire(id = 9)] async fn get_spec_chain_name( &self, _cx: &CallContext, @@ -338,7 +338,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "getSpecProperties failed:", result); /// console.log("chain properties:", result.value); /// ``` - #[wire(request_id = 22)] + #[wire(id = 10)] async fn get_spec_properties( &self, _cx: &CallContext, @@ -360,7 +360,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "broadcastTransaction failed:", result); /// console.log("transaction broadcast:", result.value); /// ``` - #[wire(request_id = 24)] + #[wire(id = 11)] async fn broadcast_transaction( &self, _cx: &CallContext, @@ -395,7 +395,7 @@ pub trait Chain: Send + Sync { /// assert(result.isOk(), "stopTransaction failed:", result); /// console.log("transaction broadcast stopped"); /// ``` - #[wire(request_id = 26)] + #[wire(id = 12)] async fn stop_transaction( &self, _cx: &CallContext, @@ -416,7 +416,7 @@ pub trait Chain: Send + Sync { /// console.log("network:", result.value.network); /// console.log("asset hub genesis:", result.value.genesisHash); /// ``` - #[wire(request_id = 166)] + #[wire(id = 13)] async fn get_chain_info( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/chat.rs b/rust/crates/truapi/src/api/chat.rs index 40a08394f..7bb591cca 100644 --- a/rust/crates/truapi/src/api/chat.rs +++ b/rust/crates/truapi/src/api/chat.rs @@ -26,7 +26,7 @@ pub trait Chat: Send + Sync { /// assert(result.isOk(), "createRoom failed:", result); /// console.log("room created:", result.value); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn create_room( &self, _cx: &CallContext, @@ -46,7 +46,7 @@ pub trait Chat: Send + Sync { /// assert(result.isOk(), "registerBot failed:", result); /// console.log("bot registered:", result.value); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn register_bot( &self, _cx: &CallContext, @@ -65,7 +65,7 @@ pub trait Chat: Send + Sync { /// ); /// console.log("room list received:", item); /// ``` - #[wire(start_id = 4)] + #[wire(id = 2)] async fn list_subscribe(&self, _cx: &CallContext) -> Subscription { Subscription::empty() } @@ -92,7 +92,7 @@ pub trait Chat: Send + Sync { /// assert(result.isOk(), "postMessage failed:", result); /// console.log("message posted:", result.value); /// ``` - #[wire(request_id = 8)] + #[wire(id = 3)] async fn post_message( &self, _cx: &CallContext, @@ -111,7 +111,7 @@ pub trait Chat: Send + Sync { /// ); /// console.log("action received:", item); /// ``` - #[wire(start_id = 10)] + #[wire(id = 4)] async fn action_subscribe( &self, _cx: &CallContext, @@ -127,7 +127,7 @@ pub trait Chat: Send + Sync { /// return of({ tag: "String", value: { text: `${messageType}: ${payload}` } }); /// }); /// ``` - #[wire(host_initiated, start_id = 14)] + #[wire(host_initiated, id = 5)] fn custom_message_render( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/coin_payment.rs b/rust/crates/truapi/src/api/coin_payment.rs index 9fc424c97..47bf97217 100644 --- a/rust/crates/truapi/src/api/coin_payment.rs +++ b/rust/crates/truapi/src/api/coin_payment.rs @@ -34,7 +34,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "createPurse failed:", result); /// console.log("purse created:", result.value.purse); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn create_purse( &self, _cx: &CallContext, @@ -51,7 +51,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "queryPurse failed:", result); /// console.log("purse info:", result.value.info); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn query_purse( &self, _cx: &CallContext, @@ -74,7 +74,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("rebalance status:", status); /// ``` - #[wire(start_id = 4)] + #[wire(id = 2)] async fn rebalance_purse( &self, _cx: &CallContext, @@ -100,7 +100,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("delete status:", status); /// ``` - #[wire(start_id = 8)] + #[wire(id = 3)] async fn delete_purse( &self, _cx: &CallContext, @@ -119,7 +119,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "createReceivable failed:", result); /// console.log("receivable created:", result.value.receivable); /// ``` - #[wire(request_id = 12)] + #[wire(id = 4)] async fn create_receivable( &self, _cx: &CallContext, @@ -142,7 +142,7 @@ pub trait CoinPayment: Send + Sync { /// assert(result.isOk(), "createCheque failed:", result); /// console.log("cheque created:", result.value.cheque); /// ``` - #[wire(request_id = 14)] + #[wire(id = 5)] async fn create_cheque( &self, _cx: &CallContext, @@ -169,7 +169,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("deposit status:", status); /// ``` - #[wire(start_id = 16)] + #[wire(id = 6)] async fn deposit( &self, _cx: &CallContext, @@ -196,7 +196,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("refund status:", status); /// ``` - #[wire(start_id = 20)] + #[wire(id = 7)] async fn refund( &self, _cx: &CallContext, @@ -223,7 +223,7 @@ pub trait CoinPayment: Send + Sync { /// ); /// console.log("payment received:", item); /// ``` - #[wire(start_id = 24)] + #[wire(id = 8)] async fn listen_for_payment( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/entropy.rs b/rust/crates/truapi/src/api/entropy.rs index 5cf7a17ab..882acd7db 100644 --- a/rust/crates/truapi/src/api/entropy.rs +++ b/rust/crates/truapi/src/api/entropy.rs @@ -19,7 +19,7 @@ pub trait Entropy: Send + Sync { /// assert(result.isOk(), "derive failed:", result); /// console.log("entropy derived:", result.value); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn derive( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/local_storage.rs b/rust/crates/truapi/src/api/local_storage.rs index 5f9107118..299aa3f2e 100644 --- a/rust/crates/truapi/src/api/local_storage.rs +++ b/rust/crates/truapi/src/api/local_storage.rs @@ -19,7 +19,7 @@ pub trait LocalStorage: Send + Sync { /// assert(result.isOk(), "read failed:", result); /// console.log("storage value read:", result.value.value); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn read( &self, cx: &CallContext, @@ -36,7 +36,7 @@ pub trait LocalStorage: Send + Sync { /// assert(result.isOk(), "write failed:", result); /// console.log("storage write succeeded"); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn write( &self, cx: &CallContext, @@ -50,7 +50,7 @@ pub trait LocalStorage: Send + Sync { /// assert(result.isOk(), "clear failed:", result); /// console.log("storage clear succeeded"); /// ``` - #[wire(request_id = 4)] + #[wire(id = 2)] async fn clear( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/locale.rs b/rust/crates/truapi/src/api/locale.rs index 4051bb45e..8009b2389 100644 --- a/rust/crates/truapi/src/api/locale.rs +++ b/rust/crates/truapi/src/api/locale.rs @@ -18,7 +18,7 @@ pub trait Locale: Send + Sync { /// ); /// console.log("locale received:", locale.languageTag); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn subscribe(&self, _cx: &CallContext) -> Subscription { Subscription::empty() } diff --git a/rust/crates/truapi/src/api/notifications.rs b/rust/crates/truapi/src/api/notifications.rs index e0b7e58ff..707189682 100644 --- a/rust/crates/truapi/src/api/notifications.rs +++ b/rust/crates/truapi/src/api/notifications.rs @@ -29,7 +29,7 @@ pub trait Notifications: Send + Sync { /// assert(result.isOk(), "sendPushNotification failed:", result); /// console.log("notification sent:", result.value); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn send_push_notification( &self, cx: &CallContext, @@ -50,7 +50,7 @@ pub trait Notifications: Send + Sync { /// assert(result.isOk(), "cancelPushNotification failed:", result); /// console.log("notification cancelled"); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn cancel_push_notification( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/payment.rs b/rust/crates/truapi/src/api/payment.rs index 94f8ff12a..40744d1dc 100644 --- a/rust/crates/truapi/src/api/payment.rs +++ b/rust/crates/truapi/src/api/payment.rs @@ -24,7 +24,7 @@ pub trait Payment: Send + Sync { /// ); /// console.log("balance received:", balance); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn balance_subscribe( &self, _cx: &CallContext, @@ -54,7 +54,7 @@ pub trait Payment: Send + Sync { /// assert(result.isOk(), "request failed:", result); /// console.log("payment requested:", result.value); /// ``` - #[wire(request_id = 6)] + #[wire(id = 2)] async fn request( &self, _cx: &CallContext, @@ -91,7 +91,7 @@ pub trait Payment: Send + Sync { /// ); /// console.log("payment status received:", status); /// ``` - #[wire(start_id = 8)] + #[wire(id = 3)] async fn status_subscribe( &self, _cx: &CallContext, @@ -113,7 +113,7 @@ pub trait Payment: Send + Sync { /// assert(result.isOk(), "topUp failed:", result); /// console.log("balance topped up"); /// ``` - #[wire(request_id = 4)] + #[wire(id = 1)] async fn top_up( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/permissions.rs b/rust/crates/truapi/src/api/permissions.rs index 19b9c022e..996951466 100644 --- a/rust/crates/truapi/src/api/permissions.rs +++ b/rust/crates/truapi/src/api/permissions.rs @@ -18,7 +18,7 @@ pub trait Permissions: Send + Sync { /// assert(result.isOk(), "requestDevicePermission failed:", result); /// console.log("device permission result:", result.value); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn request_device_permission( &self, cx: &CallContext, @@ -34,7 +34,7 @@ pub trait Permissions: Send + Sync { /// assert(result.isOk(), "requestRemotePermission failed:", result); /// console.log("remote permission result:", result.value); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn request_remote_permission( &self, cx: &CallContext, diff --git a/rust/crates/truapi/src/api/preimage.rs b/rust/crates/truapi/src/api/preimage.rs index 98a627fdf..f178fcc5c 100644 --- a/rust/crates/truapi/src/api/preimage.rs +++ b/rust/crates/truapi/src/api/preimage.rs @@ -27,7 +27,7 @@ pub trait Preimage: Send + Sync { /// assert(item.value === value, "preimage lookup returned the wrong value:", item); /// console.log("preimage lookup received:", item); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn lookup_subscribe( &self, _cx: &CallContext, @@ -44,7 +44,7 @@ pub trait Preimage: Send + Sync { /// assert(result.isOk(), "submit failed:", result); /// console.log("preimage submitted:", result.value); /// ``` - #[wire(request_id = 4)] + #[wire(id = 1)] async fn submit( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/resource_allocation.rs b/rust/crates/truapi/src/api/resource_allocation.rs index e303f0438..a0ae1cb4a 100644 --- a/rust/crates/truapi/src/api/resource_allocation.rs +++ b/rust/crates/truapi/src/api/resource_allocation.rs @@ -39,7 +39,7 @@ pub trait ResourceAllocation: Send + Sync { /// ); /// console.log("resource allocation outcomes:", result.value.outcomes); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn request( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 1fbaf9ff9..8b8e5e3de 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -62,7 +62,7 @@ pub trait Signing: Send + Sync { /// console.log(`${version} transaction created:`, result.value); /// } /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn create_transaction( &self, _cx: &CallContext, @@ -119,7 +119,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "createTransactionWithLegacyAccount failed:", result); /// console.log("transaction created:", result.value); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn create_transaction_with_legacy_account( &self, _cx: &CallContext, @@ -151,7 +151,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signRawWithLegacyAccount failed:", result); /// console.log("raw bytes signed:", result.value); /// ``` - #[wire(request_id = 4)] + #[wire(id = 2)] async fn sign_raw_with_legacy_account( &self, _cx: &CallContext, @@ -197,7 +197,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signPayloadWithLegacyAccount failed:", result); /// console.log("payload signed:", result.value); /// ``` - #[wire(request_id = 6)] + #[wire(id = 3)] async fn sign_payload_with_legacy_account( &self, _cx: &CallContext, @@ -227,7 +227,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signRaw failed:", result); /// console.log("raw bytes signed:", result.value); /// ``` - #[wire(request_id = 8)] + #[wire(id = 4)] async fn sign_raw( &self, _cx: &CallContext, @@ -264,7 +264,7 @@ pub trait Signing: Send + Sync { /// assert(result.isOk(), "signPayload failed:", result); /// console.log("payload signed:", result.value); /// ``` - #[wire(request_id = 10)] + #[wire(id = 5)] async fn sign_payload( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 3f739e554..83a3fb0e0 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -58,7 +58,7 @@ pub trait StatementStore: Send + Sync { /// const page = await waitForStatement(); /// console.log("subscribe received", page); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn subscribe( &self, _cx: &CallContext, @@ -100,7 +100,7 @@ pub trait StatementStore: Send + Sync { /// console.log("proof created:", result.value); /// } /// ``` - #[wire(request_id = 4)] + #[wire(id = 1)] async fn create_proof( &self, _cx: &CallContext, @@ -127,7 +127,7 @@ pub trait StatementStore: Send + Sync { /// assert(result.isOk(), "createProof failed:", result); /// console.log("proof created:", result.value); /// ``` - #[wire(request_id = 8)] + #[wire(id = 3)] async fn create_proof_authorized( &self, _cx: &CallContext, @@ -159,7 +159,7 @@ pub trait StatementStore: Send + Sync { /// assert(result.isOk(), "submit failed:", result); /// console.log("statement submitted"); /// ``` - #[wire(request_id = 6)] + #[wire(id = 2)] async fn submit( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 58e48ffd0..632098f7d 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -22,7 +22,7 @@ pub trait System: Send + Sync { /// assert(result.isOk(), "handshake failed:", result); /// console.log("handshake succeeded"); /// ``` - #[wire(request_id = 0)] + #[wire(id = 0)] async fn handshake( &self, _cx: &CallContext, @@ -53,7 +53,7 @@ pub trait System: Send + Sync { /// assert(result.isOk(), "featureSupported failed:", result); /// console.log("feature supported:", result.value.supported); /// ``` - #[wire(request_id = 2)] + #[wire(id = 1)] async fn feature_supported( &self, cx: &CallContext, @@ -76,7 +76,7 @@ pub trait System: Send + Sync { /// assert(result.isOk(), "navigateTo failed:", result); /// console.log("navigation succeeded"); /// ``` - #[wire(request_id = 4)] + #[wire(id = 2)] async fn navigate_to( &self, cx: &CallContext, @@ -96,7 +96,7 @@ pub trait System: Send + Sync { /// const info = result.value; /// console.log(`${info.name} ${info.version} on ${info.platform}`); /// ``` - #[wire(request_id = 6)] + #[wire(id = 3)] async fn host_info( &self, cx: &CallContext, @@ -110,7 +110,7 @@ pub trait System: Send + Sync { /// assert(context.isOk(), "getProductContext failed:", context); /// console.log("product id:", context.value.productId); /// ``` - #[wire(request_id = 8)] + #[wire(id = 4)] async fn get_product_context( &self, _cx: &CallContext, diff --git a/rust/crates/truapi/src/api/theme.rs b/rust/crates/truapi/src/api/theme.rs index 07c41ca52..48c15dc83 100644 --- a/rust/crates/truapi/src/api/theme.rs +++ b/rust/crates/truapi/src/api/theme.rs @@ -18,7 +18,7 @@ pub trait Theme: Send + Sync { /// ); /// console.log("theme received:", theme); /// ``` - #[wire(start_id = 0)] + #[wire(id = 0)] async fn subscribe(&self, _cx: &CallContext) -> Subscription { Subscription::empty() } diff --git a/rust/crates/truapi/src/versioned.rs b/rust/crates/truapi/src/versioned.rs index 4d5e37c26..10a52a31c 100644 --- a/rust/crates/truapi/src/versioned.rs +++ b/rust/crates/truapi/src/versioned.rs @@ -4,7 +4,12 @@ //! successive versions of one logical message, newest last. A server normalizes //! incoming values to [`Versioned::Latest`] with [`IntoLatest`], handles them in //! latest terms, then maps results back to the caller's version with -//! [`FromLatest`]. The envelopes themselves are generated by `versioned_type!`. +//! [`FromLatest`]. The envelopes themselves are generated by `versioned_type!`, +//! and each variant wraps either [`Request`] or [`Subscription`] rather than a +//! bare payload: version selects a method's shape, direction is carried inside +//! that version's payload rather than addressed by a separate wire id. + +use parity_scale_codec::{Decode, Encode}; /// A versioned message envelope. pub trait Versioned: Sized { @@ -30,6 +35,32 @@ pub trait FromLatest: Versioned { fn from_latest(latest: Self::Latest, target: u8) -> Self; } +/// Direction tag for a request/response method: which half of the exchange +/// this frame carries. Wrapped inside a method's version enum, so a method's +/// version history is the one place its shape can change. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum Request { + /// Product-to-host call. + Request(Req), + /// Host-to-product reply. + Response(Res), +} + +/// Direction tag for a subscription method: which half of the four-frame +/// exchange this frame carries. `Stop` carries no payload; `Interrupt` carries +/// `None` for natural stream completion or `Some(err)` for a failure. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum Subscription { + /// Product-to-host subscription request. + Start(Start), + /// Product-to-host cancellation. + Stop, + /// Host-to-product termination: `None` on clean completion, `Some` on failure. + Interrupt(Option), + /// Host-to-product streamed item. + Receive(Item), +} + pub mod account; pub mod chain; pub mod chat; @@ -115,4 +146,67 @@ mod tests { .expect("decode"); assert_eq!(original, decoded); } + + #[test] + fn request_direction_tag_roundtrips_both_variants() { + use super::Request; + + let request = Request::::Request(7); + let decoded = Request::::decode(&mut &request.encode()[..]).expect("decode"); + assert_eq!(request, decoded); + assert_eq!(request.encode()[0], 0, "Request must encode discriminant 0"); + + let response = Request::::Response("ok".to_string()); + let decoded = Request::::decode(&mut &response.encode()[..]).expect("decode"); + assert_eq!(response, decoded); + assert_eq!( + response.encode()[0], + 1, + "Response must encode discriminant 1" + ); + } + + #[test] + fn subscription_direction_tag_roundtrips_every_variant() { + use super::Subscription; + + let start = Subscription::::Start(7); + assert_eq!(start.encode()[0], 0, "Start must encode discriminant 0"); + assert_eq!( + Subscription::::decode(&mut &start.encode()[..]).expect("decode"), + start + ); + + let stop = Subscription::::Stop; + assert_eq!(stop.encode()[0], 1, "Stop must encode discriminant 1"); + assert_eq!( + Subscription::::decode(&mut &stop.encode()[..]).expect("decode"), + stop + ); + + let clean_end = Subscription::::Interrupt(None); + assert_eq!( + clean_end.encode()[0], + 2, + "Interrupt must encode discriminant 2" + ); + assert_eq!( + Subscription::::decode(&mut &clean_end.encode()[..]) + .expect("decode"), + clean_end + ); + + let failed = Subscription::::Interrupt(Some("boom".to_string())); + assert_eq!( + Subscription::::decode(&mut &failed.encode()[..]).expect("decode"), + failed + ); + + let item = Subscription::::Receive("hello".to_string()); + assert_eq!(item.encode()[0], 3, "Receive must encode discriminant 3"); + assert_eq!( + Subscription::::decode(&mut &item.encode()[..]).expect("decode"), + item + ); + } } diff --git a/rust/crates/truapi/src/versioned/account.rs b/rust/crates/truapi/src/versioned/account.rs index fd0bfd607..c351ac0df 100644 --- a/rust/crates/truapi/src/versioned/account.rs +++ b/rust/crates/truapi/src/versioned/account.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`Account`](crate::api::Account) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum HostAccountGetRequest { V1 => v01::HostAccountGetRequest } @@ -34,4 +37,76 @@ truapi_macros::versioned_type! { pub enum HostGetUserIdRequest { V1 } pub enum HostGetUserIdResponse { V1 => v01::HostGetUserIdResponse } pub enum HostGetUserIdError { V1 => v01::HostGetUserIdError } + + /// Wire-envelope version for + /// [`Account::connection_status_subscribe`](crate::api::Account::connection_status_subscribe). + /// Used only by the generated dispatcher/client. + pub enum HostAccountConnectionStatusSubscribeVersion { + V1 => SubscriptionEnvelope<(), v01::HostAccountConnectionStatusSubscribeItem, CallError>, + } + + /// Wire-envelope version for [`Account::get_account`](crate::api::Account::get_account). + /// Used only by the generated dispatcher/client. + pub enum HostAccountGetVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Account::get_account_alias`](crate::api::Account::get_account_alias). + /// Used only by the generated dispatcher/client. + pub enum HostAccountGetAliasVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Account::create_account_proof`](crate::api::Account::create_account_proof). + /// Used only by the generated dispatcher/client. + pub enum HostAccountCreateProofVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Account::get_legacy_accounts`](crate::api::Account::get_legacy_accounts). + /// Used only by the generated dispatcher/client. + pub enum HostGetLegacyAccountsVersion { + V1 => RequestEnvelope<(), Result>>, + } + + /// Wire-envelope version for [`Account::get_user_id`](crate::api::Account::get_user_id). + /// Used only by the generated dispatcher/client. + pub enum HostGetUserIdVersion { + V1 => RequestEnvelope<(), Result>>, + } + + /// Wire-envelope version for [`Account::request_login`](crate::api::Account::request_login). + /// Used only by the generated dispatcher/client. + pub enum HostRequestLoginVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Account::sign_vrf`](crate::api::Account::sign_vrf). + /// Used only by the generated dispatcher/client. + pub enum HostAccountSignVrfVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Account::register_ring_vrf_key`](crate::api::Account::register_ring_vrf_key). + /// Used only by the generated dispatcher/client. + pub enum HostAccountRegisterRingVrfKeyVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Account::list_ring_vrf_keys`](crate::api::Account::list_ring_vrf_keys). + /// Used only by the generated dispatcher/client. + pub enum HostAccountListRingVrfKeysVersion { + V1 => RequestEnvelope, CallError>>, + } + + /// Wire-envelope version for [`Account::ring_vrf_sign`](crate::api::Account::ring_vrf_sign). + /// Used only by the generated dispatcher/client. + pub enum HostAccountRingVrfSignVersion { + V1 => RequestEnvelope, CallError>>, + } } diff --git a/rust/crates/truapi/src/versioned/chain.rs b/rust/crates/truapi/src/versioned/chain.rs index d96275729..a4aa64f30 100644 --- a/rust/crates/truapi/src/versioned/chain.rs +++ b/rust/crates/truapi/src/versioned/chain.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`Chain`](crate::api::Chain) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum RemoteChainHeadFollowRequest { V1 => v01::RemoteChainHeadFollowRequest } @@ -44,4 +47,94 @@ truapi_macros::versioned_type! { pub enum RemoteChainInfoRequest { V1 => v01::RemoteChainInfoRequest } pub enum RemoteChainInfoResponse { V1 => v01::RemoteChainInfoResponse } pub enum RemoteChainInfoError { V1 => v01::RemoteChainInfoError } + + /// Wire-envelope version for + /// [`Chain::follow_head_subscribe`](crate::api::Chain::follow_head_subscribe). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadFollowVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for [`Chain::get_head_header`](crate::api::Chain::get_head_header). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadHeaderVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::get_head_body`](crate::api::Chain::get_head_body). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadBodyVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::get_head_storage`](crate::api::Chain::get_head_storage). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadStorageVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::call_head`](crate::api::Chain::call_head). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadCallVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::unpin_head`](crate::api::Chain::unpin_head). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadUnpinVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::continue_head`](crate::api::Chain::continue_head). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadContinueVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Chain::stop_head_operation`](crate::api::Chain::stop_head_operation). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainHeadStopOperationVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Chain::get_spec_genesis_hash`](crate::api::Chain::get_spec_genesis_hash). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainSpecGenesisHashVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Chain::get_spec_chain_name`](crate::api::Chain::get_spec_chain_name). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainSpecChainNameVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Chain::get_spec_properties`](crate::api::Chain::get_spec_properties). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainSpecPropertiesVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Chain::broadcast_transaction`](crate::api::Chain::broadcast_transaction). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainTransactionBroadcastVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::stop_transaction`](crate::api::Chain::stop_transaction). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainTransactionStopVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chain::get_chain_info`](crate::api::Chain::get_chain_info). + /// Used only by the generated dispatcher/client. + pub enum RemoteChainInfoVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/chat.rs b/rust/crates/truapi/src/versioned/chat.rs index 562a9bae7..92729dfee 100644 --- a/rust/crates/truapi/src/versioned/chat.rs +++ b/rust/crates/truapi/src/versioned/chat.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`Chat`](crate::api::Chat) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum HostChatCreateRoomRequest { V1 => v01::HostChatCreateRoomRequest } @@ -16,6 +19,43 @@ truapi_macros::versioned_type! { pub enum HostChatActionSubscribeItem { V1 => v01::HostChatActionSubscribeItem } pub enum ProductChatCustomMessageRenderRequest { V1 => v01::ProductChatCustomMessageRenderRequest } pub enum ProductChatCustomMessageRenderItem { V1 => v01::CustomRendererNode } + + /// Wire-envelope version for [`Chat::create_room`](crate::api::Chat::create_room). + /// Used only by the generated dispatcher/client. + pub enum HostChatCreateRoomVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chat::register_bot`](crate::api::Chat::register_bot). + /// Used only by the generated dispatcher/client. + pub enum HostChatRegisterBotVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chat::list_subscribe`](crate::api::Chat::list_subscribe). + /// Used only by the generated dispatcher/client. + pub enum HostChatListSubscribeVersion { + V1 => SubscriptionEnvelope<(), v01::HostChatListSubscribeItem, CallError>, + } + + /// Wire-envelope version for [`Chat::post_message`](crate::api::Chat::post_message). + /// Used only by the generated dispatcher/client. + pub enum HostChatPostMessageVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Chat::action_subscribe`](crate::api::Chat::action_subscribe). + /// Used only by the generated dispatcher/client. + pub enum HostChatActionSubscribeVersion { + V1 => SubscriptionEnvelope<(), v01::HostChatActionSubscribeItem, CallError>, + } + + /// Wire-envelope version for + /// [`Chat::custom_message_render`](crate::api::Chat::custom_message_render). + /// Used only by the generated dispatcher/client. + pub enum ProductChatCustomMessageRenderVersion { + V1 => SubscriptionEnvelope>, + } } #[cfg(test)] diff --git a/rust/crates/truapi/src/versioned/coin_payment.rs b/rust/crates/truapi/src/versioned/coin_payment.rs index 3e488a7f9..18aac4e59 100644 --- a/rust/crates/truapi/src/versioned/coin_payment.rs +++ b/rust/crates/truapi/src/versioned/coin_payment.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`CoinPayment`](crate::api::CoinPayment) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum HostCoinPaymentCreatePurseRequest { V1 => v01::HostCoinPaymentCreatePurseRequest } @@ -30,4 +33,65 @@ truapi_macros::versioned_type! { pub enum HostCoinPaymentListenForRequest { V1 => v01::HostCoinPaymentListenForRequest } pub enum HostCoinPaymentListenForItem { V1 => v01::HostCoinPaymentListenForItem } pub enum HostCoinPaymentListenForError { V1 => v01::CoinPaymentError } + + /// Wire-envelope version for + /// [`CoinPayment::create_purse`](crate::api::CoinPayment::create_purse). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentCreatePurseVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`CoinPayment::query_purse`](crate::api::CoinPayment::query_purse). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentQueryPurseVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`CoinPayment::rebalance_purse`](crate::api::CoinPayment::rebalance_purse). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentRebalancePurseVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for + /// [`CoinPayment::delete_purse`](crate::api::CoinPayment::delete_purse). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentDeletePurseVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for + /// [`CoinPayment::create_receivable`](crate::api::CoinPayment::create_receivable). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentCreateReceivableVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`CoinPayment::create_cheque`](crate::api::CoinPayment::create_cheque). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentCreateChequeVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`CoinPayment::deposit`](crate::api::CoinPayment::deposit). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentDepositVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for [`CoinPayment::refund`](crate::api::CoinPayment::refund). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentRefundVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for + /// [`CoinPayment::listen_for_payment`](crate::api::CoinPayment::listen_for_payment). + /// Used only by the generated dispatcher/client. + pub enum HostCoinPaymentListenForVersion { + V1 => SubscriptionEnvelope>, + } } diff --git a/rust/crates/truapi/src/versioned/entropy.rs b/rust/crates/truapi/src/versioned/entropy.rs index 5027e4f03..530732d2d 100644 --- a/rust/crates/truapi/src/versioned/entropy.rs +++ b/rust/crates/truapi/src/versioned/entropy.rs @@ -1,9 +1,17 @@ //! Versioned wrappers for [`Entropy`](crate::api::Entropy) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { pub enum HostDeriveEntropyRequest { V1 => v01::HostDeriveEntropyRequest } pub enum HostDeriveEntropyResponse { V1 => v01::HostDeriveEntropyResponse } pub enum HostDeriveEntropyError { V1 => v01::HostDeriveEntropyError } + + /// Wire-envelope version for [`Entropy::derive`](crate::api::Entropy::derive). + /// Used only by the generated dispatcher/client. + pub enum HostDeriveEntropyVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/local_storage.rs b/rust/crates/truapi/src/versioned/local_storage.rs index 708eb4c7b..932c86c77 100644 --- a/rust/crates/truapi/src/versioned/local_storage.rs +++ b/rust/crates/truapi/src/versioned/local_storage.rs @@ -1,6 +1,8 @@ //! Versioned wrappers for [`LocalStorage`](crate::api::LocalStorage) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { pub enum HostLocalStorageReadRequest { V1 => v01::HostLocalStorageReadRequest } @@ -12,4 +14,22 @@ truapi_macros::versioned_type! { pub enum HostLocalStorageClearRequest { V1 => v01::HostLocalStorageClearRequest } pub enum HostLocalStorageClearResponse { V1 } pub enum HostLocalStorageClearError { V1 => v01::HostLocalStorageReadError } + + /// Wire-envelope version for [`LocalStorage::read`](crate::api::LocalStorage::read). + /// Used only by the generated dispatcher/client. + pub enum HostLocalStorageReadVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`LocalStorage::write`](crate::api::LocalStorage::write). + /// Used only by the generated dispatcher/client. + pub enum HostLocalStorageWriteVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`LocalStorage::clear`](crate::api::LocalStorage::clear). + /// Used only by the generated dispatcher/client. + pub enum HostLocalStorageClearVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/locale.rs b/rust/crates/truapi/src/versioned/locale.rs index 29e903743..1fbae9eca 100644 --- a/rust/crates/truapi/src/versioned/locale.rs +++ b/rust/crates/truapi/src/versioned/locale.rs @@ -1,7 +1,18 @@ //! Versioned wrappers for [`Locale`](crate::api::Locale) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum HostLocaleSubscribeItem { V1 => v01::HostLocaleSubscribeItem } } + +truapi_macros::versioned_type! { + /// Wire-envelope version for [`Locale::subscribe`](crate::api::Locale::subscribe). + /// Used only by the generated dispatcher/client — trait signatures keep + /// naming [`HostLocaleSubscribeItem`] directly. + pub enum HostLocaleSubscribeVersion { + V1 => SubscriptionEnvelope<(), v01::HostLocaleSubscribeItem, CallError>, + } +} diff --git a/rust/crates/truapi/src/versioned/notifications.rs b/rust/crates/truapi/src/versioned/notifications.rs index 3a8b7c5e5..c6859718b 100644 --- a/rust/crates/truapi/src/versioned/notifications.rs +++ b/rust/crates/truapi/src/versioned/notifications.rs @@ -1,6 +1,8 @@ //! Versioned wrappers for [`Notifications`](crate::api::Notifications) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { pub enum HostPushNotificationRequest { V1 => v01::HostPushNotificationRequest } @@ -9,4 +11,18 @@ truapi_macros::versioned_type! { pub enum HostPushNotificationCancelRequest { V1 => v01::HostPushNotificationCancelRequest } pub enum HostPushNotificationCancelResponse { V1 } pub enum HostPushNotificationCancelError { V1 => v01::GenericError } + + /// Wire-envelope version for + /// [`Notifications::send_push_notification`](crate::api::Notifications::send_push_notification). + /// Used only by the generated dispatcher/client. + pub enum HostPushNotificationVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Notifications::cancel_push_notification`](crate::api::Notifications::cancel_push_notification). + /// Used only by the generated dispatcher/client. + pub enum HostPushNotificationCancelVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/payment.rs b/rust/crates/truapi/src/versioned/payment.rs index d252cf4ec..345aeb566 100644 --- a/rust/crates/truapi/src/versioned/payment.rs +++ b/rust/crates/truapi/src/versioned/payment.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`Payment`](crate::api::Payment) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum HostPaymentBalanceSubscribeRequest { V1 => v01::HostPaymentBalanceSubscribeRequest } @@ -15,4 +18,30 @@ truapi_macros::versioned_type! { pub enum HostPaymentStatusSubscribeRequest { V1 => v01::HostPaymentStatusSubscribeRequest } pub enum HostPaymentStatusSubscribeItem { V1 => v01::HostPaymentStatusSubscribeItem } pub enum HostPaymentStatusSubscribeError { V1 => v01::HostPaymentStatusSubscribeError } + + /// Wire-envelope version for + /// [`Payment::balance_subscribe`](crate::api::Payment::balance_subscribe). + /// Used only by the generated dispatcher/client. + pub enum HostPaymentBalanceSubscribeVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for [`Payment::top_up`](crate::api::Payment::top_up). + /// Used only by the generated dispatcher/client. + pub enum HostPaymentTopUpVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Payment::request`](crate::api::Payment::request). + /// Used only by the generated dispatcher/client. + pub enum HostPaymentVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Payment::status_subscribe`](crate::api::Payment::status_subscribe). + /// Used only by the generated dispatcher/client. + pub enum HostPaymentStatusSubscribeVersion { + V1 => SubscriptionEnvelope>, + } } diff --git a/rust/crates/truapi/src/versioned/permissions.rs b/rust/crates/truapi/src/versioned/permissions.rs index 721ce5702..bcae07d6b 100644 --- a/rust/crates/truapi/src/versioned/permissions.rs +++ b/rust/crates/truapi/src/versioned/permissions.rs @@ -1,6 +1,8 @@ //! Versioned wrappers for [`Permissions`](crate::api::Permissions) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { #[derive(derive_more::Display)] @@ -13,4 +15,18 @@ truapi_macros::versioned_type! { pub enum RemotePermissionRequest { V1 => v01::RemotePermissionRequest } pub enum RemotePermissionResponse { V1 => v01::RemotePermissionResponse } pub enum RemotePermissionError { V1 => v01::GenericError } + + /// Wire-envelope version for + /// [`Permissions::request_device_permission`](crate::api::Permissions::request_device_permission). + /// Used only by the generated dispatcher/client. + pub enum HostDevicePermissionVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Permissions::request_remote_permission`](crate::api::Permissions::request_remote_permission). + /// Used only by the generated dispatcher/client. + pub enum RemotePermissionVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/preimage.rs b/rust/crates/truapi/src/versioned/preimage.rs index 61bf4b2f3..dd21a992e 100644 --- a/rust/crates/truapi/src/versioned/preimage.rs +++ b/rust/crates/truapi/src/versioned/preimage.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`Preimage`](crate::api::Preimage) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum RemotePreimageLookupSubscribeRequest { V1 => v01::RemotePreimageLookupSubscribeRequest } @@ -8,4 +11,17 @@ truapi_macros::versioned_type! { pub enum RemotePreimageSubmitRequest { V1 => Vec } pub enum RemotePreimageSubmitResponse { V1 => Vec } pub enum RemotePreimageSubmitError { V1 => v01::PreimageSubmitError } + + /// Wire-envelope version for + /// [`Preimage::lookup_subscribe`](crate::api::Preimage::lookup_subscribe). + /// Used only by the generated dispatcher/client. + pub enum RemotePreimageLookupSubscribeVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for [`Preimage::submit`](crate::api::Preimage::submit). + /// Used only by the generated dispatcher/client. + pub enum RemotePreimageSubmitVersion { + V1 => RequestEnvelope, Result, CallError>>, + } } diff --git a/rust/crates/truapi/src/versioned/resource_allocation.rs b/rust/crates/truapi/src/versioned/resource_allocation.rs index 7d69c24bf..ba5c05880 100644 --- a/rust/crates/truapi/src/versioned/resource_allocation.rs +++ b/rust/crates/truapi/src/versioned/resource_allocation.rs @@ -1,9 +1,18 @@ //! Versioned wrappers for [`ResourceAllocation`](crate::api::ResourceAllocation) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { pub enum HostRequestResourceAllocationRequest { V1 => v01::HostRequestResourceAllocationRequest } pub enum HostRequestResourceAllocationResponse { V1 => v01::HostRequestResourceAllocationResponse } pub enum HostRequestResourceAllocationError { V1 => v01::ResourceAllocationError } + + /// Wire-envelope version for + /// [`ResourceAllocation::request`](crate::api::ResourceAllocation::request). + /// Used only by the generated dispatcher/client. + pub enum HostRequestResourceAllocationVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/signing.rs b/rust/crates/truapi/src/versioned/signing.rs index 8c9acc6ab..d7b63eb24 100644 --- a/rust/crates/truapi/src/versioned/signing.rs +++ b/rust/crates/truapi/src/versioned/signing.rs @@ -1,6 +1,8 @@ //! Versioned wrappers for [`Signing`](crate::api::Signing) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { pub enum HostSignPayloadRequest { V1 => v01::HostSignPayloadRequest } @@ -21,4 +23,44 @@ truapi_macros::versioned_type! { pub enum HostCreateTransactionWithLegacyAccountRequest { V1 => v01::LegacyAccountTxPayload } pub enum HostCreateTransactionWithLegacyAccountResponse { V1 => v01::HostCreateTransactionWithLegacyAccountResponse } pub enum HostCreateTransactionWithLegacyAccountError { V1 => v01::HostCreateTransactionError } + + /// Wire-envelope version for + /// [`Signing::create_transaction`](crate::api::Signing::create_transaction). + /// Used only by the generated dispatcher/client. + pub enum HostCreateTransactionVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Signing::create_transaction_with_legacy_account`](crate::api::Signing::create_transaction_with_legacy_account). + /// Used only by the generated dispatcher/client. + pub enum HostCreateTransactionWithLegacyAccountVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Signing::sign_raw_with_legacy_account`](crate::api::Signing::sign_raw_with_legacy_account). + /// Used only by the generated dispatcher/client. + pub enum HostSignRawWithLegacyAccountVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`Signing::sign_payload_with_legacy_account`](crate::api::Signing::sign_payload_with_legacy_account). + /// Used only by the generated dispatcher/client. + pub enum HostSignPayloadWithLegacyAccountVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Signing::sign_raw`](crate::api::Signing::sign_raw). + /// Used only by the generated dispatcher/client. + pub enum HostSignRawVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`Signing::sign_payload`](crate::api::Signing::sign_payload). + /// Used only by the generated dispatcher/client. + pub enum HostSignPayloadVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/statement_store.rs b/rust/crates/truapi/src/versioned/statement_store.rs index ed31d47ec..3340c389e 100644 --- a/rust/crates/truapi/src/versioned/statement_store.rs +++ b/rust/crates/truapi/src/versioned/statement_store.rs @@ -1,6 +1,9 @@ //! Versioned wrappers for [`StatementStore`](crate::api::StatementStore) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum RemoteStatementStoreSubscribeRequest { V1 => v01::RemoteStatementStoreSubscribeRequest } @@ -14,4 +17,31 @@ truapi_macros::versioned_type! { pub enum RemoteStatementStoreCreateProofAuthorizedError { V1 => v01::RemoteStatementStoreCreateProofError } pub enum RemoteStatementStoreSubmitRequest { V1 => v01::SignedStatement } pub enum RemoteStatementStoreSubmitError { V1 => v01::GenericError } + + /// Wire-envelope version for + /// [`StatementStore::subscribe`](crate::api::StatementStore::subscribe). + /// Used only by the generated dispatcher/client. + pub enum RemoteStatementStoreSubscribeVersion { + V1 => SubscriptionEnvelope>, + } + + /// Wire-envelope version for + /// [`StatementStore::create_proof`](crate::api::StatementStore::create_proof). + /// Used only by the generated dispatcher/client. + pub enum RemoteStatementStoreCreateProofVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`StatementStore::submit`](crate::api::StatementStore::submit). + /// Used only by the generated dispatcher/client. + pub enum RemoteStatementStoreSubmitVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`StatementStore::create_proof_authorized`](crate::api::StatementStore::create_proof_authorized). + /// Used only by the generated dispatcher/client. + pub enum RemoteStatementStoreCreateProofAuthorizedVersion { + V1 => RequestEnvelope>>, + } } diff --git a/rust/crates/truapi/src/versioned/system.rs b/rust/crates/truapi/src/versioned/system.rs index 2e7cc4dcc..5078f5af5 100644 --- a/rust/crates/truapi/src/versioned/system.rs +++ b/rust/crates/truapi/src/versioned/system.rs @@ -1,6 +1,8 @@ //! Versioned wrappers for [`System`](crate::api::System) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Request as RequestEnvelope; truapi_macros::versioned_type! { pub enum HostHandshakeRequest { V1 => v01::HostHandshakeRequest } @@ -18,4 +20,36 @@ truapi_macros::versioned_type! { pub enum HostGetProductContextRequest { V1 } pub enum HostGetProductContextResponse { V1 => v01::HostGetProductContextResponse } pub enum HostGetProductContextError { V1 => v01::GenericError } + + /// Wire-envelope version for [`System::handshake`](crate::api::System::handshake). + /// Used only by the generated dispatcher/client. + pub enum HostHandshakeVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for + /// [`System::feature_supported`](crate::api::System::feature_supported). + /// Used only by the generated dispatcher/client. + pub enum HostFeatureSupportedVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`System::navigate_to`](crate::api::System::navigate_to). + /// Used only by the generated dispatcher/client. + pub enum HostNavigateToVersion { + V1 => RequestEnvelope>>, + } + + /// Wire-envelope version for [`System::host_info`](crate::api::System::host_info). + /// Used only by the generated dispatcher/client. + pub enum HostInfoVersion { + V1 => RequestEnvelope<(), Result>>, + } + + /// Wire-envelope version for + /// [`System::get_product_context`](crate::api::System::get_product_context). + /// Used only by the generated dispatcher/client. + pub enum HostGetProductContextVersion { + V1 => RequestEnvelope<(), Result>>, + } } diff --git a/rust/crates/truapi/src/versioned/theme.rs b/rust/crates/truapi/src/versioned/theme.rs index cac165eaa..cdfba1351 100644 --- a/rust/crates/truapi/src/versioned/theme.rs +++ b/rust/crates/truapi/src/versioned/theme.rs @@ -1,7 +1,15 @@ //! Versioned wrappers for [`Theme`](crate::api::Theme) methods. +use crate::CallError; use crate::v01; +use crate::versioned::Subscription as SubscriptionEnvelope; truapi_macros::versioned_type! { pub enum HostThemeSubscribeItem { V1 => v01::HostThemeSubscribeItem } + + /// Wire-envelope version for [`Theme::subscribe`](crate::api::Theme::subscribe). + /// Used only by the generated dispatcher/client. + pub enum HostThemeSubscribeVersion { + V1 => SubscriptionEnvelope<(), v01::HostThemeSubscribeItem, CallError>, + } } From fed09fbf1d37ba33e7b27f9763fb5d185ad40e83 Mon Sep 17 00:00:00 2001 From: Nidish Date: Tue, 1 Sep 2026 14:42:32 +0530 Subject: [PATCH 12/16] fix(wire): catch truapi-server up to the nested envelope (RFC 0028) --- rust/crates/truapi-server/src/core.rs | 101 +- rust/crates/truapi-server/src/dispatcher.rs | 123 +- rust/crates/truapi-server/src/frame.rs | 155 +- .../truapi-server/src/generated/dispatcher.rs | 3709 +++++++++++------ .../truapi-server/src/generated/wire_table.rs | 444 +- rust/crates/truapi-server/src/host_core.rs | 82 +- rust/crates/truapi-server/src/native.rs | 40 +- rust/crates/truapi-server/src/subscription.rs | 172 +- rust/crates/truapi-server/src/ws_bridge.rs | 25 +- .../tests/device_permission_revalidation.rs | 18 +- .../truapi-server/tests/golden_frame.rs | 43 +- .../tests/snapshots/golden-account-get.bin | Bin 16 -> 17 bytes .../truapi-server/tests/wire_result_shape.rs | 278 +- .../tests/wire_table_ts_parity.rs | 113 +- 14 files changed, 3321 insertions(+), 1982 deletions(-) diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index dd04dc099..aa96b19c0 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -182,12 +182,6 @@ mod tests { use super::*; use parity_scale_codec::Encode; use truapi::v01; - use truapi::versioned::local_storage::{ - HostLocalStorageClearRequest, HostLocalStorageReadRequest, HostLocalStorageWriteRequest, - }; - use truapi::versioned::notifications::HostPushNotificationRequest; - use truapi::versioned::permissions::RemotePermissionRequest; - use truapi::versioned::system::HostFeatureSupportedRequest; use crate::frame::{Payload, request_ids, subscription_ids}; use crate::test_support::{StubPlatform, runtime_config, test_spawner}; @@ -201,16 +195,19 @@ mod tests { product, test_spawner(), ); - let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + let request = v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], - }); + }; let ids = request_ids("system_feature_supported").expect("known request method"); + // [version=0, direction=Request=0][request bytes] + let mut value = vec![0x00, 0x00]; + value.extend(request.encode()); let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value, }, }; let encoded = frame.encode(); @@ -219,24 +216,26 @@ mod tests { let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); assert_eq!(response.request_id, "p:1"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - // Wire payload is `Result`-shaped: - // [Ok disc=0x00][V1 variant 0x00][supported=1] - assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); + assert_eq!(response.payload.method_id, ids.method_id); + // [version=0, direction=Response=1][Ok disc=0x00][supported=1] + assert_eq!(response.payload.value, vec![0x00, 0x01, 0x00, 0x01]); } /// Drive a request frame through `TrUApiCore::receive_from_product`, - /// decode the response envelope, and return its payload bytes (without - /// the wrapping ProtocolMessage). Shared by the runtime-delegation - /// tests below. + /// decode the response envelope, and return the `Result>` bytes (the envelope's `[version, direction]` prefix + /// stripped). Shared by the runtime-delegation tests below. fn run_request(core: &TrUApiCore, method: &str, request_bytes: Vec) -> Vec { let ids = request_ids(method).expect("known request method"); + // [version=0, direction=Request=0][request bytes] + let mut value = vec![0x00, 0x00]; + value.extend(request_bytes); let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request_bytes, + method_id: ids.method_id, + value, }, }; let response_bytes = @@ -245,8 +244,13 @@ mod tests { let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); assert_eq!(response.request_id, "p:1"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - response.payload.value + assert_eq!(response.payload.method_id, ids.method_id); + assert_eq!( + &response.payload.value[..2], + &[0x00, 0x01], + "expected version=V1, direction=Response" + ); + response.payload.value[2..].to_vec() } fn make_core() -> TrUApiCore { @@ -262,72 +266,68 @@ mod tests { #[test] fn local_storage_read_round_trips_none() { let core = make_core(); - let request = HostLocalStorageReadRequest::V1(v01::HostLocalStorageReadRequest { + let request = v01::HostLocalStorageReadRequest { key: "missing".into(), - }); + }; let payload = run_request(&core, "local_storage_read", request.encode()); - // Ok disc 0x00, V1 variant 0x00, Option::None = 0x00. - assert_eq!(payload, vec![0x00, 0x00, 0x00]); + // Ok disc 0x00, Option::None = 0x00. + assert_eq!(payload, vec![0x00, 0x00]); } #[test] fn local_storage_write_round_trips_unit_ok() { let core = make_core(); - let request = HostLocalStorageWriteRequest::V1(v01::HostLocalStorageWriteRequest { + let request = v01::HostLocalStorageWriteRequest { key: "k".into(), value: vec![1, 2, 3], - }); + }; let payload = run_request(&core, "local_storage_write", request.encode()); - // Ok disc 0x00, V1 variant 0x00. - assert_eq!(payload, vec![0x00, 0x00]); + // Ok disc 0x00. + assert_eq!(payload, vec![0x00]); } #[test] fn local_storage_clear_round_trips_unit_ok() { let core = make_core(); - let request = - HostLocalStorageClearRequest::V1(v01::HostLocalStorageClearRequest { key: "k".into() }); + let request = v01::HostLocalStorageClearRequest { key: "k".into() }; let payload = run_request(&core, "local_storage_clear", request.encode()); - // Ok disc 0x00, V1 variant 0x00. - assert_eq!(payload, vec![0x00, 0x00]); + // Ok disc 0x00. + assert_eq!(payload, vec![0x00]); } #[test] fn send_push_notification_delegates_to_platform() { let core = make_core(); - let request = HostPushNotificationRequest::V1(v01::HostPushNotificationRequest { + let request = v01::HostPushNotificationRequest { text: "hi".into(), deeplink: None, scheduled_at: None, - }); + }; let payload = run_request( &core, "notifications_send_push_notification", request.encode(), ); - // Ok disc 0x00, V1 variant 0x00, notification id 0. + // Ok disc 0x00, notification id 0. let mut expected = vec![0x00u8]; - truapi::versioned::notifications::HostPushNotificationResponse::V1( - v01::HostPushNotificationResponse { id: 0 }, - ) - .encode_to(&mut expected); + v01::HostPushNotificationResponse { id: 0 }.encode_to(&mut expected); assert_eq!(payload, expected); } #[test] fn request_remote_permission_round_trips_granted() { let core = make_core(); - let request = RemotePermissionRequest::V1(v01::RemotePermissionRequest { + let request = v01::RemotePermissionRequest { permission: v01::RemotePermission::ChainSubmit, - }); + }; let payload = run_request( &core, "permissions_request_remote_permission", request.encode(), ); - // Stub permissions grants every request. Wire is Ok disc 0x00, V1 - // variant 0x00, granted=1. - assert_eq!(payload, vec![0x00, 0x00, 0x01]); + // Stub permissions grants every request. Wire is Ok disc 0x00, + // granted=1. + assert_eq!(payload, vec![0x00, 0x01]); } /// `connection_status_subscribe` produces a stream whose first item is @@ -363,8 +363,9 @@ mod tests { request_id: "p:1".into(), payload: Payload { trait_id: sub_ids.trait_id, - method_id: sub_ids.start_id, - value: Vec::new(), + method_id: sub_ids.method_id, + // [version=0, direction=Start=0], no start payload. + value: vec![0x00, 0x00], }, }; futures::executor::block_on(core.dispatch(frame, dyn_transport)); @@ -385,8 +386,8 @@ mod tests { assert!(!sent.is_empty(), "expected at least one _receive frame"); let first = &sent[0]; assert_eq!(first.payload.trait_id, sub_ids.trait_id); - assert_eq!(first.payload.method_id, sub_ids.receive_id); - // V1(Disconnected): V1 variant 0x00, Disconnected discriminant 0x00. - assert_eq!(first.payload.value, vec![0x00, 0x00]); + assert_eq!(first.payload.method_id, sub_ids.method_id); + // [version=0, direction=Receive=3][Disconnected discriminant=0x00] + assert_eq!(first.payload.value, vec![0x00, 0x03, 0x00]); } } diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index a7304611d..92df40db0 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -6,7 +6,7 @@ //! [`crate::generated::dispatcher::register`] function; this module provides //! the framework that owns the registration tables and the routing logic. -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::sync::Arc; use futures::future::BoxFuture; @@ -16,9 +16,9 @@ use truapi::{MIN_TRAIT_ID, WIRE_CODEC_VERSION}; use crate::frame::{ PROTOCOL_ERROR_KEY, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, - ProtocolErrorV1, ProtocolMessage, VersionedProtocolError, + ProtocolErrorV1, ProtocolMessage, VersionedProtocolError, is_subscription_stop, }; -use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds}; +use crate::generated::wire_table::MethodIds; use crate::subscription::{Spawner, SubscriptionManager, SubscriptionStream}; use crate::transport::Transport; @@ -32,23 +32,26 @@ use crate::transport::Transport; pub type RequestHandler = Arc) -> BoxFuture<'static, Result, Vec>> + Send + Sync>; -/// A handler for a subscription method. On the error path the handler returns -/// the complete SCALE-encoded `_interrupt` payload. +/// A handler for a subscription method. On success it also returns the +/// wire-envelope version the caller's `Start` frame negotiated, so the +/// framework can encode a version-correct natural-completion frame without +/// needing the method's concrete envelope type. On the error path the +/// handler returns the complete SCALE-encoded `_interrupt` payload. pub type SubscriptionHandler = Arc< - dyn Fn(String, Vec) -> BoxFuture<'static, Result>> + dyn Fn(String, Vec) -> BoxFuture<'static, Result<(u8, SubscriptionStream), Vec>> + Send + Sync, >; /// A registered request handler plus the discriminants it replies on. pub struct RequestEntry { - ids: RequestFrameIds, + ids: MethodIds, handler: RequestHandler, } /// A registered subscription handler plus the discriminants its frames carry. pub struct SubscriptionEntry { - ids: SubscriptionFrameIds, + ids: MethodIds, handler: SubscriptionHandler, } @@ -57,7 +60,6 @@ pub struct SubscriptionEntry { pub struct Dispatcher { by_request: HashMap<(u8, u8), RequestEntry>, by_start: HashMap<(u8, u8), SubscriptionEntry>, - stop_ids: HashSet<(u8, u8)>, subscriptions: SubscriptionManager, /// Trusted executable kind bound to this connection; `None` leaves the /// surface unrestricted for direct dispatcher embeddings. @@ -70,7 +72,6 @@ impl Dispatcher { Self { by_request: HashMap::new(), by_start: HashMap::new(), - stop_ids: HashSet::new(), subscriptions: SubscriptionManager::new(spawner), execution: None, } @@ -93,11 +94,11 @@ impl Dispatcher { } /// Register a request-response handler, keyed on - /// `(ids.trait_id, ids.request_id)`. Returns the previously registered + /// `(ids.trait_id, ids.method_id)`. Returns the previously registered /// entry if any; callers (the generated `dispatcher::register`) should /// treat `Some` as a programming error since each discriminant pair must /// own exactly one handler. - pub fn on_request(&mut self, ids: RequestFrameIds, handler: F) -> Option + pub fn on_request(&mut self, ids: MethodIds, handler: F) -> Option where F: Fn(String, Vec) -> BoxFuture<'static, Result, Vec>> + Send @@ -105,7 +106,7 @@ impl Dispatcher { + 'static, { self.by_request.insert( - (ids.trait_id, ids.request_id), + (ids.trait_id, ids.method_id), RequestEntry { ids, handler: Arc::new(handler), @@ -114,23 +115,19 @@ impl Dispatcher { } /// Register a subscription handler, keyed on - /// `(ids.trait_id, ids.start_id)`, and record the stop pair so a matching - /// `_stop` frame tears the subscription down. Returns the previously - /// registered entry if any. - pub fn on_subscription( - &mut self, - ids: SubscriptionFrameIds, - handler: F, - ) -> Option + /// `(ids.trait_id, ids.method_id)`. A `Stop` frame arrives at this same + /// address — [`dispatch`](Self::dispatch) peeks its direction tag and + /// routes it to [`SubscriptionManager::handle_stop`] directly, without + /// invoking this handler. Returns the previously registered entry if any. + pub fn on_subscription(&mut self, ids: MethodIds, handler: F) -> Option where - F: Fn(String, Vec) -> BoxFuture<'static, Result>> + F: Fn(String, Vec) -> BoxFuture<'static, Result<(u8, SubscriptionStream), Vec>> + Send + Sync + 'static, { - self.stop_ids.insert((ids.trait_id, ids.stop_id)); self.by_start.insert( - (ids.trait_id, ids.start_id), + (ids.trait_id, ids.method_id), SubscriptionEntry { ids, handler: Arc::new(handler), @@ -153,6 +150,18 @@ impl Dispatcher { return; } + // Precondition this relies on: nothing on this side ever sends its + // *own* outbound `Request::Request` and awaits the matching + // `Request::Response` at the same address a handler is registered + // on. Every frame that arrives at a registered `by_request` key is + // unconditionally routed into that handler — there is no table of + // this dispatcher's own pending outbound calls to consult first, the + // way the TS client's `createTransport` has for exactly this reason + // (see its `system_handshake` handling, where request and response + // share one address). If a caller is ever added on this side for a + // method whose handler is also registered here, that caller's own + // response would be misrouted into the handler instead of settling + // the pending call, and answered with a spurious `MalformedFrame`. if let Some(entry) = self.by_request.get(&key) { let request_id = message.request_id.clone(); let value = (entry.handler)(request_id, message.payload.value) @@ -162,11 +171,15 @@ impl Dispatcher { request_id: message.request_id, payload: Payload { trait_id: entry.ids.trait_id, - method_id: entry.ids.response_id, + method_id: entry.ids.method_id, value, }, }); } else if let Some(entry) = self.by_start.get(&key) { + if is_subscription_stop(&message.payload.value) { + self.subscriptions.handle_stop(&message.request_id); + return; + } // Reserve the slot before awaiting the handler so a `_stop` // arriving while the handler resolves cancels the pending // subscription instead of racing the registration. @@ -174,12 +187,12 @@ impl Dispatcher { let token = self.subscriptions.reserve(request_id.clone()); let result = (entry.handler)(request_id, message.payload.value).await; match result { - Ok(stream) => { + Ok((version, stream)) => { self.subscriptions.activate( token, entry.ids.trait_id, - entry.ids.receive_id, - entry.ids.interrupt_id, + entry.ids.method_id, + version, stream, transport, ); @@ -190,14 +203,12 @@ impl Dispatcher { request_id: message.request_id, payload: Payload { trait_id: entry.ids.trait_id, - method_id: entry.ids.interrupt_id, + method_id: entry.ids.method_id, value: err_bytes, }, }); } } - } else if self.stop_ids.contains(&key) { - self.subscriptions.handle_stop(&message.request_id); } else { // Response / receive / interrupt frames are handled by the client // side and are never registered here, so they land in this arm too: @@ -341,14 +352,14 @@ mod tests { } /// A handler error already owns the complete response payload. The - /// dispatcher only routes it to the registered response id. + /// dispatcher only routes it back to the same address the request + /// arrived on — request and response now share one id. #[test] fn dispatch_request_handler_error_emits_response_payload() { let mut dispatcher = Dispatcher::new(test_spawner()); - let ids = RequestFrameIds { + let ids = MethodIds { trait_id: 7, - request_id: 200, - response_id: 201, + method_id: 200, }; dispatcher.on_request(ids, |_request_id, _bytes| { Box::pin(async move { Err(vec![9, 8, 7]) }) @@ -359,7 +370,7 @@ mod tests { let sent = transport.sent(); assert_eq!(sent.len(), 1, "exactly one response expected"); assert_eq!(sent[0].payload.trait_id, 7); - assert_eq!(sent[0].payload.method_id, 201); + assert_eq!(sent[0].payload.method_id, 200); assert_eq!(sent[0].payload.value, vec![9, 8, 7]); } @@ -369,10 +380,9 @@ mod tests { #[test] fn register_request_twice_returns_previous_handler() { let mut dispatcher = Dispatcher::new(test_spawner()); - let ids = RequestFrameIds { + let ids = MethodIds { trait_id: 7, - request_id: 200, - response_id: 201, + method_id: 200, }; let prev = dispatcher.on_request(ids, |_request_id, _bytes| { Box::pin(async move { Ok(Vec::new()) }) @@ -387,6 +397,41 @@ mod tests { ); } + /// A `Stop` frame (direction tag 1, right after the version byte) arrives + /// at the same address as `Start` and must route to + /// `SubscriptionManager::handle_stop` directly — never invoking the + /// registered handler, which would otherwise try to start a second + /// subscription instead of cancelling the first. + #[test] + fn stop_frame_never_invokes_the_subscription_handler() { + let mut dispatcher = Dispatcher::new(test_spawner()); + let ids = MethodIds { + trait_id: 7, + method_id: 50, + }; + let invoked = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let invoked_in_handler = invoked.clone(); + dispatcher.on_subscription(ids, move |_request_id, _bytes| { + invoked_in_handler.store(true, std::sync::atomic::Ordering::SeqCst); + Box::pin( + async move { Ok((1, Box::pin(futures::stream::empty()) as SubscriptionStream)) }, + ) + }); + let transport = Arc::new(RecordingTransport::default()); + let transport_dyn: Arc = transport.clone(); + // [version=0, direction=Stop=1], matching `frame::encode_envelope_stop`. + let frame = make_frame(7, 50, vec![0, 1]); + futures::executor::block_on(dispatcher.dispatch(frame, transport_dyn)); + assert!( + !invoked.load(std::sync::atomic::Ordering::SeqCst), + "a Stop frame must not invoke the subscription handler" + ); + assert!( + transport.sent().is_empty(), + "handle_stop on an unknown request id emits no frame" + ); + } + #[test] fn execution_filter_is_bound_to_the_connection() { let app = diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index 4034c3fe8..edd7fc716 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -20,7 +20,7 @@ use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, Output}; use truapi::CallError; use truapi::versioned::{FromLatest, IntoLatest, Versioned}; -use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds, WIRE_TABLE, WireKind}; +use crate::generated::wire_table::{MethodIds, WIRE_TABLE, WireKind}; /// Top-level wire message. Encoded as `[requestId][trait][method][bytes]`. #[derive(Debug, Clone, PartialEq, Eq)] @@ -141,6 +141,71 @@ pub fn encode_versioned_interrupt_payload(value: T, version: u8) -> V out } +/// Every nested wire envelope shares this fixed shape once its own +/// `[version]` byte is stripped: a `truapi::versioned::Subscription` +/// direction tag (`Start`=0, `Stop`=1, `Interrupt`=2, `Receive`=3), declared +/// in that order in `truapi::versioned` — followed by whatever bytes that +/// direction carries. These constants and the helpers below let runtime code +/// that has no concrete `{Method}Version` type in scope (the framework +/// dispatcher, the host-initiated subscription manager) still construct and +/// inspect that structure. `Start` has no constant of its own: nothing needs +/// to detect it by raw byte, since a `Start` frame is always decoded through +/// the concrete `{Method}Version` type by the generated per-method handler. +const SUBSCRIPTION_TAG_STOP: u8 = 1; +const SUBSCRIPTION_TAG_INTERRUPT: u8 = 2; +const SUBSCRIPTION_TAG_RECEIVE: u8 = 3; + +/// Peek a subscription frame's direction tag (the second byte, right after +/// the envelope's own version tag) without needing the concrete +/// `Start`/`Item`/`Err` types. +pub fn subscription_direction_tag(bytes: &[u8]) -> Option { + bytes.get(1).copied() +} + +/// Whether a subscription frame's direction tag is `Stop`. +pub fn is_subscription_stop(bytes: &[u8]) -> bool { + subscription_direction_tag(bytes) == Some(SUBSCRIPTION_TAG_STOP) +} + +/// Whether a subscription frame's direction tag is `Receive`. +pub fn is_subscription_receive(bytes: &[u8]) -> bool { + subscription_direction_tag(bytes) == Some(SUBSCRIPTION_TAG_RECEIVE) +} + +/// Whether a subscription frame's direction tag is `Interrupt`. +pub fn is_subscription_interrupt(bytes: &[u8]) -> bool { + subscription_direction_tag(bytes) == Some(SUBSCRIPTION_TAG_INTERRUPT) +} + +/// Split a subscription envelope frame into its direction tag and the +/// direction's own inner bytes (byte 2 onward) — everything after the +/// envelope's `[version, direction]` prefix. When the direction wraps a +/// versioned type (e.g. a method's item wrapper), that type's own leading +/// version tag is the first of these inner bytes and decodes directly; the +/// outer envelope's version byte (byte 0) is redundant with it by +/// construction and is discarded here, not reinserted. `None` if `bytes` is +/// shorter than the two-byte prefix every envelope carries. +pub fn split_subscription_direction(bytes: &[u8]) -> Option<(u8, &[u8])> { + let direction = *bytes.get(1)?; + Some((direction, &bytes[2..])) +} + +/// Encode `{version_type}::V{version}(Subscription::Interrupt(None))` — a +/// subscription's natural (error-free) completion — without needing the +/// concrete `{version_type}`: `Option::None` always encodes as a single +/// `0` byte, and `Interrupt`'s own tag position is fixed, so the three bytes +/// are fully determined by `version` alone. +pub fn encode_envelope_clean_interrupt(version: u8) -> Vec { + vec![version_index(version), SUBSCRIPTION_TAG_INTERRUPT, 0] +} + +/// Encode `{version_type}::V{version}(Subscription::Stop)` — a subscription +/// cancellation — without needing the concrete `{version_type}`: `Stop` +/// carries no payload, so the two bytes are fully determined by `version`. +pub fn encode_envelope_stop(version: u8) -> Vec { + vec![version_index(version), SUBSCRIPTION_TAG_STOP] +} + impl Encode for ProtocolMessage { fn encode_to(&self, dest: &mut T) { self.request_id.encode_to(dest); @@ -203,10 +268,10 @@ pub struct Payload { pub value: Vec, } -/// Request discriminants for a request method, by name. Walks the generated +/// Wire discriminants for a request method, by name. Walks the generated /// [`WIRE_TABLE`]; intended for tests and embedders that route by method /// string rather than holding the generated const. -pub fn request_ids(method: &str) -> Option { +pub fn request_ids(method: &str) -> Option { WIRE_TABLE .iter() .find_map(|entry| match (&entry.kind, entry.method == method) { @@ -215,9 +280,9 @@ pub fn request_ids(method: &str) -> Option { }) } -/// Subscription discriminants for a subscription method, by name. Walks the +/// Wire discriminants for a subscription method, by name. Walks the /// generated [`WIRE_TABLE`]. -pub fn subscription_ids(method: &str) -> Option { +pub fn subscription_ids(method: &str) -> Option { WIRE_TABLE .iter() .find_map(|entry| match (&entry.kind, entry.method == method) { @@ -378,32 +443,30 @@ mod tests { } } - /// All four subscription phases round-trip through the codec. Catches a - /// regression where `Decode` mishandles a frame whose payload is empty for - /// `_stop` / `_interrupt` (no inner data) but non-empty for `_start` / - /// `_receive`. The ids are the `account_connection_status_subscribe` - /// quartet (trait 194, methods 0..=3). + /// All four subscription phases share one `(trait, method)` address now + /// (direction lives in the payload) and still round-trip through the + /// codec. Catches a regression where `Decode` mishandles a frame whose + /// payload is a bare `[version, Stop]` pair (no further inner data) but + /// carries more for `Start`/`Interrupt`/`Receive`. The address is + /// `account_connection_status_subscribe`'s (trait 194, method 0). #[test] fn subscription_phases_round_trip_through_codec() { - let cases: &[(u8, Vec)] = &[ - (0, vec![0x00, 0xaa]), // start - (1, Vec::new()), // stop - (2, Vec::new()), // interrupt - (3, vec![0x01, 0x02, 0x03, 0x04]), // receive + let cases: &[Vec] = &[ + vec![0x00, 0x00, 0xaa], // start: [version, Start, item bytes] + vec![0x00, 0x01], // stop: [version, Stop] + vec![0x00, 0x02, 0x00], // interrupt: [version, Interrupt, None] + vec![0x00, 0x03, 0x01, 0x02, 0x03], // receive: [version, Receive, item bytes] ]; - for (method_id, value) in cases { - let msg = build(194, *method_id, value.clone()); + for value in cases { + let msg = build(194, 0, value.clone()); let bytes = msg.encode(); assert_eq!( bytes, - expected_wire(194, *method_id, value), - "encode mismatch for method id {method_id}" + expected_wire(194, 0, value), + "encode mismatch for payload {value:?}" ); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); - assert_eq!( - decoded, msg, - "round-trip mismatch for method id {method_id}" - ); + assert_eq!(decoded, msg, "round-trip mismatch for payload {value:?}"); } } @@ -413,20 +476,16 @@ mod tests { fn id_helpers_resolve_known_methods() { let handshake = request_ids("system_handshake").expect("known request method"); assert_eq!(handshake.trait_id, 193); - assert_eq!(handshake.request_id, 0); - assert_eq!(handshake.response_id, 1); + assert_eq!(handshake.method_id, 0); let get_account = request_ids("account_get_account").expect("known request method"); assert_eq!(get_account.trait_id, 194); - assert_eq!(get_account.request_id, 4); + assert_eq!(get_account.method_id, 1); let sub = subscription_ids("account_connection_status_subscribe").expect("known subscription"); assert_eq!(sub.trait_id, 194); - assert_eq!(sub.start_id, 0); - assert_eq!(sub.stop_id, 1); - assert_eq!(sub.interrupt_id, 2); - assert_eq!(sub.receive_id, 3); + assert_eq!(sub.method_id, 0); // A request method is not a subscription and vice versa. assert!(subscription_ids("system_handshake").is_none()); @@ -434,6 +493,42 @@ mod tests { assert!(request_ids("not_a_method").is_none()); } + #[test] + fn subscription_direction_helpers_read_the_second_byte() { + assert!(is_subscription_stop(&[0, 1])); + assert!(!is_subscription_stop(&[0, 0])); + assert!(is_subscription_receive(&[0, 3, 0xaa])); + assert!(is_subscription_interrupt(&[0, 2, 0])); + assert_eq!(subscription_direction_tag(&[5, 2]), Some(2)); + assert_eq!(subscription_direction_tag(&[5]), None); + } + + #[test] + fn split_subscription_direction_drops_the_two_byte_envelope_prefix() { + assert_eq!( + split_subscription_direction(&[0, 3, 0xaa, 0xbb]), + Some((3, [0xaa, 0xbb].as_slice())) + ); + assert_eq!( + split_subscription_direction(&[0, 1]), + Some((1, [].as_slice())) + ); + assert_eq!(split_subscription_direction(&[0]), None); + assert_eq!(split_subscription_direction(&[]), None); + } + + #[test] + fn encode_envelope_clean_interrupt_is_version_then_interrupt_then_none() { + assert_eq!(encode_envelope_clean_interrupt(1), vec![0, 2, 0]); + assert_eq!(encode_envelope_clean_interrupt(2), vec![1, 2, 0]); + } + + #[test] + fn encode_envelope_stop_is_version_then_stop() { + assert_eq!(encode_envelope_stop(1), vec![0, 1]); + assert_eq!(encode_envelope_stop(2), vec![1, 1]); + } + /// Genuine zero-byte payload (e.g. unit-typed response). `Decode` must /// handle `remaining_len == 0` without erroring or reading past EOF. #[test] diff --git a/rust/crates/truapi-server/src/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs index e23c58ce0..bd6d63db5 100644 --- a/rust/crates/truapi-server/src/generated/dispatcher.rs +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -9,7 +9,7 @@ use std::sync::Arc; -use parity_scale_codec::Decode; +use parity_scale_codec::{Decode, Encode}; use truapi::CallContext; use truapi::api::{ @@ -21,10 +21,6 @@ use truapi_platform::ProductExecutionKind; use crate::dispatcher::Dispatcher; use crate::frame::downgrade_call_error; -use crate::frame::encode_versioned_err_payload; -use crate::frame::encode_versioned_interrupt_payload; -use crate::frame::encode_versioned_ok_payload; -use crate::frame::encode_versioned_unit_ok_payload; use crate::generated::wire_table; use crate::subscription::{HostInitiatedSubscriptionManager, subscription_stream}; use crate::transport::Transport; @@ -60,9 +56,17 @@ pub(crate) fn chat_custom_message_render( ) -> truapi::Subscription< Result, > { + let envelope = match request { + versioned::chat::ProductChatCustomMessageRenderRequest::V1(bare) => { + versioned::chat::ProductChatCustomMessageRenderVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) + } + }; subscriptions.start( wire_table::CHAT_CUSTOM_MESSAGE_RENDER, - parity_scale_codec::Encode::encode(&request), + 1, + parity_scale_codec::Encode::encode(&envelope), transport, ) } @@ -73,36 +77,59 @@ where { { let host = host.clone(); - dispatcher.on_subscription( - wire_table::ACCOUNT_CONNECTION_STATUS_SUBSCRIBE, - move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let _ = bytes; - let cx = CallContext::with_request_id(request_id.clone()); - let stream = host.connection_status_subscribe(&cx).await; - Ok(subscription_stream::< - versioned::account::HostAccountConnectionStatusSubscribeItem, - _, - >(stream)) - }) - }, - ); + dispatcher.on_subscription(wire_table::ACCOUNT_CONNECTION_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let envelope: versioned::account::HostAccountConnectionStatusSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::account::HostAccountConnectionStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let _request: () = match envelope { + versioned::account::HostAccountConnectionStatusSubscribeVersion::V1(truapi::versioned::Subscription::Start(_bare)) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::account::HostAccountConnectionStatusSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.connection_status_subscribe(&cx).await; + let stream = futures::StreamExt::map(stream, |item: versioned::account::HostAccountConnectionStatusSubscribeItem| match item { + versioned::account::HostAccountConnectionStatusSubscribeItem::V1(bare) => versioned::account::HostAccountConnectionStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) + }) + }); } { let host = host.clone(); dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountGetRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountGetVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountGetRequest = match envelope { + versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountGetRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -110,21 +137,27 @@ where let response: versioned::account::HostAccountGetResponse = match host.get_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountGetError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountGetResponse::V1(bare) => versioned::account::HostAccountGetVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -133,15 +166,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT_ALIAS, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountGetAliasRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountGetAliasVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountGetAliasRequest = match envelope { + versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountGetAliasRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -149,21 +189,27 @@ where let response: versioned::account::HostAccountGetAliasResponse = match host.get_account_alias(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountGetAliasError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountGetAliasResponse::V1(bare) => versioned::account::HostAccountGetAliasVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -172,15 +218,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_CREATE_ACCOUNT_PROOF, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountCreateProofRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountCreateProofVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountCreateProofRequest = match envelope { + versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountCreateProofRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -188,21 +241,27 @@ where let response: versioned::account::HostAccountCreateProofResponse = match host.create_account_proof(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountCreateProofError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountCreateProofResponse::V1(bare) => versioned::account::HostAccountCreateProofVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -211,15 +270,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_SIGN_VRF, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountSignVrfRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountSignVrfVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountSignVrfRequest = match envelope { + versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountSignVrfRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -227,21 +293,27 @@ where let response: versioned::account::HostAccountSignVrfResponse = match host.sign_vrf(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountSignVrfError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountSignVrfResponse::V1(bare) => versioned::account::HostAccountSignVrfVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -250,15 +322,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_REGISTER_RING_VRF_KEY, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountRegisterRingVrfKeyRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountRegisterRingVrfKeyVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountRegisterRingVrfKeyRequest = match envelope { + versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountRegisterRingVrfKeyRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -266,21 +345,27 @@ where let response: versioned::account::HostAccountRegisterRingVrfKeyResponse = match host.register_ring_vrf_key(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountRegisterRingVrfKeyError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountRegisterRingVrfKeyResponse::V1(bare) => versioned::account::HostAccountRegisterRingVrfKeyVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -289,15 +374,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_LIST_RING_VRF_KEYS, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountListRingVrfKeysRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountListRingVrfKeysVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountListRingVrfKeysRequest = match envelope { + versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountListRingVrfKeysRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -305,21 +397,27 @@ where let response: versioned::account::HostAccountListRingVrfKeysResponse = match host.list_ring_vrf_keys(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountListRingVrfKeysError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountListRingVrfKeysResponse::V1(bare) => versioned::account::HostAccountListRingVrfKeysVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -328,15 +426,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_RING_VRF_SIGN, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostAccountRingVrfSignRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostAccountRingVrfSignVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostAccountRingVrfSignRequest = match envelope { + versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostAccountRingVrfSignRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -344,21 +449,27 @@ where let response: versioned::account::HostAccountRingVrfSignResponse = match host.ring_vrf_sign(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostAccountRingVrfSignError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostAccountRingVrfSignResponse::V1(bare) => versioned::account::HostAccountRingVrfSignVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -367,15 +478,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_LEGACY_ACCOUNTS, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostGetLegacyAccountsRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostGetLegacyAccountsVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostGetLegacyAccountsRequest = match envelope { + versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::account::HostGetLegacyAccountsRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -383,21 +501,27 @@ where let response: versioned::account::HostGetLegacyAccountsResponse = match host.get_legacy_accounts(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostGetLegacyAccountsError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostGetLegacyAccountsResponse::V1(bare) => versioned::account::HostGetLegacyAccountsVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -406,15 +530,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_GET_USER_ID, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostGetUserIdRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostGetUserIdVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostGetUserIdRequest = match envelope { + versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::account::HostGetUserIdRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -422,21 +553,27 @@ where let response: versioned::account::HostGetUserIdResponse = match host.get_user_id(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostGetUserIdError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostGetUserIdResponse::V1(bare) => versioned::account::HostGetUserIdVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -445,15 +582,22 @@ where dispatcher.on_request(wire_table::ACCOUNT_REQUEST_LOGIN, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::account::HostRequestLoginRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::account::HostRequestLoginVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::account::HostRequestLoginRequest = match envelope { + versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::account::HostRequestLoginRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -461,21 +605,27 @@ where let response: versioned::account::HostRequestLoginResponse = match host.request_login(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::account::HostRequestLoginError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::account::HostRequestLoginResponse::V1(bare) => versioned::account::HostRequestLoginVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -492,17 +642,53 @@ where move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadFollowRequest = + let envelope: versioned::chain::RemoteChainHeadFollowVersion = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(_) => return Err(Vec::new()), + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(versioned::chain::RemoteChainHeadFollowVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } }; + let request: versioned::chain::RemoteChainHeadFollowRequest = match envelope { + versioned::chain::RemoteChainHeadFollowVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => versioned::chain::RemoteChainHeadFollowRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::chain::RemoteChainHeadFollowVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.follow_head_subscribe(&cx, request).await; - Ok(subscription_stream::< - versioned::chain::RemoteChainHeadFollowItem, - _, - >(stream)) + let stream = futures::StreamExt::map( + stream, + |item: versioned::chain::RemoteChainHeadFollowItem| match item { + versioned::chain::RemoteChainHeadFollowItem::V1(bare) => { + versioned::chain::RemoteChainHeadFollowVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::( + stream, + ), + )) }) }, ); @@ -512,15 +698,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_HEAD_HEADER, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadHeaderRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadHeaderVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadHeaderRequest = match envelope { + versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadHeaderRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -528,21 +721,27 @@ where let response: versioned::chain::RemoteChainHeadHeaderResponse = match host.get_head_header(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadHeaderError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadHeaderResponse::V1(bare) => versioned::chain::RemoteChainHeadHeaderVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -551,15 +750,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_HEAD_BODY, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadBodyRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadBodyVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadBodyRequest = match envelope { + versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadBodyRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -567,21 +773,27 @@ where let response: versioned::chain::RemoteChainHeadBodyResponse = match host.get_head_body(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadBodyError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadBodyResponse::V1(bare) => versioned::chain::RemoteChainHeadBodyVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -590,15 +802,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_HEAD_STORAGE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadStorageRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadStorageVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadStorageRequest = match envelope { + versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadStorageRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -606,21 +825,27 @@ where let response: versioned::chain::RemoteChainHeadStorageResponse = match host.get_head_storage(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadStorageError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadStorageResponse::V1(bare) => versioned::chain::RemoteChainHeadStorageVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -629,15 +854,22 @@ where dispatcher.on_request(wire_table::CHAIN_CALL_HEAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadCallRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadCallVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadCallRequest = match envelope { + versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadCallRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -645,21 +877,27 @@ where let response: versioned::chain::RemoteChainHeadCallResponse = match host.call_head(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadCallError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadCallResponse::V1(bare) => versioned::chain::RemoteChainHeadCallVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -668,15 +906,22 @@ where dispatcher.on_request(wire_table::CHAIN_UNPIN_HEAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadUnpinRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadUnpinVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadUnpinRequest = match envelope { + versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadUnpinRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -684,21 +929,27 @@ where let response: versioned::chain::RemoteChainHeadUnpinResponse = match host.unpin_head(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadUnpinError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadUnpinResponse::V1 => versioned::chain::RemoteChainHeadUnpinVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -707,15 +958,22 @@ where dispatcher.on_request(wire_table::CHAIN_CONTINUE_HEAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadContinueRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadContinueVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadContinueRequest = match envelope { + versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadContinueRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -723,21 +981,27 @@ where let response: versioned::chain::RemoteChainHeadContinueResponse = match host.continue_head(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadContinueError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadContinueResponse::V1 => versioned::chain::RemoteChainHeadContinueVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -746,15 +1010,22 @@ where dispatcher.on_request(wire_table::CHAIN_STOP_HEAD_OPERATION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainHeadStopOperationRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainHeadStopOperationVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainHeadStopOperationRequest = match envelope { + versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainHeadStopOperationRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -762,21 +1033,27 @@ where let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainHeadStopOperationError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainHeadStopOperationResponse::V1 => versioned::chain::RemoteChainHeadStopOperationVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -785,15 +1062,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_SPEC_GENESIS_HASH, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainSpecGenesisHashVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match envelope { + versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainSpecGenesisHashRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -801,21 +1085,27 @@ where let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainSpecGenesisHashError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainSpecGenesisHashResponse::V1(bare) => versioned::chain::RemoteChainSpecGenesisHashVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -824,15 +1114,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_SPEC_CHAIN_NAME, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainSpecChainNameRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainSpecChainNameVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainSpecChainNameRequest = match envelope { + versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainSpecChainNameRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -840,21 +1137,27 @@ where let response: versioned::chain::RemoteChainSpecChainNameResponse = match host.get_spec_chain_name(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainSpecChainNameError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainSpecChainNameResponse::V1(bare) => versioned::chain::RemoteChainSpecChainNameVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -863,15 +1166,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_SPEC_PROPERTIES, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainSpecPropertiesRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainSpecPropertiesVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainSpecPropertiesRequest = match envelope { + versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainSpecPropertiesRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -879,21 +1189,27 @@ where let response: versioned::chain::RemoteChainSpecPropertiesResponse = match host.get_spec_properties(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainSpecPropertiesError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainSpecPropertiesResponse::V1(bare) => versioned::chain::RemoteChainSpecPropertiesVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -902,15 +1218,22 @@ where dispatcher.on_request(wire_table::CHAIN_BROADCAST_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainTransactionBroadcastVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match envelope { + versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainTransactionBroadcastRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -918,21 +1241,27 @@ where let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainTransactionBroadcastError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainTransactionBroadcastResponse::V1(bare) => versioned::chain::RemoteChainTransactionBroadcastVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -941,15 +1270,22 @@ where dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainTransactionStopRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainTransactionStopVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainTransactionStopRequest = match envelope { + versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainTransactionStopRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -957,21 +1293,27 @@ where let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainTransactionStopError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainTransactionStopResponse::V1 => versioned::chain::RemoteChainTransactionStopVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -980,15 +1322,22 @@ where dispatcher.on_request(wire_table::CHAIN_GET_CHAIN_INFO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainInfoRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chain::RemoteChainInfoVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chain::RemoteChainInfoRequest = match envelope { + versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chain::RemoteChainInfoRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -996,21 +1345,27 @@ where let response: versioned::chain::RemoteChainInfoResponse = match host.get_chain_info(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chain::RemoteChainInfoError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chain::RemoteChainInfoResponse::V1(bare) => versioned::chain::RemoteChainInfoVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1026,42 +1381,54 @@ where dispatcher.on_request(wire_table::CHAT_CREATE_ROOM, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chat::HostChatCreateRoomRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chat::HostChatCreateRoomVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chat::HostChatCreateRoomRequest = match envelope { + versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chat::HostChatCreateRoomRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, target_version)); + let error: truapi::CallError = truapi::CallError::Denied; + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } let response: versioned::chat::HostChatCreateRoomResponse = match host.create_room(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chat::HostChatCreateRoomError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chat::HostChatCreateRoomResponse::V1(bare) => versioned::chat::HostChatCreateRoomVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1071,42 +1438,54 @@ where dispatcher.on_request(wire_table::CHAT_REGISTER_BOT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chat::HostChatRegisterBotRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chat::HostChatRegisterBotVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chat::HostChatRegisterBotRequest = match envelope { + versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chat::HostChatRegisterBotRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, target_version)); + let error: truapi::CallError = truapi::CallError::Denied; + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } let response: versioned::chat::HostChatRegisterBotResponse = match host.register_bot(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chat::HostChatRegisterBotError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chat::HostChatRegisterBotResponse::V1(bare) => versioned::chat::HostChatRegisterBotVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1118,16 +1497,61 @@ where move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::chat::HostChatListSubscribeVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(versioned::chat::HostChatListSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; + let _request: () = match envelope { + versioned::chat::HostChatListSubscribeVersion::V1( + truapi::versioned::Subscription::Start(_bare), + ) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::chat::HostChatListSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - return Err(Vec::new()); + let error: truapi::CallError = + truapi::CallError::Denied; + return Err(versioned::chat::HostChatListSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); } let stream = host.list_subscribe(&cx).await; - Ok(subscription_stream::< - versioned::chat::HostChatListSubscribeItem, - _, - >(stream)) + let stream = futures::StreamExt::map( + stream, + |item: versioned::chat::HostChatListSubscribeItem| match item { + versioned::chat::HostChatListSubscribeItem::V1(bare) => { + versioned::chat::HostChatListSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::( + stream, + ), + )) }) }, ); @@ -1138,42 +1562,54 @@ where dispatcher.on_request(wire_table::CHAT_POST_MESSAGE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chat::HostChatPostMessageRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::chat::HostChatPostMessageVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::chat::HostChatPostMessageRequest = match envelope { + versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::chat::HostChatPostMessageRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, target_version)); + let error: truapi::CallError = truapi::CallError::Denied; + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } let response: versioned::chat::HostChatPostMessageResponse = match host.post_message(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::chat::HostChatPostMessageError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::chat::HostChatPostMessageResponse::V1(bare) => versioned::chat::HostChatPostMessageVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1185,16 +1621,61 @@ where move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::chat::HostChatActionSubscribeVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; + let _request: () = match envelope { + versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Start(_bare), + ) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); if !execution_allowed { - return Err(Vec::new()); + let error: truapi::CallError = + truapi::CallError::Denied; + return Err(versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); } let stream = host.action_subscribe(&cx).await; - Ok(subscription_stream::< - versioned::chat::HostChatActionSubscribeItem, - _, - >(stream)) + let stream = futures::StreamExt::map( + stream, + |item: versioned::chat::HostChatActionSubscribeItem| match item { + versioned::chat::HostChatActionSubscribeItem::V1(bare) => { + versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::( + stream, + ), + )) }) }, ); @@ -1210,15 +1691,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentCreatePurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentCreatePurseRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1226,21 +1714,27 @@ where let response: versioned::coin_payment::HostCoinPaymentCreatePurseResponse = match host.create_purse(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentCreatePurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentCreatePurseResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentCreatePurseVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1249,15 +1743,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_QUERY_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentQueryPurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentQueryPurseRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1265,21 +1766,27 @@ where let response: versioned::coin_payment::HostCoinPaymentQueryPurseResponse = match host.query_purse(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentQueryPurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentQueryPurseResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentQueryPurseVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1288,28 +1795,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_REBALANCE_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentRebalancePurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentRebalancePurseRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.rebalance_purse(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentRebalancePurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentRebalancePurseItem| match item { + versioned::coin_payment::HostCoinPaymentRebalancePurseItem::V1(bare) => versioned::coin_payment::HostCoinPaymentRebalancePurseVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1318,28 +1842,45 @@ where dispatcher.on_subscription(wire_table::COIN_PAYMENT_DELETE_PURSE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentDeletePurseVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match envelope { + versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::coin_payment::HostCoinPaymentDeletePurseRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.delete_purse(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentDeletePurseError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::coin_payment::HostCoinPaymentDeletePurseItem| match item { + versioned::coin_payment::HostCoinPaymentDeletePurseItem::V1(bare) => versioned::coin_payment::HostCoinPaymentDeletePurseVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -1348,15 +1889,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentCreateReceivableVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match envelope { + versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentCreateReceivableRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1364,21 +1912,27 @@ where let response: versioned::coin_payment::HostCoinPaymentCreateReceivableResponse = match host.create_receivable(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentCreateReceivableError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentCreateReceivableResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentCreateReceivableVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1387,15 +1941,22 @@ where dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_CHEQUE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::coin_payment::HostCoinPaymentCreateChequeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match envelope { + versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::coin_payment::HostCoinPaymentCreateChequeRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1403,113 +1964,306 @@ where let response: versioned::coin_payment::HostCoinPaymentCreateChequeResponse = match host.create_cheque(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::coin_payment::HostCoinPaymentCreateChequeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::coin_payment::HostCoinPaymentCreateChequeResponse::V1(bare) => versioned::coin_payment::HostCoinPaymentCreateChequeVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } { let host = host.clone(); - dispatcher.on_subscription(wire_table::COIN_PAYMENT_DEPOSIT, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentDepositRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { - reason: err.to_string(), - }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let stream = match host.deposit(&cx, request).await { - Ok(sub) => sub, - Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); - } - }; - Ok(subscription_stream::(stream)) - }) - }); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_DEPOSIT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let envelope: versioned::coin_payment::HostCoinPaymentDepositVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err( + versioned::coin_payment::HostCoinPaymentDepositVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let request: versioned::coin_payment::HostCoinPaymentDepositRequest = + match envelope { + versioned::coin_payment::HostCoinPaymentDepositVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => versioned::coin_payment::HostCoinPaymentDepositRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err( + versioned::coin_payment::HostCoinPaymentDepositVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.deposit(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = + match downgraded { + truapi::CallError::Domain( + versioned::coin_payment::HostCoinPaymentDepositError::V1( + bare, + ), + ) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => { + truapi::CallError::Unsupported + } + truapi::CallError::MalformedFrame { reason } => { + truapi::CallError::MalformedFrame { reason } + } + truapi::CallError::HostFailure { reason } => { + truapi::CallError::HostFailure { reason } + } + }; + return Err( + versioned::coin_payment::HostCoinPaymentDepositVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let stream = futures::StreamExt::map( + stream, + |item: versioned::coin_payment::HostCoinPaymentDepositItem| match item { + versioned::coin_payment::HostCoinPaymentDepositItem::V1(bare) => { + versioned::coin_payment::HostCoinPaymentDepositVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::< + versioned::coin_payment::HostCoinPaymentDepositVersion, + _, + >(stream), + )) + }) + }, + ); } { let host = host.clone(); - dispatcher.on_subscription(wire_table::COIN_PAYMENT_REFUND, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentRefundRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { - reason: err.to_string(), - }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let stream = match host.refund(&cx, request).await { - Ok(sub) => sub, - Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); - } - }; - Ok(subscription_stream::(stream)) - }) - }); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_REFUND, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let envelope: versioned::coin_payment::HostCoinPaymentRefundVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err( + versioned::coin_payment::HostCoinPaymentRefundVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let request: versioned::coin_payment::HostCoinPaymentRefundRequest = + match envelope { + versioned::coin_payment::HostCoinPaymentRefundVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => versioned::coin_payment::HostCoinPaymentRefundRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err( + versioned::coin_payment::HostCoinPaymentRefundVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.refund(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = + match downgraded { + truapi::CallError::Domain( + versioned::coin_payment::HostCoinPaymentRefundError::V1( + bare, + ), + ) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => { + truapi::CallError::Unsupported + } + truapi::CallError::MalformedFrame { reason } => { + truapi::CallError::MalformedFrame { reason } + } + truapi::CallError::HostFailure { reason } => { + truapi::CallError::HostFailure { reason } + } + }; + return Err(versioned::coin_payment::HostCoinPaymentRefundVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; + let stream = futures::StreamExt::map( + stream, + |item: versioned::coin_payment::HostCoinPaymentRefundItem| match item { + versioned::coin_payment::HostCoinPaymentRefundItem::V1(bare) => { + versioned::coin_payment::HostCoinPaymentRefundVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::< + versioned::coin_payment::HostCoinPaymentRefundVersion, + _, + >(stream), + )) + }) + }, + ); } { let host = host; - dispatcher.on_subscription(wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentListenForRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { - reason: err.to_string(), - }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let stream = match host.listen_for_payment(&cx, request).await { - Ok(sub) => sub, - Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); - } - }; - Ok(subscription_stream::(stream)) - }) - }); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let envelope: versioned::coin_payment::HostCoinPaymentListenForVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err( + versioned::coin_payment::HostCoinPaymentListenForVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let request: versioned::coin_payment::HostCoinPaymentListenForRequest = + match envelope { + versioned::coin_payment::HostCoinPaymentListenForVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => versioned::coin_payment::HostCoinPaymentListenForRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err( + versioned::coin_payment::HostCoinPaymentListenForVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.listen_for_payment(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = + match downgraded { + truapi::CallError::Domain( + versioned::coin_payment::HostCoinPaymentListenForError::V1( + bare, + ), + ) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => { + truapi::CallError::Unsupported + } + truapi::CallError::MalformedFrame { reason } => { + truapi::CallError::MalformedFrame { reason } + } + truapi::CallError::HostFailure { reason } => { + truapi::CallError::HostFailure { reason } + } + }; + return Err( + versioned::coin_payment::HostCoinPaymentListenForVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let stream = futures::StreamExt::map( + stream, + |item: versioned::coin_payment::HostCoinPaymentListenForItem| match item { + versioned::coin_payment::HostCoinPaymentListenForItem::V1(bare) => { + versioned::coin_payment::HostCoinPaymentListenForVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::< + versioned::coin_payment::HostCoinPaymentListenForVersion, + _, + >(stream), + )) + }) + }, + ); } } @@ -1522,15 +2276,22 @@ where dispatcher.on_request(wire_table::ENTROPY_DERIVE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::entropy::HostDeriveEntropyRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::entropy::HostDeriveEntropyVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::entropy::HostDeriveEntropyRequest = match envelope { + versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::entropy::HostDeriveEntropyRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1538,21 +2299,27 @@ where let response: versioned::entropy::HostDeriveEntropyResponse = match host.derive(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::entropy::HostDeriveEntropyError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::entropy::HostDeriveEntropyResponse::V1(bare) => versioned::entropy::HostDeriveEntropyVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1567,15 +2334,22 @@ where dispatcher.on_request(wire_table::LOCAL_STORAGE_READ, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageReadRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::local_storage::HostLocalStorageReadVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::local_storage::HostLocalStorageReadRequest = match envelope { + versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::local_storage::HostLocalStorageReadRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1583,21 +2357,27 @@ where let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::local_storage::HostLocalStorageReadError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::local_storage::HostLocalStorageReadResponse::V1(bare) => versioned::local_storage::HostLocalStorageReadVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1606,15 +2386,22 @@ where dispatcher.on_request(wire_table::LOCAL_STORAGE_WRITE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageWriteRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::local_storage::HostLocalStorageWriteVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::local_storage::HostLocalStorageWriteRequest = match envelope { + versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::local_storage::HostLocalStorageWriteRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1622,21 +2409,27 @@ where let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::local_storage::HostLocalStorageWriteError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::local_storage::HostLocalStorageWriteResponse::V1 => versioned::local_storage::HostLocalStorageWriteVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1645,15 +2438,22 @@ where dispatcher.on_request(wire_table::LOCAL_STORAGE_CLEAR, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageClearRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::local_storage::HostLocalStorageClearVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::local_storage::HostLocalStorageClearRequest = match envelope { + versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::local_storage::HostLocalStorageClearRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1661,21 +2461,27 @@ where let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::local_storage::HostLocalStorageClearError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::local_storage::HostLocalStorageClearResponse::V1 => versioned::local_storage::HostLocalStorageClearVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1692,13 +2498,53 @@ where move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::locale::HostLocaleSubscribeVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(versioned::locale::HostLocaleSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; + let _request: () = match envelope { + versioned::locale::HostLocaleSubscribeVersion::V1( + truapi::versioned::Subscription::Start(_bare), + ) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::locale::HostLocaleSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe(&cx).await; - Ok(subscription_stream::< - versioned::locale::HostLocaleSubscribeItem, - _, - >(stream)) + let stream = futures::StreamExt::map( + stream, + |item: versioned::locale::HostLocaleSubscribeItem| match item { + versioned::locale::HostLocaleSubscribeItem::V1(bare) => { + versioned::locale::HostLocaleSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::( + stream, + ), + )) }) }, ); @@ -1714,15 +2560,22 @@ where dispatcher.on_request(wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::notifications::HostPushNotificationRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::notifications::HostPushNotificationVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::notifications::HostPushNotificationRequest = match envelope { + versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::notifications::HostPushNotificationRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1730,21 +2583,27 @@ where let response: versioned::notifications::HostPushNotificationResponse = match host.send_push_notification(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::notifications::HostPushNotificationError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::notifications::HostPushNotificationResponse::V1(bare) => versioned::notifications::HostPushNotificationVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1753,15 +2612,22 @@ where dispatcher.on_request(wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::notifications::HostPushNotificationCancelRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::notifications::HostPushNotificationCancelVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::notifications::HostPushNotificationCancelRequest = match envelope { + versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::notifications::HostPushNotificationCancelRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1769,21 +2635,27 @@ where let response: versioned::notifications::HostPushNotificationCancelResponse = match host.cancel_push_notification(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::notifications::HostPushNotificationCancelError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::notifications::HostPushNotificationCancelResponse::V1 => versioned::notifications::HostPushNotificationCancelVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1795,48 +2667,117 @@ where { { let host = host.clone(); - dispatcher.on_subscription(wire_table::PAYMENT_BALANCE_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::payment::HostPaymentBalanceSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { - reason: err.to_string(), + dispatcher.on_subscription( + wire_table::PAYMENT_BALANCE_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let envelope: versioned::payment::HostPaymentBalanceSubscribeVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError< + truapi::v01::HostPaymentBalanceSubscribeError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err( + versioned::payment::HostPaymentBalanceSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = + match envelope { + versioned::payment::HostPaymentBalanceSubscribeVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => versioned::payment::HostPaymentBalanceSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError< + truapi::v01::HostPaymentBalanceSubscribeError, + > = truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err( + versioned::payment::HostPaymentBalanceSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.balance_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError< + truapi::v01::HostPaymentBalanceSubscribeError, + > = match downgraded { + truapi::CallError::Domain( + versioned::payment::HostPaymentBalanceSubscribeError::V1(bare), + ) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => { + truapi::CallError::MalformedFrame { reason } + } + truapi::CallError::HostFailure { reason } => { + truapi::CallError::HostFailure { reason } + } }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let stream = match host.balance_subscribe(&cx, request).await { - Ok(sub) => sub, - Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); - } - }; - Ok(subscription_stream::(stream)) - }) - }); + return Err( + versioned::payment::HostPaymentBalanceSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let stream = futures::StreamExt::map( + stream, + |item: versioned::payment::HostPaymentBalanceSubscribeItem| match item { + versioned::payment::HostPaymentBalanceSubscribeItem::V1(bare) => { + versioned::payment::HostPaymentBalanceSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::< + versioned::payment::HostPaymentBalanceSubscribeVersion, + _, + >(stream), + )) + }) + }, + ); } { let host = host.clone(); dispatcher.on_request(wire_table::PAYMENT_REQUEST, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::payment::HostPaymentRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::payment::HostPaymentVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::payment::HostPaymentRequest = match envelope { + versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::payment::HostPaymentRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1844,68 +2785,141 @@ where let response: versioned::payment::HostPaymentResponse = match host.request(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::payment::HostPaymentError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::payment::HostPaymentResponse::V1(bare) => versioned::payment::HostPaymentVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } { let host = host.clone(); - dispatcher.on_subscription(wire_table::PAYMENT_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::payment::HostPaymentStatusSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { - reason: err.to_string(), + dispatcher.on_subscription( + wire_table::PAYMENT_STATUS_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let envelope: versioned::payment::HostPaymentStatusSubscribeVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError< + truapi::v01::HostPaymentStatusSubscribeError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err( + versioned::payment::HostPaymentStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let request: versioned::payment::HostPaymentStatusSubscribeRequest = + match envelope { + versioned::payment::HostPaymentStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => versioned::payment::HostPaymentStatusSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError< + truapi::v01::HostPaymentStatusSubscribeError, + > = truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err( + versioned::payment::HostPaymentStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.status_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError< + truapi::v01::HostPaymentStatusSubscribeError, + > = match downgraded { + truapi::CallError::Domain( + versioned::payment::HostPaymentStatusSubscribeError::V1(bare), + ) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => { + truapi::CallError::MalformedFrame { reason } + } + truapi::CallError::HostFailure { reason } => { + truapi::CallError::HostFailure { reason } + } }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let stream = match host.status_subscribe(&cx, request).await { - Ok(sub) => sub, - Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); - } - }; - Ok(subscription_stream::(stream)) - }) - }); + return Err(versioned::payment::HostPaymentStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; + let stream = futures::StreamExt::map( + stream, + |item: versioned::payment::HostPaymentStatusSubscribeItem| match item { + versioned::payment::HostPaymentStatusSubscribeItem::V1(bare) => { + versioned::payment::HostPaymentStatusSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::< + versioned::payment::HostPaymentStatusSubscribeVersion, + _, + >(stream), + )) + }) + }, + ); } { let host = host; dispatcher.on_request(wire_table::PAYMENT_TOP_UP, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::payment::HostPaymentTopUpRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::payment::HostPaymentTopUpVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::payment::HostPaymentTopUpRequest = match envelope { + versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::payment::HostPaymentTopUpRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1913,21 +2927,27 @@ where let response: versioned::payment::HostPaymentTopUpResponse = match host.top_up(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::payment::HostPaymentTopUpError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::payment::HostPaymentTopUpResponse::V1 => versioned::payment::HostPaymentTopUpVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -1942,15 +2962,22 @@ where dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::permissions::HostDevicePermissionRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::permissions::HostDevicePermissionVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::permissions::HostDevicePermissionRequest = match envelope { + versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::permissions::HostDevicePermissionRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1958,21 +2985,27 @@ where let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::permissions::HostDevicePermissionError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::permissions::HostDevicePermissionResponse::V1(bare) => versioned::permissions::HostDevicePermissionVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -1981,15 +3014,22 @@ where dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_REMOTE_PERMISSION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::permissions::RemotePermissionRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::permissions::RemotePermissionVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::permissions::RemotePermissionRequest = match envelope { + versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::permissions::RemotePermissionRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -1997,21 +3037,27 @@ where let response: versioned::permissions::RemotePermissionResponse = match host.request_remote_permission(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::permissions::RemotePermissionError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::permissions::RemotePermissionResponse::V1(bare) => versioned::permissions::RemotePermissionVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2028,17 +3074,61 @@ where move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + let envelope: versioned::preimage::RemotePreimageLookupSubscribeVersion = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(_) => return Err(Vec::new()), + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err( + versioned::preimage::RemotePreimageLookupSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } + }; + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + match envelope { + versioned::preimage::RemotePreimageLookupSubscribeVersion::V1( + truapi::versioned::Subscription::Start(bare), + ) => { + versioned::preimage::RemotePreimageLookupSubscribeRequest::V1(bare) + } + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err( + versioned::preimage::RemotePreimageLookupSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode(), + ); + } }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.lookup_subscribe(&cx, request).await; - Ok(subscription_stream::< - versioned::preimage::RemotePreimageLookupSubscribeItem, - _, - >(stream)) + let stream = futures::StreamExt::map( + stream, + |item: versioned::preimage::RemotePreimageLookupSubscribeItem| match item { + versioned::preimage::RemotePreimageLookupSubscribeItem::V1(bare) => { + versioned::preimage::RemotePreimageLookupSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::< + versioned::preimage::RemotePreimageLookupSubscribeVersion, + _, + >(stream), + )) }) }, ); @@ -2048,15 +3138,22 @@ where dispatcher.on_request(wire_table::PREIMAGE_SUBMIT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::preimage::RemotePreimageSubmitRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::preimage::RemotePreimageSubmitVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::preimage::RemotePreimageSubmitRequest = match envelope { + versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::preimage::RemotePreimageSubmitRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2064,21 +3161,27 @@ where let response: versioned::preimage::RemotePreimageSubmitResponse = match host.submit(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::preimage::RemotePreimageSubmitError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::preimage::RemotePreimageSubmitResponse::V1(bare) => versioned::preimage::RemotePreimageSubmitVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2093,15 +3196,22 @@ where dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::resource_allocation::HostRequestResourceAllocationVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match envelope { + versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::resource_allocation::HostRequestResourceAllocationRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2109,21 +3219,27 @@ where let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::resource_allocation::HostRequestResourceAllocationError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::resource_allocation::HostRequestResourceAllocationResponse::V1(bare) => versioned::resource_allocation::HostRequestResourceAllocationVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2138,15 +3254,22 @@ where dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostCreateTransactionRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostCreateTransactionVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostCreateTransactionRequest = match envelope { + versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostCreateTransactionRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2154,21 +3277,27 @@ where let response: versioned::signing::HostCreateTransactionResponse = match host.create_transaction(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostCreateTransactionError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostCreateTransactionResponse::V1(bare) => versioned::signing::HostCreateTransactionVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2177,15 +3306,22 @@ where dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostCreateTransactionWithLegacyAccountVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match envelope { + versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostCreateTransactionWithLegacyAccountRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2193,21 +3329,27 @@ where let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostCreateTransactionWithLegacyAccountError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostCreateTransactionWithLegacyAccountResponse::V1(bare) => versioned::signing::HostCreateTransactionWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2216,15 +3358,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignRawWithLegacyAccountVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match envelope { + versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignRawWithLegacyAccountRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2232,21 +3381,27 @@ where let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignRawWithLegacyAccountError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignRawWithLegacyAccountResponse::V1(bare) => versioned::signing::HostSignRawWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2255,15 +3410,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignPayloadWithLegacyAccountVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match envelope { + versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignPayloadWithLegacyAccountRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2271,21 +3433,27 @@ where let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignPayloadWithLegacyAccountError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignPayloadWithLegacyAccountResponse::V1(bare) => versioned::signing::HostSignPayloadWithLegacyAccountVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2294,15 +3462,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_RAW, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignRawRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignRawVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignRawRequest = match envelope { + versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignRawRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2310,21 +3485,27 @@ where let response: versioned::signing::HostSignRawResponse = match host.sign_raw(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignRawError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignRawResponse::V1(bare) => versioned::signing::HostSignRawVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2333,15 +3514,22 @@ where dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::signing::HostSignPayloadRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::signing::HostSignPayloadVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::signing::HostSignPayloadRequest = match envelope { + versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::signing::HostSignPayloadRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2349,21 +3537,27 @@ where let response: versioned::signing::HostSignPayloadResponse = match host.sign_payload(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::signing::HostSignPayloadError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::signing::HostSignPayloadResponse::V1(bare) => versioned::signing::HostSignPayloadVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2378,28 +3572,45 @@ where dispatcher.on_subscription(wire_table::STATEMENT_STORE_SUBSCRIBE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreSubscribeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Err(versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match envelope { + versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Start(bare)) => versioned::statement_store::RemoteStatementStoreSubscribeRequest::V1(bare), + _ => { + let error: truapi::CallError = truapi::CallError::MalformedFrame { - reason: err.to_string(), + reason: "expected a start-direction frame".to_string(), }; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); + return Err(versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.subscribe(&cx, request).await { Ok(sub) => sub, Err(err) => { - return Err(encode_versioned_interrupt_payload(err, target_version)); + let downgraded = downgrade_call_error(err, 1); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreSubscribeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Err(versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1(truapi::versioned::Subscription::Interrupt(Some(error))).encode()); } }; - Ok(subscription_stream::(stream)) + let stream = futures::StreamExt::map(stream, |item: versioned::statement_store::RemoteStatementStoreSubscribeItem| match item { + versioned::statement_store::RemoteStatementStoreSubscribeItem::V1(bare) => versioned::statement_store::RemoteStatementStoreSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ), + }); + Ok((1, subscription_stream::(stream))) }) }); } @@ -2408,15 +3619,22 @@ where dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreCreateProofVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match envelope { + versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::statement_store::RemoteStatementStoreCreateProofRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2424,21 +3642,27 @@ where let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreCreateProofError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::statement_store::RemoteStatementStoreCreateProofResponse::V1(bare) => versioned::statement_store::RemoteStatementStoreCreateProofVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2447,15 +3671,22 @@ where dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF_AUTHORIZED, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match envelope { + versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2463,21 +3694,27 @@ where let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse::V1(bare) => versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2486,23 +3723,38 @@ where dispatcher.on_request(wire_table::STATEMENT_STORE_SUBMIT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::statement_store::RemoteStatementStoreSubmitVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match envelope { + versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::statement_store::RemoteStatementStoreSubmitRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); match host.submit(&cx, request).await { - Ok(()) => Ok(encode_versioned_unit_ok_payload(target_version)), + Ok(()) => Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Ok(()))).encode()), Err(err) => { - Ok(encode_versioned_err_payload(err, target_version)) + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::statement_store::RemoteStatementStoreSubmitError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + Ok(versioned::statement_store::RemoteStatementStoreSubmitVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()) } } }) @@ -2519,15 +3771,22 @@ where dispatcher.on_request(wire_table::SYSTEM_HANDSHAKE, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostHandshakeRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostHandshakeVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostHandshakeRequest = match envelope { + versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::system::HostHandshakeRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2535,21 +3794,27 @@ where let response: versioned::system::HostHandshakeResponse = match host.handshake(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostHandshakeError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostHandshakeResponse::V1 => versioned::system::HostHandshakeVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -2558,15 +3823,22 @@ where dispatcher.on_request(wire_table::SYSTEM_FEATURE_SUPPORTED, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostFeatureSupportedRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostFeatureSupportedVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostFeatureSupportedRequest = match envelope { + versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::system::HostFeatureSupportedRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2574,21 +3846,27 @@ where let response: versioned::system::HostFeatureSupportedResponse = match host.feature_supported(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostFeatureSupportedError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostFeatureSupportedResponse::V1(bare) => versioned::system::HostFeatureSupportedVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2597,15 +3875,22 @@ where dispatcher.on_request(wire_table::SYSTEM_NAVIGATE_TO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostNavigateToRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostNavigateToVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostNavigateToRequest = match envelope { + versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Request(bare)) => versioned::system::HostNavigateToRequest::V1(bare), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2613,21 +3898,27 @@ where let response: versioned::system::HostNavigateToResponse = match host.navigate_to(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostNavigateToError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostNavigateToResponse::V1 => versioned::system::HostNavigateToVersion::V1(truapi::versioned::Request::Response(Ok(()))), + }.encode()) }) }); } @@ -2636,15 +3927,22 @@ where dispatcher.on_request(wire_table::SYSTEM_HOST_INFO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostInfoRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostInfoVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostInfoRequest = match envelope { + versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::system::HostInfoRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2652,21 +3950,27 @@ where let response: versioned::system::HostInfoResponse = match host.host_info(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostInfoError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostInfoResponse::V1(bare) => versioned::system::HostInfoVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2675,15 +3979,22 @@ where dispatcher.on_request(wire_table::SYSTEM_GET_PRODUCT_CONTEXT, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::system::HostGetProductContextRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, + let envelope: versioned::system::HostGetProductContextVersion = match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + return Ok(versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); + } + }; + let request: versioned::system::HostGetProductContextRequest = match envelope { + versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Request(_bare)) => versioned::system::HostGetProductContextRequest::V1, + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a request-direction frame".to_string(), + }; + return Ok(versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; let target_version = request.version(); @@ -2691,21 +4002,27 @@ where let response: versioned::system::HostGetProductContextResponse = match host.get_product_context(&cx, request).await { Ok(value) => value, Err(err) => { - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, target_version), - target_version, - )); + let downgraded = downgrade_call_error(err, target_version); + let error: truapi::CallError = match downgraded { + truapi::CallError::Domain(versioned::system::HostGetProductContextError::V1(bare)) => truapi::CallError::Domain(bare), + truapi::CallError::Denied => truapi::CallError::Denied, + truapi::CallError::Unsupported => truapi::CallError::Unsupported, + truapi::CallError::MalformedFrame { reason } => truapi::CallError::MalformedFrame { reason }, + truapi::CallError::HostFailure { reason } => truapi::CallError::HostFailure { reason }, + }; + return Ok(versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Err(error))).encode()); } }; + let response = ::from_latest( + truapi::versioned::IntoLatest::into_latest(response), + target_version, + ); // Downgraded to the caller's version: a handler answers in // latest terms, and a peer that asked in an older version // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - ), - )) + Ok(match response { + versioned::system::HostGetProductContextResponse::V1(bare) => versioned::system::HostGetProductContextVersion::V1(truapi::versioned::Request::Response(Ok(bare))), + }.encode()) }) }); } @@ -2722,13 +4039,53 @@ where move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let _ = bytes; + let envelope: versioned::theme::HostThemeSubscribeVersion = + match Decode::decode(&mut &bytes[..]) { + Ok(envelope) => envelope, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(versioned::theme::HostThemeSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; + let _request: () = match envelope { + versioned::theme::HostThemeSubscribeVersion::V1( + truapi::versioned::Subscription::Start(_bare), + ) => (), + _ => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: "expected a start-direction frame".to_string(), + }; + return Err(versioned::theme::HostThemeSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some(error)), + ) + .encode()); + } + }; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe(&cx).await; - Ok(subscription_stream::< - versioned::theme::HostThemeSubscribeItem, - _, - >(stream)) + let stream = futures::StreamExt::map( + stream, + |item: versioned::theme::HostThemeSubscribeItem| match item { + versioned::theme::HostThemeSubscribeItem::V1(bare) => { + versioned::theme::HostThemeSubscribeVersion::V1( + truapi::versioned::Subscription::Receive(bare), + ) + } + }, + ); + Ok(( + 1, + subscription_stream::( + stream, + ), + )) }) }, ); diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs index 891fce3c1..c3d06f400 100644 --- a/rust/crates/truapi-server/src/generated/wire_table.rs +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -2,38 +2,23 @@ //! //! Auto-generated by truapi-codegen. Do not edit. //! -//! Every frame carries a `(trait, method)` discriminant pair. Each -//! method reserves either two method ids (request/response) or four -//! (start/stop/interrupt/receive) within its trait. The ids for each -//! method are exposed as a named const (`PREIMAGE_SUBMIT`, ...); -//! [`WIRE_TABLE`] and the generated dispatcher both reference those -//! consts so the numbers live in exactly one place. The table is -//! sorted by (trait id, request/start id). - -/// Request method wire discriminants. +//! Every frame carries a `(trait, method)` discriminant pair; one +//! method id addresses every frame a method ever sends or receives, +//! regardless of shape. Direction (request/response, or a +//! subscription's start/stop/interrupt/receive) and version are +//! carried inside the payload. The ids for each method are exposed as +//! a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the +//! generated dispatcher both reference those consts so the numbers +//! live in exactly one place. The table is sorted by (trait id, +//! method id). + +/// Wire discriminants for one method. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RequestFrameIds { - /// Trait discriminant carried by both frames. +pub struct MethodIds { + /// Trait discriminant carried by every frame of this method. pub trait_id: u8, - /// Method discriminant for the request frame. - pub request_id: u8, - /// Method discriminant for the response frame. - pub response_id: u8, -} - -/// Subscription method wire discriminants. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SubscriptionFrameIds { - /// Trait discriminant carried by all four frames. - pub trait_id: u8, - /// Method discriminant for the start frame. - pub start_id: u8, - /// Method discriminant for the stop frame. - pub stop_id: u8, - /// Method discriminant for the interrupt frame (server-initiated termination). - pub interrupt_id: u8, - /// Method discriminant for each receive frame (a streamed item). - pub receive_id: u8, + /// Method discriminant carried by every frame of this method. + pub method_id: u8, } /// A single wire-table row. @@ -44,548 +29,445 @@ pub struct WireEntry { pub kind: WireKind, } -/// Wire-slot shape: request/response pair or subscription quartet. +/// Wire-slot shape: request/response or a subscription's +/// start/stop/interrupt/receive quartet. pub enum WireKind { /// Request/response method. - Request(RequestFrameIds), + Request(MethodIds), /// Subscription method. - Subscription(SubscriptionFrameIds), + Subscription(MethodIds), } /// Wire discriminants for `system_handshake`. -pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_HANDSHAKE: MethodIds = MethodIds { trait_id: 193, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `system_feature_supported`. -pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_FEATURE_SUPPORTED: MethodIds = MethodIds { trait_id: 193, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `system_navigate_to`. -pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_NAVIGATE_TO: MethodIds = MethodIds { trait_id: 193, - request_id: 4, - response_id: 5, + method_id: 2, }; /// Wire discriminants for `system_host_info`. -pub const SYSTEM_HOST_INFO: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_HOST_INFO: MethodIds = MethodIds { trait_id: 193, - request_id: 6, - response_id: 7, + method_id: 3, }; /// Wire discriminants for `system_get_product_context`. -pub const SYSTEM_GET_PRODUCT_CONTEXT: RequestFrameIds = RequestFrameIds { +pub const SYSTEM_GET_PRODUCT_CONTEXT: MethodIds = MethodIds { trait_id: 193, - request_id: 8, - response_id: 9, + method_id: 4, }; /// Wire discriminants for `account_connection_status_subscribe`. -pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: MethodIds = MethodIds { trait_id: 194, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `account_get_account`. -pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_ACCOUNT: MethodIds = MethodIds { trait_id: 194, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `account_get_account_alias`. -pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_ACCOUNT_ALIAS: MethodIds = MethodIds { trait_id: 194, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `account_create_account_proof`. -pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_CREATE_ACCOUNT_PROOF: MethodIds = MethodIds { trait_id: 194, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `account_get_legacy_accounts`. -pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_LEGACY_ACCOUNTS: MethodIds = MethodIds { trait_id: 194, - request_id: 10, - response_id: 11, + method_id: 4, }; /// Wire discriminants for `account_get_user_id`. -pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_GET_USER_ID: MethodIds = MethodIds { trait_id: 194, - request_id: 12, - response_id: 13, + method_id: 5, }; /// Wire discriminants for `account_request_login`. -pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_REQUEST_LOGIN: MethodIds = MethodIds { trait_id: 194, - request_id: 14, - response_id: 15, + method_id: 6, }; /// Wire discriminants for `account_sign_vrf`. -pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_SIGN_VRF: MethodIds = MethodIds { trait_id: 194, - request_id: 16, - response_id: 17, + method_id: 7, }; /// Wire discriminants for `account_register_ring_vrf_key`. -pub const ACCOUNT_REGISTER_RING_VRF_KEY: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_REGISTER_RING_VRF_KEY: MethodIds = MethodIds { trait_id: 194, - request_id: 168, - response_id: 169, + method_id: 8, }; /// Wire discriminants for `account_list_ring_vrf_keys`. -pub const ACCOUNT_LIST_RING_VRF_KEYS: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_LIST_RING_VRF_KEYS: MethodIds = MethodIds { trait_id: 194, - request_id: 170, - response_id: 171, + method_id: 9, }; /// Wire discriminants for `account_ring_vrf_sign`. -pub const ACCOUNT_RING_VRF_SIGN: RequestFrameIds = RequestFrameIds { +pub const ACCOUNT_RING_VRF_SIGN: MethodIds = MethodIds { trait_id: 194, - request_id: 172, - response_id: 173, + method_id: 10, }; /// Wire discriminants for `chain_follow_head_subscribe`. -pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: MethodIds = MethodIds { trait_id: 195, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `chain_get_head_header`. -pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_HEAD_HEADER: MethodIds = MethodIds { trait_id: 195, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `chain_get_head_body`. -pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_HEAD_BODY: MethodIds = MethodIds { trait_id: 195, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `chain_get_head_storage`. -pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_HEAD_STORAGE: MethodIds = MethodIds { trait_id: 195, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `chain_call_head`. -pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { +pub const CHAIN_CALL_HEAD: MethodIds = MethodIds { trait_id: 195, - request_id: 10, - response_id: 11, + method_id: 4, }; /// Wire discriminants for `chain_unpin_head`. -pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { +pub const CHAIN_UNPIN_HEAD: MethodIds = MethodIds { trait_id: 195, - request_id: 12, - response_id: 13, + method_id: 5, }; /// Wire discriminants for `chain_continue_head`. -pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { +pub const CHAIN_CONTINUE_HEAD: MethodIds = MethodIds { trait_id: 195, - request_id: 14, - response_id: 15, + method_id: 6, }; /// Wire discriminants for `chain_stop_head_operation`. -pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { +pub const CHAIN_STOP_HEAD_OPERATION: MethodIds = MethodIds { trait_id: 195, - request_id: 16, - response_id: 17, + method_id: 7, }; /// Wire discriminants for `chain_get_spec_genesis_hash`. -pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_SPEC_GENESIS_HASH: MethodIds = MethodIds { trait_id: 195, - request_id: 18, - response_id: 19, + method_id: 8, }; /// Wire discriminants for `chain_get_spec_chain_name`. -pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_SPEC_CHAIN_NAME: MethodIds = MethodIds { trait_id: 195, - request_id: 20, - response_id: 21, + method_id: 9, }; /// Wire discriminants for `chain_get_spec_properties`. -pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_SPEC_PROPERTIES: MethodIds = MethodIds { trait_id: 195, - request_id: 22, - response_id: 23, + method_id: 10, }; /// Wire discriminants for `chain_broadcast_transaction`. -pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { +pub const CHAIN_BROADCAST_TRANSACTION: MethodIds = MethodIds { trait_id: 195, - request_id: 24, - response_id: 25, + method_id: 11, }; /// Wire discriminants for `chain_stop_transaction`. -pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { +pub const CHAIN_STOP_TRANSACTION: MethodIds = MethodIds { trait_id: 195, - request_id: 26, - response_id: 27, + method_id: 12, }; /// Wire discriminants for `chain_get_chain_info`. -pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { +pub const CHAIN_GET_CHAIN_INFO: MethodIds = MethodIds { trait_id: 195, - request_id: 166, - response_id: 167, + method_id: 13, }; /// Wire discriminants for `chat_create_room`. -pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { +pub const CHAT_CREATE_ROOM: MethodIds = MethodIds { trait_id: 196, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `chat_register_bot`. -pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { +pub const CHAT_REGISTER_BOT: MethodIds = MethodIds { trait_id: 196, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `chat_list_subscribe`. -pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAT_LIST_SUBSCRIBE: MethodIds = MethodIds { trait_id: 196, - start_id: 4, - stop_id: 5, - interrupt_id: 6, - receive_id: 7, + method_id: 2, }; /// Wire discriminants for `chat_post_message`. -pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { +pub const CHAT_POST_MESSAGE: MethodIds = MethodIds { trait_id: 196, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `chat_action_subscribe`. -pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAT_ACTION_SUBSCRIBE: MethodIds = MethodIds { trait_id: 196, - start_id: 10, - stop_id: 11, - interrupt_id: 12, - receive_id: 13, + method_id: 4, }; /// Wire discriminants for `chat_custom_message_render`. -pub const CHAT_CUSTOM_MESSAGE_RENDER: SubscriptionFrameIds = SubscriptionFrameIds { +pub const CHAT_CUSTOM_MESSAGE_RENDER: MethodIds = MethodIds { trait_id: 196, - start_id: 14, - stop_id: 15, - interrupt_id: 16, - receive_id: 17, + method_id: 5, }; /// Wire discriminants for `coin_payment_create_purse`. -pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_CREATE_PURSE: MethodIds = MethodIds { trait_id: 197, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `coin_payment_query_purse`. -pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_QUERY_PURSE: MethodIds = MethodIds { trait_id: 197, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `coin_payment_rebalance_purse`. -pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_REBALANCE_PURSE: MethodIds = MethodIds { trait_id: 197, - start_id: 4, - stop_id: 5, - interrupt_id: 6, - receive_id: 7, + method_id: 2, }; /// Wire discriminants for `coin_payment_delete_purse`. -pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_DELETE_PURSE: MethodIds = MethodIds { trait_id: 197, - start_id: 8, - stop_id: 9, - interrupt_id: 10, - receive_id: 11, + method_id: 3, }; /// Wire discriminants for `coin_payment_create_receivable`. -pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_CREATE_RECEIVABLE: MethodIds = MethodIds { trait_id: 197, - request_id: 12, - response_id: 13, + method_id: 4, }; /// Wire discriminants for `coin_payment_create_cheque`. -pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { +pub const COIN_PAYMENT_CREATE_CHEQUE: MethodIds = MethodIds { trait_id: 197, - request_id: 14, - response_id: 15, + method_id: 5, }; /// Wire discriminants for `coin_payment_deposit`. -pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_DEPOSIT: MethodIds = MethodIds { trait_id: 197, - start_id: 16, - stop_id: 17, - interrupt_id: 18, - receive_id: 19, + method_id: 6, }; /// Wire discriminants for `coin_payment_refund`. -pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_REFUND: MethodIds = MethodIds { trait_id: 197, - start_id: 20, - stop_id: 21, - interrupt_id: 22, - receive_id: 23, + method_id: 7, }; /// Wire discriminants for `coin_payment_listen_for_payment`. -pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { +pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: MethodIds = MethodIds { trait_id: 197, - start_id: 24, - stop_id: 25, - interrupt_id: 26, - receive_id: 27, + method_id: 8, }; /// Wire discriminants for `entropy_derive`. -pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { +pub const ENTROPY_DERIVE: MethodIds = MethodIds { trait_id: 198, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `local_storage_read`. -pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { +pub const LOCAL_STORAGE_READ: MethodIds = MethodIds { trait_id: 199, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `local_storage_write`. -pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { +pub const LOCAL_STORAGE_WRITE: MethodIds = MethodIds { trait_id: 199, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `local_storage_clear`. -pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { +pub const LOCAL_STORAGE_CLEAR: MethodIds = MethodIds { trait_id: 199, - request_id: 4, - response_id: 5, + method_id: 2, }; /// Wire discriminants for `notifications_send_push_notification`. -pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: MethodIds = MethodIds { trait_id: 200, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `notifications_cancel_push_notification`. -pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: MethodIds = MethodIds { trait_id: 200, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `payment_balance_subscribe`. -pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const PAYMENT_BALANCE_SUBSCRIBE: MethodIds = MethodIds { trait_id: 201, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `payment_top_up`. -pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { +pub const PAYMENT_TOP_UP: MethodIds = MethodIds { trait_id: 201, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `payment_request`. -pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { +pub const PAYMENT_REQUEST: MethodIds = MethodIds { trait_id: 201, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `payment_status_subscribe`. -pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const PAYMENT_STATUS_SUBSCRIBE: MethodIds = MethodIds { trait_id: 201, - start_id: 8, - stop_id: 9, - interrupt_id: 10, - receive_id: 11, + method_id: 3, }; /// Wire discriminants for `permissions_request_device_permission`. -pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: MethodIds = MethodIds { trait_id: 202, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `permissions_request_remote_permission`. -pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: MethodIds = MethodIds { trait_id: 202, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `preimage_lookup_subscribe`. -pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const PREIMAGE_LOOKUP_SUBSCRIBE: MethodIds = MethodIds { trait_id: 203, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `preimage_submit`. -pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { +pub const PREIMAGE_SUBMIT: MethodIds = MethodIds { trait_id: 203, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `resource_allocation_request`. -pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { +pub const RESOURCE_ALLOCATION_REQUEST: MethodIds = MethodIds { trait_id: 204, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `signing_create_transaction`. -pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { +pub const SIGNING_CREATE_TRANSACTION: MethodIds = MethodIds { trait_id: 205, - request_id: 0, - response_id: 1, + method_id: 0, }; /// Wire discriminants for `signing_create_transaction_with_legacy_account`. -pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { trait_id: 205, - request_id: 2, - response_id: 3, + method_id: 1, }; /// Wire discriminants for `signing_sign_raw_with_legacy_account`. -pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { trait_id: 205, - request_id: 4, - response_id: 5, + method_id: 2, }; /// Wire discriminants for `signing_sign_payload_with_legacy_account`. -pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { trait_id: 205, - request_id: 6, - response_id: 7, + method_id: 3, }; /// Wire discriminants for `signing_sign_raw`. -pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_RAW: MethodIds = MethodIds { trait_id: 205, - request_id: 8, - response_id: 9, + method_id: 4, }; /// Wire discriminants for `signing_sign_payload`. -pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { +pub const SIGNING_SIGN_PAYLOAD: MethodIds = MethodIds { trait_id: 205, - request_id: 10, - response_id: 11, + method_id: 5, }; /// Wire discriminants for `statement_store_subscribe`. -pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const STATEMENT_STORE_SUBSCRIBE: MethodIds = MethodIds { trait_id: 206, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `statement_store_create_proof`. -pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { +pub const STATEMENT_STORE_CREATE_PROOF: MethodIds = MethodIds { trait_id: 206, - request_id: 4, - response_id: 5, + method_id: 1, }; /// Wire discriminants for `statement_store_submit`. -pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { +pub const STATEMENT_STORE_SUBMIT: MethodIds = MethodIds { trait_id: 206, - request_id: 6, - response_id: 7, + method_id: 2, }; /// Wire discriminants for `statement_store_create_proof_authorized`. -pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: MethodIds = MethodIds { trait_id: 206, - request_id: 8, - response_id: 9, + method_id: 3, }; /// Wire discriminants for `theme_subscribe`. -pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const THEME_SUBSCRIBE: MethodIds = MethodIds { trait_id: 207, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// Wire discriminants for `locale_subscribe`. -pub const LOCALE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { +pub const LOCALE_SUBSCRIBE: MethodIds = MethodIds { trait_id: 208, - start_id: 0, - stop_id: 1, - interrupt_id: 2, - receive_id: 3, + method_id: 0, }; /// The full wire table. Trait ids and per-trait method ordering are diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index 54732245a..5fec7e87d 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -1400,19 +1400,20 @@ mod tests { sink.clone(), ); let ids = crate::frame::request_ids("chat_create_room").expect("known Chat request"); - let request = truapi::versioned::chat::HostChatCreateRoomRequest::V1( - v01::HostChatCreateRoomRequest { - room_id: "room".into(), - name: "Room".into(), - icon: String::new(), - }, - ); + let request = v01::HostChatCreateRoomRequest { + room_id: "room".into(), + name: "Room".into(), + icon: String::new(), + }; + // [version=0, direction=Request=0][request bytes] + let mut value = vec![0x00, 0x00]; + value.extend(request.encode()); let frame = ProtocolMessage { request_id: "chat:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value, }, }; @@ -1422,11 +1423,13 @@ mod tests { assert_eq!(frames.len(), 1); let response = ProtocolMessage::decode(&mut frames[0].as_slice()).unwrap(); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - let expected = crate::frame::encode_versioned_err_payload( - truapi::CallError::::Denied, - 1, - ); + assert_eq!(response.payload.method_id, ids.method_id); + let expected = truapi::versioned::chat::HostChatCreateRoomVersion::V1( + truapi::versioned::Request::Response(Err(truapi::CallError::< + v01::HostChatCreateRoomError, + >::Denied)), + ) + .encode(); assert_eq!(response.payload.value, expected); } @@ -1442,19 +1445,20 @@ mod tests { sink.clone(), ); let ids = crate::frame::request_ids("chat_register_bot").expect("known Chat request"); - let request = truapi::versioned::chat::HostChatRegisterBotRequest::V1( - v01::HostChatRegisterBotRequest { - bot_id: "bot".into(), - name: "Bot".into(), - icon: String::new(), - }, - ); + let request = v01::HostChatRegisterBotRequest { + bot_id: "bot".into(), + name: "Bot".into(), + icon: String::new(), + }; + // [version=0, direction=Request=0][request bytes] + let mut value = vec![0x00, 0x00]; + value.extend(request.encode()); let frame = ProtocolMessage { request_id: "chat:bot".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value, }, }; @@ -1464,11 +1468,13 @@ mod tests { assert_eq!(frames.len(), 1); let response = ProtocolMessage::decode(&mut frames[0].as_slice()).unwrap(); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - let expected = crate::frame::encode_versioned_err_payload( - truapi::CallError::::Denied, - 1, - ); + assert_eq!(response.payload.method_id, ids.method_id); + let expected = truapi::versioned::chat::HostChatRegisterBotVersion::V1( + truapi::versioned::Request::Response(Err(truapi::CallError::< + v01::HostChatRegisterBotError, + >::Denied)), + ) + .encode(); assert_eq!(response.payload.value, expected); } @@ -1488,8 +1494,9 @@ mod tests { request_id: "chat:actions".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.start_id, - value: Vec::new(), + method_id: ids.method_id, + // [version=0, direction=Start=0], no start payload. + value: vec![0x00, 0x00], }, }; @@ -1500,8 +1507,14 @@ mod tests { let response = ProtocolMessage::decode(&mut frames[0].as_slice()).unwrap(); assert_eq!(response.request_id, "chat:actions"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.interrupt_id); - assert!(response.payload.value.is_empty()); + assert_eq!(response.payload.method_id, ids.method_id); + let expected = truapi::versioned::chat::HostChatActionSubscribeVersion::V1( + truapi::versioned::Subscription::Interrupt(Some( + truapi::CallError::::Denied, + )), + ) + .encode(); + assert_eq!(response.payload.value, expected); } #[test] @@ -1527,8 +1540,9 @@ mod tests { request_id: "theme:1".to_string(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.start_id, - value: Vec::new(), + method_id: ids.method_id, + // [version=0, direction=Start=0], no start payload. + value: vec![0x00, 0x00], }, }; futures::executor::block_on(runtime.receive_frame(frame.encode())).unwrap(); diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 960e5bbb7..37206e850 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -3171,8 +3171,6 @@ mod tests { use futures::SinkExt; use parity_scale_codec::Decode; use tokio_tungstenite::tungstenite::Message as WsMessage; - use truapi::versioned::permissions::HostDevicePermissionRequest; - use truapi::versioned::system::HostFeatureSupportedRequest; use crate::frame::{Payload, ProtocolMessage, request_ids}; @@ -3326,15 +3324,15 @@ mod tests { let (feature_response, permission_response) = rt.block_on(async { let (mut ws, _) = tokio_tungstenite::connect_async(&url).await.expect("dial"); + // [version=0, direction=Request=0][request bytes] + let mut permission_value = vec![0x00, 0x00]; + permission_value.extend(v01::HostDevicePermissionRequest::Camera.encode()); let permission_frame = ProtocolMessage { request_id: "p:permission".into(), payload: Payload { trait_id: permission_ids.trait_id, - method_id: permission_ids.request_id, - value: HostDevicePermissionRequest::V1( - v01::HostDevicePermissionRequest::Camera, - ) - .encode(), + method_id: permission_ids.method_id, + value: permission_value, }, }; ws.send(WsMessage::Binary(permission_frame.encode())) @@ -3353,17 +3351,20 @@ mod tests { "permission callback was not invoked" ); + // [version=0, direction=Request=0][request bytes] + let mut feature_value = vec![0x00, 0x00]; + feature_value.extend( + v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + } + .encode(), + ); let feature_frame = ProtocolMessage { request_id: "p:feature".into(), payload: Payload { trait_id: feature_ids.trait_id, - method_id: feature_ids.request_id, - value: HostFeatureSupportedRequest::V1( - v01::HostFeatureSupportedRequest::Chain { - genesis_hash: vec![0u8; 32], - }, - ) - .encode(), + method_id: feature_ids.method_id, + value: feature_value, }, }; ws.send(WsMessage::Binary(feature_frame.encode())) @@ -3413,7 +3414,7 @@ mod tests { assert_eq!(feature_response.request_id, "p:feature"); assert_eq!(feature_response.payload.trait_id, feature_ids.trait_id); - assert_eq!(feature_response.payload.method_id, feature_ids.response_id); + assert_eq!(feature_response.payload.method_id, feature_ids.method_id); assert_eq!(permission_response.request_id, "p:permission"); assert_eq!( @@ -3422,10 +3423,13 @@ mod tests { ); assert_eq!( permission_response.payload.method_id, - permission_ids.response_id + permission_ids.method_id + ); + // [version=0, direction=Response=1][Ok=0x00][granted=1] + assert_eq!( + permission_response.payload.value, + vec![0x00, 0x01, 0x00, 0x01] ); - // [Ok 0x00][V1 0x00][granted=1] - assert_eq!(permission_response.payload.value, vec![0x00, 0x00, 0x01]); execution.stop_ws_bridge(); } diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index d28db3b8f..122a164ff 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -21,9 +21,11 @@ use truapi::v01; use crate::frame::{ IdFactory, PROTOCOL_ERROR_KEY, Payload, ProtocolErrorV1, ProtocolMessage, - VersionedProtocolError, decode_protocol_error_payload, + VersionedProtocolError, decode_protocol_error_payload, encode_envelope_clean_interrupt, + encode_envelope_stop, is_subscription_interrupt, is_subscription_receive, + split_subscription_direction, }; -use crate::generated::wire_table::SubscriptionFrameIds; +use crate::generated::wire_table::MethodIds; use crate::transport::Transport; type StopFn = Box; @@ -151,13 +153,15 @@ impl SubscriptionManager { /// items as `_receive` frames until the stream ends or `_stop` is /// received. No-ops without starting the stream if the reservation was /// cancelled by a `_stop` or superseded by a newer reservation for the - /// same id. + /// same id. `version` is the wire-envelope version the caller's `Start` + /// frame negotiated, needed to encode a version-correct natural- + /// completion frame with no per-method type knowledge. pub fn activate( &self, token: ReservationToken, trait_id: u8, - receive_id: u8, - interrupt_id: u8, + method_id: u8, + version: u8, mut stream: SubscriptionStream, transport: Arc, ) { @@ -214,7 +218,7 @@ impl SubscriptionManager { request_id: rid.clone(), payload: Payload { trait_id, - method_id: receive_id, + method_id, value, }, }) @@ -224,7 +228,7 @@ impl SubscriptionManager { request_id: rid.clone(), payload: Payload { trait_id, - method_id: interrupt_id, + method_id, value, }, }); @@ -256,8 +260,8 @@ impl SubscriptionManager { request_id, payload: Payload { trait_id, - method_id: interrupt_id, - value: Vec::new(), + method_id, + value: encode_envelope_clean_interrupt(version), }, }); } @@ -272,13 +276,13 @@ impl SubscriptionManager { &self, request_id: String, trait_id: u8, - receive_id: u8, - interrupt_id: u8, + method_id: u8, + version: u8, stream: SubscriptionStream, transport: Arc, ) { let token = self.reserve(request_id); - self.activate(token, trait_id, receive_id, interrupt_id, stream, transport); + self.activate(token, trait_id, method_id, version, stream, transport); } /// Handle a `_stop` frame from the product side. Cancels a live @@ -329,7 +333,7 @@ enum HostInitiatedFrame { } struct HostInitiatedSlot { - ids: SubscriptionFrameIds, + ids: MethodIds, sender: mpsc::UnboundedSender, } @@ -366,10 +370,16 @@ impl HostInitiatedSubscriptionManager { } } - /// Start one typed subscription and send its `_start` frame to the product. + /// Start one typed subscription and send its `_start` frame to the + /// product. `payload` is already the SCALE-encoded `{Method}Version` + /// envelope value (`V{version}(Subscription::Start(request))`), + /// constructed by the generated caller, which is why `version` — needed + /// only for the eventual `Stop` frame — is passed alongside it rather + /// than derived here. pub fn start( &self, - ids: SubscriptionFrameIds, + ids: MethodIds, + version: u8, payload: Vec, transport: Arc, ) -> truapi::Subscription> @@ -396,7 +406,7 @@ impl HostInitiatedSubscriptionManager { request_id: request_id.clone(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.start_id, + method_id: ids.method_id, value: payload, }, }); @@ -404,6 +414,7 @@ impl HostInitiatedSubscriptionManager { truapi::Subscription::new(Box::pin(HostInitiatedSubscription:: { request_id, ids, + version, receiver, state: self.state.clone(), transport, @@ -440,7 +451,7 @@ impl HostInitiatedSubscriptionManager { }; // Only OUR start frame going unsupported ends this render; an error // about any other pair belongs to a different subscription. - if (trait_id, method_id) != (slot.ids.trait_id, slot.ids.start_id) { + if (trait_id, method_id) != (slot.ids.trait_id, slot.ids.method_id) { return None; } let sender = slot.sender.clone(); @@ -448,14 +459,17 @@ impl HostInitiatedSubscriptionManager { state.active.remove(&message.request_id); return None; } - if message.payload.trait_id != slot.ids.trait_id { + if message.payload.trait_id != slot.ids.trait_id + || message.payload.method_id != slot.ids.method_id + { return None; } - if message.payload.method_id == slot.ids.receive_id { + if is_subscription_receive(&message.payload.value) { let sender = slot.sender.clone(); drop(state); - let _ = sender.unbounded_send(HostInitiatedFrame::Item(message.payload.value)); - } else if message.payload.method_id == slot.ids.interrupt_id { + let (_, item_bytes) = split_subscription_direction(&message.payload.value)?; + let _ = sender.unbounded_send(HostInitiatedFrame::Item(item_bytes.to_vec())); + } else if is_subscription_interrupt(&message.payload.value) { // Deliver the terminal before dropping the sender, so the stream // reports a declining product rather than a silent end. let sender = slot.sender.clone(); @@ -478,7 +492,8 @@ impl HostInitiatedSubscriptionManager { struct HostInitiatedSubscription { request_id: String, - ids: SubscriptionFrameIds, + ids: MethodIds, + version: u8, receiver: mpsc::UnboundedReceiver, state: Arc>, transport: Arc, @@ -504,8 +519,8 @@ impl HostInitiatedSubscription { request_id: self.request_id.clone(), payload: Payload { trait_id: self.ids.trait_id, - method_id: self.ids.stop_id, - value: Vec::new(), + method_id: self.ids.method_id, + value: encode_envelope_stop(self.version), }, }); } @@ -642,23 +657,24 @@ mod tests { )) } - fn host_ids() -> SubscriptionFrameIds { - SubscriptionFrameIds { + fn host_ids() -> MethodIds { + MethodIds { trait_id: 195, - start_id: 14, - stop_id: 15, - interrupt_id: 16, - receive_id: 17, + method_id: 14, } } - /// Product frame on [`host_ids`]'s trait, carrying one of its method ids. - fn host_frame(request_id: &str, method_id: u8, value: Vec) -> ProtocolMessage { + /// Product frame on [`host_ids`]'s address, tagged `direction` + /// (`Start`=0, `Stop`=1, `Interrupt`=2, `Receive`=3) with `inner` as the + /// direction's own payload — matching `[version, direction, ...inner]`. + fn host_frame(request_id: &str, direction: u8, inner: Vec) -> ProtocolMessage { + let mut value = vec![0, direction]; + value.extend(inner); ProtocolMessage { request_id: request_id.into(), payload: Payload { trait_id: host_ids().trait_id, - method_id, + method_id: host_ids().method_id, value, }, } @@ -669,7 +685,7 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut subscription = manager.start::(host_ids(), vec![0xaa], transport); + let mut subscription = manager.start::(host_ids(), 1, vec![0xaa], transport); assert_eq!(transport_typed.sent()[0].request_id, "h:1"); assert_eq!(transport_typed.sent()[0].payload.trait_id, 195); @@ -678,7 +694,7 @@ mod tests { assert!( manager - .handle_message(host_frame("h:1", 17, 7_u32.encode())) + .handle_message(host_frame("h:1", 3, 7_u32.encode())) .is_none() ); assert_eq!( @@ -691,8 +707,8 @@ mod tests { assert_eq!(frames.len(), 2); assert_eq!(frames[1].request_id, "h:1"); assert_eq!(frames[1].payload.trait_id, 195); - assert_eq!(frames[1].payload.method_id, 15); - assert!(frames[1].payload.value.is_empty()); + assert_eq!(frames[1].payload.method_id, 14); + assert_eq!(frames[1].payload.value, encode_envelope_stop(1)); } #[test] @@ -705,24 +721,20 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut nested = manager.start::(host_ids(), vec![], transport.clone()); - let mut healthy = manager.start::(host_ids(), vec![], transport); + let mut nested = manager.start::(host_ids(), 1, vec![], transport.clone()); + let mut healthy = manager.start::(host_ids(), 1, vec![], transport); // One `Deeper` byte per level, terminated by `Leaf`. let mut bomb = vec![0x01; (MAX_SUBSCRIPTION_DECODE_DEPTH as usize) * 4]; bomb.push(0x00); - manager.handle_message(host_frame("h:1", host_ids().receive_id, bomb)); + manager.handle_message(host_frame("h:1", 3, bomb)); assert!(matches!( futures::executor::block_on(nested.next()), Some(Err(_)) )); // A payload inside the bound still arrives, on its own subscription. - manager.handle_message(host_frame( - "h:2", - host_ids().receive_id, - NestedItem::Leaf.encode(), - )); + manager.handle_message(host_frame("h:2", 3, NestedItem::Leaf.encode())); assert_eq!( futures::executor::block_on(healthy.next()), Some(Ok(NestedItem::Leaf)) @@ -780,11 +792,11 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut malformed = manager.start::(host_ids(), vec![], transport.clone()); - let mut healthy = manager.start::(host_ids(), vec![], transport); + let mut malformed = manager.start::(host_ids(), 1, vec![], transport.clone()); + let mut healthy = manager.start::(host_ids(), 1, vec![], transport); - manager.handle_message(host_frame("h:1", 17, vec![0xff])); - manager.handle_message(host_frame("h:2", 17, 9_u32.encode())); + manager.handle_message(host_frame("h:1", 3, vec![0xff])); + manager.handle_message(host_frame("h:2", 3, 9_u32.encode())); // A partial tree left on screen as final is the failure this prevents. assert!(matches!( @@ -795,7 +807,7 @@ mod tests { assert_eq!(futures::executor::block_on(healthy.next()), Some(Ok(9))); assert_eq!(transport_typed.sent()[2].request_id, "h:1"); assert_eq!(transport_typed.sent()[2].payload.trait_id, 195); - assert_eq!(transport_typed.sent()[2].payload.method_id, 15); + assert_eq!(transport_typed.sent()[2].payload.method_id, 14); } #[test] @@ -803,9 +815,9 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut declined = manager.start::(host_ids(), vec![], transport); + let mut declined = manager.start::(host_ids(), 1, vec![], transport); - manager.handle_message(host_frame("h:1", 16, vec![0])); + manager.handle_message(host_frame("h:1", 2, vec![0])); assert!(matches!( futures::executor::block_on(declined.next()), @@ -820,7 +832,7 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut unsupported = manager.start::(host_ids(), vec![], transport); + let mut unsupported = manager.start::(host_ids(), 1, vec![], transport); manager.handle_message(ProtocolMessage { request_id: "h:1".into(), @@ -829,7 +841,7 @@ mod tests { method_id: PROTOCOL_ERROR_METHOD_ID, value: VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { trait_id: host_ids().trait_id, - method_id: host_ids().start_id, + method_id: host_ids().method_id, }) .encode(), }, @@ -850,12 +862,15 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut render = manager.start::(host_ids(), vec![], transport); + let mut render = manager.start::(host_ids(), 1, vec![], transport); for value in [ + // A different address entirely (direction no longer distinguishes + // addresses under the nested envelope, so "unrelated" now means a + // different (trait, method) pair). VersionedProtocolError::V1(ProtocolErrorV1::UnsupportedMessage { trait_id: host_ids().trait_id, - method_id: host_ids().stop_id, + method_id: host_ids().method_id + 1, }) .encode(), vec![0, 0], @@ -871,14 +886,7 @@ mod tests { } assert_eq!(render.next().now_or_never(), None); - manager.handle_message(ProtocolMessage { - request_id: "h:1".into(), - payload: Payload { - trait_id: host_ids().trait_id, - method_id: host_ids().receive_id, - value: 7_u32.encode(), - }, - }); + manager.handle_message(host_frame("h:1", 3, 7_u32.encode())); assert_eq!(futures::executor::block_on(render.next()), Some(Ok(7))); } @@ -887,7 +895,7 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut render = manager.start::(host_ids(), vec![], transport); + let mut render = manager.start::(host_ids(), 1, vec![], transport); manager.close(); @@ -899,14 +907,14 @@ mod tests { let transport_typed = Arc::new(RecordingTransport::new()); let transport: Arc = transport_typed.clone(); let manager = HostInitiatedSubscriptionManager::new(); - let mut render = manager.start::(host_ids(), vec![], transport.clone()); + let mut render = manager.start::(host_ids(), 1, vec![], transport.clone()); manager.close(); assert_eq!(futures::executor::block_on(render.next()), None); assert_eq!(transport_typed.sent().len(), 1); - let mut after_close = manager.start::(host_ids(), vec![], transport); + let mut after_close = manager.start::(host_ids(), 1, vec![], transport); assert_eq!(futures::executor::block_on(after_close.next()), None); assert_eq!(transport_typed.sent().len(), 1); } @@ -916,18 +924,18 @@ mod tests { let first_transport_typed = Arc::new(RecordingTransport::new()); let first_transport: Arc = first_transport_typed.clone(); let first = HostInitiatedSubscriptionManager::new(); - let mut first_render = first.start::(host_ids(), vec![], first_transport); + let mut first_render = first.start::(host_ids(), 1, vec![], first_transport); let second_transport_typed = Arc::new(RecordingTransport::new()); let second_transport: Arc = second_transport_typed.clone(); let second = HostInitiatedSubscriptionManager::new(); - let mut second_render = second.start::(host_ids(), vec![], second_transport); + let mut second_render = second.start::(host_ids(), 1, vec![], second_transport); assert_eq!(first_transport_typed.sent()[0].request_id, "h:1"); assert_eq!(second_transport_typed.sent()[0].request_id, "h:1"); - first.handle_message(host_frame("h:1", 17, 7_u32.encode())); - second.handle_message(host_frame("h:1", 17, 9_u32.encode())); + first.handle_message(host_frame("h:1", 3, 7_u32.encode())); + second.handle_message(host_frame("h:1", 3, 9_u32.encode())); assert_eq!( futures::executor::block_on(first_render.next()), @@ -969,7 +977,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let slow_stream: SubscriptionStream = Box::pin(stream::pending()); - manager.register("p:1".to_string(), 7, 99, 98, slow_stream, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 1, slow_stream, transport_dyn); manager.handle_stop("p:1"); // Give the worker thread a beat to observe the cancel. std::thread::sleep(std::time::Duration::from_millis(50)); @@ -987,7 +995,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let items = dummy_stream(vec![vec![0xaa], vec![0xbb]]); - manager.register("p:1".to_string(), 7, 99, 98, items, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 1, items, transport_dyn); let observed = transport_typed.wait_for(3, std::time::Duration::from_secs(2)); assert_eq!(observed, 3, "expected 2 receive frames + 1 interrupt"); let frames = transport_typed.sent(); @@ -997,8 +1005,8 @@ mod tests { assert_eq!(frames[1].payload.method_id, 99); assert_eq!(frames[1].payload.value, vec![0xbb]); assert_eq!(frames[2].payload.trait_id, 7); - assert_eq!(frames[2].payload.method_id, 98); - assert_eq!(frames[2].payload.value, Vec::::new()); + assert_eq!(frames[2].payload.method_id, 99); + assert_eq!(frames[2].payload.value, encode_envelope_clean_interrupt(1)); } /// Calling `handle_stop` twice on the same request id must be a @@ -1010,7 +1018,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let slow_stream: SubscriptionStream = Box::pin(stream::pending()); - manager.register("p:1".to_string(), 7, 99, 98, slow_stream, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 1, slow_stream, transport_dyn); manager.handle_stop("p:1"); // Second call must not panic and must not emit any frame. manager.handle_stop("p:1"); @@ -1037,7 +1045,7 @@ mod tests { let transport_dyn: Arc = transport_typed.clone(); let manager = SubscriptionManager::new(spawner); let items = dummy_stream(vec![vec![0xcc]]); - manager.register("p:1".to_string(), 7, 99, 98, items, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 1, items, transport_dyn); // Wait for the worker future to drain to completion so we know // the spawner closure ran on this path. @@ -1060,7 +1068,7 @@ mod tests { let token = manager.reserve("p:1".to_string()); manager.handle_stop("p:1"); let items = dummy_stream(vec![vec![0x01], vec![0x02]]); - manager.activate(token, 7, 99, 98, items, transport_dyn); + manager.activate(token, 7, 99, 1, items, transport_dyn); std::thread::sleep(std::time::Duration::from_millis(50)); assert!( transport_typed.sent().is_empty(), @@ -1081,11 +1089,11 @@ mod tests { // First subscription never yields; the second reservation for the // same id must stop it. let pending: SubscriptionStream = Box::pin(stream::pending()); - manager.register("p:1".to_string(), 7, 99, 98, pending, transport_dyn.clone()); + manager.register("p:1".to_string(), 7, 99, 1, pending, transport_dyn.clone()); // Second subscription yields one item then ends. let items = dummy_stream(vec![vec![0xaa]]); - manager.register("p:1".to_string(), 7, 99, 98, items, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 1, items, transport_dyn); // Exactly the second stream's frames appear: one receive + one // completion interrupt. The first (pending) stream contributes none. @@ -1099,7 +1107,7 @@ mod tests { assert_eq!(frames[0].payload.method_id, 99); assert_eq!(frames[0].payload.value, vec![0xaa]); assert_eq!(frames[1].payload.trait_id, 7); - assert_eq!(frames[1].payload.method_id, 98); + assert_eq!(frames[1].payload.method_id, 99); manager.handle_stop("p:1"); std::thread::sleep(std::time::Duration::from_millis(50)); @@ -1120,7 +1128,7 @@ mod tests { dropped: dropped.clone(), }); - manager.register("p:1".to_string(), 7, 99, 98, stream, transport_dyn); + manager.register("p:1".to_string(), 7, 99, 1, stream, transport_dyn); manager.cancel_all(); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); diff --git a/rust/crates/truapi-server/src/ws_bridge.rs b/rust/crates/truapi-server/src/ws_bridge.rs index 5deb96e75..685c993bf 100644 --- a/rust/crates/truapi-server/src/ws_bridge.rs +++ b/rust/crates/truapi-server/src/ws_bridge.rs @@ -507,7 +507,6 @@ mod tests { use parity_scale_codec::Decode; use parity_scale_codec::Encode; use truapi::v01; - use truapi::versioned::system::HostFeatureSupportedRequest; use truapi_platform::{HostInfo, PlatformInfo, ProductContext, SigningHostConfig}; use crate::SigningHostRuntime; @@ -636,17 +635,20 @@ mod tests { let response_bytes = rt.block_on(async { let (mut ws, _) = tokio_tungstenite::connect_async(&url).await.expect("dial"); + // [version=0, direction=Request=0][request bytes] + let mut value = vec![0x00, 0x00]; + value.extend( + v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + } + .encode(), + ); let request_frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: HostFeatureSupportedRequest::V1( - v01::HostFeatureSupportedRequest::Chain { - genesis_hash: vec![0u8; 32], - }, - ) - .encode(), + method_id: ids.method_id, + value, }, }; ws.send(WsMessage::Binary(request_frame.encode())) @@ -667,10 +669,9 @@ mod tests { let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); assert_eq!(response.request_id, "p:1"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - // Wire payload is `Result`-shaped: - // [Ok disc=0x00][V1 variant 0x00][supported=1] - assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); + assert_eq!(response.payload.method_id, ids.method_id); + // [version=0, direction=Response=1][Ok disc=0x00][supported=1] + assert_eq!(response.payload.value, vec![0x00, 0x01, 0x00, 0x01]); bridge.stop(); } diff --git a/rust/crates/truapi-server/tests/device_permission_revalidation.rs b/rust/crates/truapi-server/tests/device_permission_revalidation.rs index eebf2f104..987f262a1 100644 --- a/rust/crates/truapi-server/tests/device_permission_revalidation.rs +++ b/rust/crates/truapi-server/tests/device_permission_revalidation.rs @@ -17,7 +17,6 @@ use std::sync::{Arc, Mutex}; use parity_scale_codec::{Decode, Encode}; use truapi::v01; -use truapi::versioned::permissions::HostDevicePermissionRequest; use truapi_platform::{DevicePermissionStatus, PermissionStatusHost}; use truapi_platform::{PermissionAuthorizationRequest, PermissionAuthorizationStatus}; use truapi_server::frame::{Payload, ProtocolMessage, request_ids}; @@ -69,13 +68,15 @@ fn request_camera(status: Option>) -> bool { let product_runtime = runtime.product_runtime(product, sink.clone()); let ids = request_ids("permissions_request_device_permission").expect("known request method"); + // [version=0, direction=Request=0][request bytes] + let mut value = vec![0x00, 0x00]; + value.extend(v01::HostDevicePermissionRequest::Camera.encode()); let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: HostDevicePermissionRequest::V1(v01::HostDevicePermissionRequest::Camera) - .encode(), + method_id: ids.method_id, + value, }, }; futures::executor::block_on(product_runtime.receive_frame(frame.encode())) @@ -86,14 +87,15 @@ fn request_camera(status: Option>) -> bool { .iter() .map(|bytes| ProtocolMessage::decode(&mut &bytes[..]).expect("decode emitted frame")) .find(|message| { - message.payload.trait_id == ids.trait_id && message.payload.method_id == ids.response_id + message.payload.trait_id == ids.trait_id && message.payload.method_id == ids.method_id }) .expect("dispatcher emitted a device-permission response"); - // Wire payload is [version disc][Ok disc][body]. Assert the whole thing - // against each possible answer rather than splicing bytes out by index. + // Wire payload is [version disc][direction=Response][Ok disc][body]. + // Assert the whole thing against each possible answer rather than + // splicing bytes out by index. for granted in [true, false] { - let mut expected = vec![0x00u8, 0x00u8]; + let mut expected = vec![0x00u8, 0x01u8, 0x00u8]; v01::HostDevicePermissionResponse { granted }.encode_to(&mut expected); if response.payload.value == expected { return granted; diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs index 849fd5bd6..0b468685a 100644 --- a/rust/crates/truapi-server/tests/golden_frame.rs +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -13,17 +13,18 @@ //! //! The frame encodes: //! requestId = "p:1" -//! payload = account_get_account_request, -//! inner = HostAccountGetRequest::V1(ProductAccountId { +//! payload = account_get_account, +//! inner = HostAccountGetVersion::V1(Request::Request(ProductAccountId { //! dot_ns_identifier: "foo", //! derivation_index: DerivationIndex::Index(0), -//! }) +//! })) //! -//! On the wire (16 bytes): +//! On the wire (17 bytes): //! [0c 70 3a 31] requestId = compact-len(3) + "p:1" //! [c2] trait discriminant 194 = account -//! [04] method discriminant 4 = get_account request -//! [00] versioned wrapper variant V1 +//! [01] method discriminant 1 = get_account +//! [00] envelope version V1 +//! [00] direction tag: Request //! [0c 66 6f 6f] compact-len(3) + "foo" //! [00] DerivationIndex variant Index //! [00 00 00 00] u32 = 0 @@ -35,25 +36,27 @@ use parity_scale_codec::{Decode, Encode}; use truapi::v01; -use truapi::versioned::account::HostAccountGetRequest; +use truapi::versioned::Request as RequestEnvelope; +use truapi::versioned::account::HostAccountGetVersion; use truapi_server::frame::{Payload, ProtocolMessage}; use truapi_server::generated::wire_table; const GOLDEN: &[u8] = include_bytes!("snapshots/golden-account-get.bin"); -/// Payload byte count of the golden frame: one versioned-wrapper variant byte, -/// a compact-length-prefixed 3-byte identifier, one `DerivationIndex` variant -/// byte, and a `u32`. Spelled out term by term rather than measured from the -/// codec, so a layout change has to move this number by hand. -const GOLDEN_PAYLOAD_LEN: usize = 1 + 1 + 3 + 1 + 4; +/// Payload byte count of the golden frame: the envelope's `[version, +/// direction]` prefix, a compact-length-prefixed 3-byte identifier, one +/// `DerivationIndex` variant byte, and a `u32`. Spelled out term by term +/// rather than measured from the codec, so a layout change has to move this +/// number by hand. +const GOLDEN_PAYLOAD_LEN: usize = 2 + 1 + 3 + 1 + 4; -fn expected_request() -> HostAccountGetRequest { - HostAccountGetRequest::V1(v01::HostAccountGetRequest { +fn expected_envelope() -> HostAccountGetVersion { + HostAccountGetVersion::V1(RequestEnvelope::Request(v01::HostAccountGetRequest { product_account_id: v01::ProductAccountId { dot_ns_identifier: "foo".to_string(), derivation_index: v01::DerivationIndex::Index(0), }, - }) + })) } #[test] @@ -65,8 +68,8 @@ fn golden_account_get_frame_decodes_to_expected_message() { request_id: "p:1".to_string(), payload: Payload { trait_id: wire_table::ACCOUNT_GET_ACCOUNT.trait_id, - method_id: wire_table::ACCOUNT_GET_ACCOUNT.request_id, - value: expected_request().encode(), + method_id: wire_table::ACCOUNT_GET_ACCOUNT.method_id, + value: expected_envelope().encode(), }, }; assert_eq!(decoded, expected); @@ -82,9 +85,9 @@ fn golden_account_get_payload_decodes_as_the_typed_request() { built against an older @parity/truapi now fails to decode" ); - let request = HostAccountGetRequest::decode(&mut &decoded.payload.value[..]) - .expect("golden payload must decode as the typed request"); - assert_eq!(request, expected_request()); + let envelope = HostAccountGetVersion::decode(&mut &decoded.payload.value[..]) + .expect("golden payload must decode as the typed envelope"); + assert_eq!(envelope, expected_envelope()); } #[test] diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin index 8bb6351121f63e01ca64b62425fd8c3925ff4618..b6357a4bb906dba2db8ba3394bceb95569dbdd38 100644 GIT binary patch literal 17 Vcmd-nurfTv$iTppmY>f60stWv0{Z{} literal 16 Ucmd-nurfTv!oZW3pU(gS03VD3{r~^~ diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index 4625180b8..249b575f8 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -1,34 +1,36 @@ //! Result-wire-shape regression test. //! -//! The TS host/client codec expects every request response to be a -//! `Versioned>` envelope on the wire (one leading version byte, -//! then one result discriminant byte, then the SCALE-encoded value). This test stands up a +//! The TS host/client codec expects every request/response and subscription +//! frame to be a nested wire envelope: one leading version byte, then one +//! direction byte (`Request`/`Response`, or a subscription's +//! `Start`/`Stop`/`Interrupt`/`Receive`), then the SCALE-encoded inner value. +//! A request/response method's `Response` direction carries a further +//! `Result` discriminant. This test stands up a //! `TrUApiCore::from_platform_with_config` with a platform whose `Features` //! impl returns `Ok(supported = true)` and asserts: //! -//! - A `system_feature_supported_request` produces a response whose -//! payload begins with `0x00` (V1), then `0x00` (Ok), followed by the encoded -//! `HostFeatureSupportedResponse`. -//! - A `local_storage_read_request` whose stub returns +//! - A `system_feature_supported` request produces a response whose payload +//! begins with `0x00` (V1), `0x01` (direction=Response), then `0x00` (Ok), +//! followed by the encoded `HostFeatureSupportedResponse`. +//! - A `local_storage_read` request whose stub returns //! `Err(HostLocalStorageReadError::Full)` produces a response whose -//! payload begins with `0x00` (V1), then `0x01` (Err), followed by the encoded -//! `HostLocalStorageReadError::Full`. +//! payload begins with `0x00` (V1), `0x01` (direction=Response), then +//! `0x01` (Err), followed by the encoded `HostLocalStorageReadError::Full`. //! //! Both halves prove the wire layout stays in lockstep with the TS -//! `S.indexedTaggedUnion({ V1: S.Result(ok, err) })` codec. +//! `S.indexedTaggedUnion({ V1: S.indexedTaggedUnion({ Request, Response: +//! S.Result(ok, err) }) })` codec. use std::sync::Arc; use parity_scale_codec::{Decode, Encode}; -use truapi::versioned::system::HostFeatureSupportedRequest; -use truapi::versioned::{Versioned, account, payment, statement_store}; use truapi::{CallError, v01}; use truapi_server::core::TrUApiCore; use truapi_server::frame::{ PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, ProtocolErrorV1, ProtocolMessage, - VersionedProtocolError, request_ids, subscription_ids, + VersionedProtocolError, encode_envelope_stop, request_ids, subscription_ids, }; mod common; @@ -43,57 +45,66 @@ fn dispatch(core: &TrUApiCore, frame: ProtocolMessage) -> ProtocolMessage { ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response") } +/// Wrap already-encoded bare request (or subscription start) bytes in the +/// envelope's `[version=V1, direction=Request/Start]` prefix. `Request` and +/// `Start` share direction tag `0`, so this helper covers both. +fn envelope_request(bytes: Vec) -> Vec { + let mut value = vec![0x00u8, 0x00u8]; + value.extend(bytes); + value +} + #[test] fn feature_supported_ok_response_uses_ok_discriminant() { let core = make_core(); - let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + let request = v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], - }); + }; let ids = request_ids("system_feature_supported").expect("known request method"); let frame = ProtocolMessage { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value: envelope_request(request.encode()), }, }; let response = dispatch(&core, frame); assert_eq!(response.request_id, "p:1"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); + assert_eq!(response.payload.method_id, ids.method_id); - // Wire payload: [V1 disc=0x00][Ok disc=0x00][encoded response body]. - let mut expected = vec![0x00u8, 0x00u8]; + // Wire payload: [V1=0x00][direction=Response=0x01][Ok disc=0x00][encoded response body]. + let mut expected = vec![0x00u8, 0x01u8, 0x00u8]; v01::HostFeatureSupportedResponse { supported: true }.encode_to(&mut expected); assert_eq!(response.payload.value, expected); assert_eq!(response.payload.value.first(), Some(&0x00)); - assert_eq!(response.payload.value.get(1), Some(&0x00)); + assert_eq!(response.payload.value.get(1), Some(&0x01)); + assert_eq!(response.payload.value.get(2), Some(&0x00)); } #[test] fn get_chain_info_ok_response_round_trips_over_the_wire() { let core = make_core(); - let request = - truapi::versioned::chain::RemoteChainInfoRequest::V1(v01::RemoteChainInfoRequest { - chain: v01::ChainIdentifier::AssetHub, - }); + let request = v01::RemoteChainInfoRequest { + chain: v01::ChainIdentifier::AssetHub, + }; let ids = request_ids("chain_get_chain_info").expect("known request method"); let frame = ProtocolMessage { request_id: "p:9".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value: envelope_request(request.encode()), }, }; let response = dispatch(&core, frame); assert_eq!(response.request_id, "p:9"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); + assert_eq!(response.payload.method_id, ids.method_id); - // Wire payload: [V1 disc=0x00][Ok disc=0x00][encoded response body]. - let mut expected = vec![0x00u8, 0x00u8]; + // Wire payload: [V1=0x00][direction=Response=0x01][Ok disc=0x00][encoded response body]. + let mut expected = vec![0x00u8, 0x01u8, 0x00u8]; v01::RemoteChainInfoResponse { network: "paseo".to_string(), chain: v01::ChainIdentifier::AssetHub, @@ -106,95 +117,82 @@ fn get_chain_info_ok_response_round_trips_over_the_wire() { #[test] fn get_chain_info_unserved_chain_uses_err_discriminant() { let core = make_core(); - let request = - truapi::versioned::chain::RemoteChainInfoRequest::V1(v01::RemoteChainInfoRequest { - chain: v01::ChainIdentifier::Bulletin, - }); + let request = v01::RemoteChainInfoRequest { + chain: v01::ChainIdentifier::Bulletin, + }; let ids = request_ids("chain_get_chain_info").expect("known request method"); let frame = ProtocolMessage { request_id: "p:10".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value: envelope_request(request.encode()), }, }; let response = dispatch(&core, frame); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); + assert_eq!(response.payload.method_id, ids.method_id); - // Wire payload: [V1 disc=0x00][Err disc=0x01][encoded domain error]. - let mut expected = vec![0x00u8, 0x01u8]; - CallError::Domain(truapi::versioned::chain::RemoteChainInfoError::V1( - v01::RemoteChainInfoError::NotSupported, - )) - .encode_to(&mut expected); - assert_eq!(response.payload.value, expected); + // Wire payload: [V1=0x00][direction=Response=0x01][Err disc=0x01][encoded domain error]. + assert_eq!( + response.payload.value, + versioned_result_err_payload(v01::RemoteChainInfoError::NotSupported) + ); } #[test] fn local_storage_read_err_response_uses_err_discriminant() { let core = make_core(); - let request = truapi::versioned::local_storage::HostLocalStorageReadRequest::V1( - v01::HostLocalStorageReadRequest { - key: "missing".to_string(), - }, - ); + let request = v01::HostLocalStorageReadRequest { + key: "missing".to_string(), + }; let ids = request_ids("local_storage_read").expect("known request method"); let frame = ProtocolMessage { request_id: "p:2".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value: envelope_request(request.encode()), }, }; let response = dispatch(&core, frame); assert_eq!(response.request_id, "p:2"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - - // Wire payload: - // [V1 disc=0x00][Err disc=0x01][CallError::Domain][V1 error][encoded error body]. - let mut expected = vec![0x00u8, 0x01u8]; - CallError::Domain( - truapi::versioned::local_storage::HostLocalStorageReadError::V1( - v01::HostLocalStorageReadError::Full, - ), - ) - .encode_to(&mut expected); + assert_eq!(response.payload.method_id, ids.method_id); + + // Wire payload: [V1=0x00][direction=Response=0x01][Err disc=0x01] + // [CallError::Domain=0x00][encoded error body]. + let expected = versioned_result_err_payload(v01::HostLocalStorageReadError::Full); assert_eq!(response.payload.value, expected); assert_eq!(response.payload.value.first(), Some(&0x00)); assert_eq!(response.payload.value.get(1), Some(&0x01)); } -fn versioned_result_err_payload(error: E) -> Vec -where - E: Clone + Encode + Versioned, -{ - let mut expected = vec![version_index(error.version()), 0x01u8]; +/// Expected bytes for a request/response method's `Response` direction +/// answering with a domain error: `[V1=0x00][direction=Response=0x01] +/// [Result::Err=0x01][CallError::Domain=0x00][encoded error body]`. +fn versioned_result_err_payload(error: E) -> Vec { + let mut expected = vec![0x00u8, 0x01u8, 0x01u8]; CallError::Domain(error).encode_to(&mut expected); expected } -fn versioned_interrupt_err_payload(error: E) -> Vec -where - E: Clone + Encode + Versioned, -{ - let mut expected = vec![version_index(error.version())]; +/// Expected bytes for a subscription's `Interrupt` direction ending with a +/// domain error: `[V1=0x00][direction=Interrupt=0x02][Option::Some=0x01] +/// [CallError::Domain=0x00][encoded error body]`. +fn versioned_interrupt_err_payload(error: E) -> Vec { + let mut expected = vec![0x00u8, 0x02u8, 0x01u8]; CallError::Domain(error).encode_to(&mut expected); expected } -fn assert_request_returns_domain_error( +fn assert_request_returns_domain_error( core: &TrUApiCore, request_id: &str, method: &str, value: Vec, error: E, -) where - E: Clone + Encode + Versioned, -{ +) { let ids = request_ids(method).expect("known request method"); let response = dispatch( core, @@ -202,26 +200,24 @@ fn assert_request_returns_domain_error( request_id: request_id.into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value, + method_id: ids.method_id, + value: envelope_request(value), }, }, ); assert_eq!(response.request_id, request_id); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); + assert_eq!(response.payload.method_id, ids.method_id); assert_eq!(response.payload.value, versioned_result_err_payload(error)); } -fn assert_subscription_start_interrupts_error( +fn assert_subscription_start_interrupts_error( core: &TrUApiCore, request_id: &str, method: &str, value: Vec, error: E, -) where - E: Clone + Encode + Versioned, -{ +) { let ids = subscription_ids(method).expect("known subscription method"); let transport = Arc::new(RecordingTransport::default()); futures::executor::block_on(core.dispatch( @@ -229,8 +225,8 @@ fn assert_subscription_start_interrupts_error( request_id: request_id.into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.start_id, - value, + method_id: ids.method_id, + value: envelope_request(value), }, }, transport.clone(), @@ -240,21 +236,17 @@ fn assert_subscription_start_interrupts_error( assert_eq!(sent.len(), 1); assert_eq!(sent[0].request_id, request_id); assert_eq!(sent[0].payload.trait_id, ids.trait_id); - assert_eq!(sent[0].payload.method_id, ids.interrupt_id); + assert_eq!(sent[0].payload.method_id, ids.method_id); assert_eq!( sent[0].payload.value, versioned_interrupt_err_payload(error) ); } -fn version_index(version: u8) -> u8 { - version.saturating_sub(1) -} - #[test] fn foreign_account_proof_returns_not_allowlisted_without_confirmation() { let core = make_core(); - let request = account::HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest { + let request = v01::HostAccountCreateProofRequest { key_handle: v01::ProductAccountId { dot_ns_identifier: "peopl.dot".to_string(), derivation_index: v01::DerivationIndex::Index(0), @@ -268,7 +260,7 @@ fn foreign_account_proof_returns_not_allowlisted_without_confirmation() { junctions: vec![v01::RingLocationJunction::PalletInstance(0)], }, message: Vec::new(), - }); + }; let ids = request_ids("account_create_account_proof").expect("known request method"); let response = dispatch( @@ -277,107 +269,95 @@ fn foreign_account_proof_returns_not_allowlisted_without_confirmation() { request_id: "p:account-proof".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value: envelope_request(request.encode()), }, }, ); assert_eq!(response.request_id, "p:account-proof"); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); + assert_eq!(response.payload.method_id, ids.method_id); // RFC-0024 forbids a prompt fallback for bearer proofs made with a foreign key. - let expected = versioned_result_err_payload(account::HostAccountCreateProofError::V1( - v01::HostAccountCreateProofError::NotAllowlisted, - )); + let expected = versioned_result_err_payload(v01::HostAccountCreateProofError::NotAllowlisted); assert_eq!(response.payload.value, expected); } #[test] fn deferred_payment_requests_return_dotli_not_implemented_errors() { let core = make_core(); - let request = payment::HostPaymentRequest::V1(v01::HostPaymentRequest { + let request = v01::HostPaymentRequest { from: None, amount: 1, destination: [0u8; 32], - }); + }; assert_request_returns_domain_error( &core, "p:payment", "payment_request", request.encode(), - payment::HostPaymentError::V1(v01::HostPaymentError::Unknown { + v01::HostPaymentError::Unknown { reason: PAYMENTS_NOT_IMPLEMENTED.to_string(), - }), + }, ); - let top_up = payment::HostPaymentTopUpRequest::V1(v01::HostPaymentTopUpRequest { + let top_up = v01::HostPaymentTopUpRequest { into: None, amount: 1, source: v01::PaymentTopUpSource::ProductAccount { derivation_index: v01::DerivationIndex::Index(0), }, - }); + }; assert_request_returns_domain_error( &core, "p:top-up", "payment_top_up", top_up.encode(), - payment::HostPaymentTopUpError::V1(v01::HostPaymentTopUpError::Unknown { + v01::HostPaymentTopUpError::Unknown { reason: PAYMENTS_NOT_IMPLEMENTED.to_string(), - }), + }, ); } #[test] fn deferred_payment_subscriptions_interrupt_dotli_not_implemented_errors() { let core = make_core(); - let balance = - payment::HostPaymentBalanceSubscribeRequest::V1(v01::HostPaymentBalanceSubscribeRequest { - purse: None, - }); + let balance = v01::HostPaymentBalanceSubscribeRequest { purse: None }; assert_subscription_start_interrupts_error( &core, "p:balance", "payment_balance_subscribe", balance.encode(), - payment::HostPaymentBalanceSubscribeError::V1( - v01::HostPaymentBalanceSubscribeError::PermissionDenied, - ), + v01::HostPaymentBalanceSubscribeError::PermissionDenied, ); - let status = - payment::HostPaymentStatusSubscribeRequest::V1(v01::HostPaymentStatusSubscribeRequest { - payment_id: "payment-id".to_string(), - }); + let status = v01::HostPaymentStatusSubscribeRequest { + payment_id: "payment-id".to_string(), + }; assert_subscription_start_interrupts_error( &core, "p:status", "payment_status_subscribe", status.encode(), - payment::HostPaymentStatusSubscribeError::V1( - v01::HostPaymentStatusSubscribeError::Unknown { - reason: PAYMENTS_NOT_IMPLEMENTED.to_string(), - }, - ), + v01::HostPaymentStatusSubscribeError::Unknown { + reason: PAYMENTS_NOT_IMPLEMENTED.to_string(), + }, ); } #[test] fn statement_store_subscribe_topic_limit_interrupts_with_typed_error() { let core = make_core(); - let request = statement_store::RemoteStatementStoreSubscribeRequest::V1( - v01::RemoteStatementStoreSubscribeRequest::MatchAny(vec![[7u8; 32]; 129]), - ); + let request = v01::RemoteStatementStoreSubscribeRequest::MatchAny(vec![[7u8; 32]; 129]); assert_subscription_start_interrupts_error( &core, "p:ss-too-many", "statement_store_subscribe", request.encode(), - statement_store::RemoteStatementStoreSubscribeError::V1(v01::GenericError { + v01::GenericError { reason: "MatchAny has 129 topics, maximum is 128".to_string(), - }), + }, ); } @@ -393,7 +373,7 @@ fn malformed_result_subscription_start_interrupts_with_malformed_frame() { request_id: "p:malformed-sub".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.start_id, + method_id: ids.method_id, value: vec![0xff], }, }, @@ -404,15 +384,20 @@ fn malformed_result_subscription_start_interrupts_with_malformed_frame() { assert_eq!(sent.len(), 1); assert_eq!(sent[0].request_id, "p:malformed-sub"); assert_eq!(sent[0].payload.trait_id, ids.trait_id); - assert_eq!(sent[0].payload.method_id, ids.interrupt_id); + assert_eq!(sent[0].payload.method_id, ids.method_id); assert_eq!(sent[0].payload.value.first(), Some(&0x00)); + assert_eq!( + sent[0].payload.value.get(1), + Some(&0x02), + "direction=Interrupt" + ); - let mut payload = &sent[0].payload.value[1..]; - let error = CallError::::decode(&mut payload) + let mut payload = &sent[0].payload.value[2..]; + let error = Option::>::decode(&mut payload) .expect("decode malformed interrupt error"); assert!(payload.is_empty()); match error { - CallError::MalformedFrame { reason } => assert!(!reason.is_empty()), + Some(CallError::MalformedFrame { reason }) => assert!(!reason.is_empty()), other => panic!("expected MalformedFrame interrupt, got {other:?}"), } } @@ -498,8 +483,8 @@ fn subscription_start_receive_stop_through_wire_boundary() { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.start_id, - value: Vec::new(), + method_id: ids.method_id, + value: envelope_request(Vec::new()), }, }; futures::executor::block_on(core.dispatch(start, dyn_transport.clone())); @@ -516,7 +501,7 @@ fn subscription_start_receive_stop_through_wire_boundary() { ); assert_eq!( transport.sent.lock().unwrap()[0].payload.method_id, - ids.receive_id + ids.method_id ); // Stop the subscription, then push a session change. A live subscription @@ -525,8 +510,8 @@ fn subscription_start_receive_stop_through_wire_boundary() { request_id: "p:1".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.stop_id, - value: Vec::new(), + method_id: ids.method_id, + value: encode_envelope_stop(1), }, }; futures::executor::block_on(core.dispatch(stop, dyn_transport)); @@ -557,23 +542,22 @@ fn subscription_start_receive_stop_through_wire_boundary() { #[test] fn coin_payment_request_reports_unsupported_on_the_wire() { let core = make_core(); - let request = truapi::versioned::coin_payment::HostCoinPaymentQueryPurseRequest::V1( - v01::HostCoinPaymentQueryPurseRequest { - purse: v01::MAIN_PURSE, - }, - ); + let request = v01::HostCoinPaymentQueryPurseRequest { + purse: v01::MAIN_PURSE, + }; let ids = request_ids("coin_payment_query_purse").expect("known request method"); let frame = ProtocolMessage { request_id: "p:coin".into(), payload: Payload { trait_id: ids.trait_id, - method_id: ids.request_id, - value: request.encode(), + method_id: ids.method_id, + value: envelope_request(request.encode()), }, }; let response = dispatch(&core, frame); assert_eq!(response.payload.trait_id, ids.trait_id); - assert_eq!(response.payload.method_id, ids.response_id); - // [V1 disc=0x00][Err disc=0x01][CallError::Unsupported=0x02], and nothing more. - assert_eq!(response.payload.value, vec![0x00u8, 0x01u8, 0x02u8]); + assert_eq!(response.payload.method_id, ids.method_id); + // [V1=0x00][direction=Response=0x01][Err disc=0x01][CallError::Unsupported=0x02], + // and nothing more. + assert_eq!(response.payload.value, vec![0x00u8, 0x01u8, 0x01u8, 0x02u8]); } diff --git a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs index 3306097ac..cb80f95bc 100644 --- a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs +++ b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs @@ -1,6 +1,6 @@ //! Cross-language parity check: the Rust `WIRE_TABLE` and the TS -//! `wire-table.ts` must list the exact same -//! `(method, trait_id, request_id, response_id)` tuples in the same order. A drift here means a product built against one +//! `wire-table.ts` must list the exact same `(method, trait_id, method_id)` +//! tuples in the same order. A drift here means a product built against one //! side will fail to decode frames produced by the other. //! //! Both files are auto-generated text artifacts of `truapi-codegen`; the @@ -21,12 +21,7 @@ const RUST_TABLE: &str = include_str!("../src/generated/wire_table.rs"); struct Row { method: String, trait_id: u8, - request_or_start: u8, - response_or_receive: u8, - /// Subscription `_stop` / `_interrupt` ids; `None` for request methods. - stop: Option, - interrupt: Option, - is_subscription: bool, + method_id: u8, } /// Parse a wire id. A malformed id is a hard failure, never a silent `0`: a @@ -40,10 +35,10 @@ fn parse_id(raw: &str, method: &str) -> u8 { } fn parse_rust(src: &str) -> Vec { - // The Rust codegen emits one named `pub const FOO_BAR: RequestFrameIds = ...` - // (or `SubscriptionFrameIds`) per method. The const name is - // `SCREAMING_SNAKE_CASE` of the method name; we lowercase it to match the - // TS const names. This mirrors `parse_ts` below. + // The Rust codegen emits one named `pub const FOO_BAR: MethodIds = { ... }` + // per method. The const name is `SCREAMING_SNAKE_CASE` of the method name; + // we lowercase it to match the TS const names. This mirrors `parse_ts` + // below. let mut out = Vec::new(); let mut iter = src.lines(); while let Some(line) = iter.next() { @@ -54,17 +49,13 @@ fn parse_rust(src: &str) -> Vec { let Some(colon) = rest.find(':') else { continue; }; - let is_subscription = rest.contains("SubscriptionFrameIds"); // Skip non-id consts (e.g. `WIRE_TABLE: &[WireEntry]`). - if !is_subscription && !rest.contains("RequestFrameIds") { + if !rest.contains("MethodIds") { continue; } let method = rest[..colon].trim().to_ascii_lowercase(); let mut trait_id = None; - let mut request_or_start = None; - let mut response_or_receive = None; - let mut stop = None; - let mut interrupt = None; + let mut method_id = None; for inner in iter.by_ref() { let t = inner.trim(); if t.starts_with("};") { @@ -73,37 +64,17 @@ fn parse_rust(src: &str) -> Vec { if let Some(rest) = t.strip_prefix("trait_id: ") { trait_id = Some(parse_id(rest, &method)); } - if let Some(rest) = t - .strip_prefix("request_id: ") - .or_else(|| t.strip_prefix("start_id: ")) - { - request_or_start = Some(parse_id(rest, &method)); + if let Some(rest) = t.strip_prefix("method_id: ") { + method_id = Some(parse_id(rest, &method)); } - if let Some(rest) = t - .strip_prefix("response_id: ") - .or_else(|| t.strip_prefix("receive_id: ")) - { - response_or_receive = Some(parse_id(rest, &method)); - } - if let Some(rest) = t.strip_prefix("stop_id: ") { - stop = Some(parse_id(rest, &method)); - } - if let Some(rest) = t.strip_prefix("interrupt_id: ") { - interrupt = Some(parse_id(rest, &method)); - } - } - if let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) { - out.push(Row { - method: method.clone(), - trait_id: trait_id - .unwrap_or_else(|| panic!("missing trait_id for `{method}` in Rust table")), - request_or_start: rs, - response_or_receive: rr, - stop, - interrupt, - is_subscription, - }); } + out.push(Row { + trait_id: trait_id + .unwrap_or_else(|| panic!("missing trait_id for `{method}` in Rust table")), + method_id: method_id + .unwrap_or_else(|| panic!("missing method_id for `{method}` in Rust table")), + method, + }); } out } @@ -124,51 +95,23 @@ fn parse_ts(src: &str) -> Vec { }; let method = rest[..name_end].to_ascii_lowercase(); let mut trait_id = None; - let mut request_or_start = None; - let mut response_or_receive = None; - let mut stop = None; - let mut interrupt = None; - let mut is_subscription = false; + let mut method_id = None; for inner in iter.by_ref() { let t = inner.trim(); - if t.starts_with("start:") || t.contains("SubscriptionFrameIds") { - is_subscription = true; - } if let Some(rest) = t.strip_prefix("trait: ") { trait_id = Some(parse_id(rest, &method)); } - if let Some(rest) = t - .strip_prefix("request: ") - .or_else(|| t.strip_prefix("start: ")) - { - request_or_start = Some(parse_id(rest, &method)); - } - if let Some(rest) = t - .strip_prefix("response: ") - .or_else(|| t.strip_prefix("receive: ")) - { - response_or_receive = Some(parse_id(rest, &method)); - } - if let Some(rest) = t.strip_prefix("stop: ") { - stop = Some(parse_id(rest, &method)); - } - if let Some(rest) = t.strip_prefix("interrupt: ") { - interrupt = Some(parse_id(rest, &method)); + if let Some(rest) = t.strip_prefix("method: ") { + method_id = Some(parse_id(rest, &method)); } if t.starts_with("} as const") || t == "}" { - if let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) { - out.push(Row { - trait_id: trait_id.unwrap_or_else(|| { - panic!("missing trait id for `{method}` in TS table") - }), - method, - request_or_start: rs, - response_or_receive: rr, - stop, - interrupt, - is_subscription, - }); - } + out.push(Row { + trait_id: trait_id + .unwrap_or_else(|| panic!("missing trait id for `{method}` in TS table")), + method_id: method_id + .unwrap_or_else(|| panic!("missing method id for `{method}` in TS table")), + method, + }); break; } } From 3da97987e47efc7a8d215a3fcbacf1c4c95f409a Mon Sep 17 00:00:00 2001 From: Nidish Date: Tue, 1 Sep 2026 14:42:32 +0530 Subject: [PATCH 13/16] feat(wire): bring the TS client onto the nested envelope (RFC 0028) --- js/packages/truapi/src/client.test.ts | 261 +++++++++------- js/packages/truapi/src/client.ts | 303 +++++++++---------- js/packages/truapi/src/index.ts | 3 +- js/packages/truapi/src/scale.ts | 67 ++++ js/packages/truapi/src/transport.ts | 83 ++--- js/packages/truapi/src/wire-equality.test.ts | 35 ++- 6 files changed, 405 insertions(+), 347 deletions(-) diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 845451a18..4c789e160 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -2,15 +2,7 @@ import type { Result } from "neverthrow"; import { describe, expect, it } from "bun:test"; import { createTransport } from "./client.js"; -import { - CallError, - indexedTaggedUnion, - Result as ScaleResult, - str, - _void, - type CallErrorValue, -} from "./scale.js"; -import type { Codec } from "./scale.js"; +import { str, type CallErrorValue } from "./scale.js"; import { createClient, SubscriptionError } from "./generated/client.js"; import * as T from "./generated/types.js"; import * as W from "./generated/wire-table.js"; @@ -21,9 +13,6 @@ import { UnsupportedMessageError, } from "./transport.js"; -/** Wrap a codec in the `{ V1: [0, codec] }` indexed-tagged-union envelope. */ -const versionedV1 = (codec: Codec) => indexedTaggedUnion({ V1: [0, codec] }); - function toHex(u: Uint8Array): string { return Array.from(u) .map((b) => b.toString(16).padStart(2, "0")) @@ -70,28 +59,21 @@ function providerFixture() { }; } -/** Encode a V1 host-handshake response result payload. */ -/** Encode the `UnsupportedProtocolVersion` handshake response payload. */ -function unsupportedHandshakeResponsePayload(): Uint8Array { - return versionedV1(ScaleResult(_void, CallError(T.VersionedHostHandshakeError))).enc({ - tag: "V1", - value: { - success: false, - value: { - tag: "Domain", - value: { tag: "V1", value: { tag: "UnsupportedProtocolVersion", value: undefined } }, - }, - }, - }); -} - +/** Encode a successful V1 host-handshake response envelope. */ function handshakeResponsePayload(value: { success: true; value: undefined }): Uint8Array { - return versionedV1(ScaleResult(_void, CallError(T.VersionedHostHandshakeError))).enc({ + return T.HostHandshakeVersion.enc({ tag: "V1", - value, + value: { tag: "Response", value }, }); } +/** + * Encode a V1 `account_get_account` response envelope. `value`'s domain + * error case takes the wrapped public shape (`{tag:"Domain",value: + * T.VersionedHostAccountGetError}`) and is unwrapped to the bare error the + * merged envelope carries on the wire — mirroring what the generated client + * does in reverse when decoding a response. + */ function accountGetResponsePayload( value: | { @@ -103,9 +85,15 @@ function accountGetResponsePayload( value: { tag: "Domain"; value: T.VersionedHostAccountGetError }; }, ): Uint8Array { - return versionedV1( - ScaleResult(T.HostAccountGetResponse, CallError(T.VersionedHostAccountGetError)), - ).enc({ tag: "V1", value }); + return T.HostAccountGetVersion.enc({ + tag: "V1", + value: { + tag: "Response", + value: value.success + ? value + : { success: false, value: { tag: "Domain", value: value.value.value.value } }, + }, + }); } function rendererStart( @@ -117,10 +105,10 @@ function rendererStart( requestId, payload: { traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, - methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.start, - value: T.VersionedProductChatCustomMessageRenderRequest.enc({ + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.method, + value: T.ProductChatCustomMessageRenderVersion.enc({ tag: "V1", - value: request, + value: { tag: "Start", value: request }, }), }, }), @@ -134,10 +122,10 @@ function rendererReceive(requestId: string, node: T.CustomRendererNode): Uint8Ar requestId, payload: { traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, - methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.receive, - value: T.VersionedProductChatCustomMessageRenderItem.enc({ + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.method, + value: T.ProductChatCustomMessageRenderVersion.enc({ tag: "V1", - value: node, + value: { tag: "Receive", value: node }, }), }, }), @@ -145,14 +133,19 @@ function rendererReceive(requestId: string, node: T.CustomRendererNode): Uint8Ar ); } +/** + * The fixed frame `transport.ts` sends to decline a host-initiated render: + * `[version=V1, direction=Interrupt, Option::None]`. The host only reads the + * direction byte for this flow, so one constant frame covers every method. + */ function rendererInterrupt(requestId: string): Uint8Array { return unwrap( encodeWireMessage({ requestId, payload: { traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, - methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.interrupt, - value: new Uint8Array([0]), + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.method, + value: new Uint8Array([0, 2, 0]), }, }), "encode renderer interrupt", @@ -165,8 +158,8 @@ function rendererStop(requestId: string): Uint8Array { requestId, payload: { traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, - methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.stop, - value: new Uint8Array(), + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.method, + value: new Uint8Array([0, 1]), }, }), "encode renderer stop", @@ -217,11 +210,14 @@ describe("generated client transport", () => { }; void client.account.getAccount(request); - const expectedPayload = T.VersionedHostAccountGetRequest.enc({ tag: "V1", value: request }); + const expectedPayload = T.HostAccountGetVersion.enc({ + tag: "V1", + value: { tag: "Request", value: request }, + }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); expectedFrame[str.enc("p:1").length] = 194; // account trait - expectedFrame[str.enc("p:1").length + 1] = 4; // get_account request + expectedFrame[str.enc("p:1").length + 1] = 1; // get_account expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); @@ -234,14 +230,14 @@ describe("generated client transport", () => { void client.system.handshake(); - const expectedPayload = T.VersionedHostHandshakeRequest.enc({ + const expectedPayload = T.HostHandshakeVersion.enc({ tag: "V1", - value: { codecVersion: 2 }, + value: { tag: "Request", value: { codecVersion: 2 } }, }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); expectedFrame[str.enc("p:1").length] = 193; // system trait - expectedFrame[str.enc("p:1").length + 1] = 0; // handshake request + expectedFrame[str.enc("p:1").length + 1] = 0; // handshake expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); @@ -258,8 +254,7 @@ describe("generated client transport", () => { requestId: "p:1", payload: { traitId: W.SYSTEM_HANDSHAKE.trait, - - methodId: W.SYSTEM_HANDSHAKE.response, + methodId: W.SYSTEM_HANDSHAKE.method, value: handshakeResponsePayload({ success: true, value: undefined }), }, }), @@ -281,17 +276,15 @@ describe("generated client transport", () => { requestId: "p:1", payload: { traitId: W.SYSTEM_GET_PRODUCT_CONTEXT.trait, - methodId: W.SYSTEM_GET_PRODUCT_CONTEXT.response, - value: versionedV1( - ScaleResult( - T.HostGetProductContextResponse, - CallError(T.VersionedHostGetProductContextError), - ), - ).enc({ + methodId: W.SYSTEM_GET_PRODUCT_CONTEXT.method, + value: T.HostGetProductContextVersion.enc({ tag: "V1", value: { - success: true, - value: { productId: "truapi-playground.paseo" }, + tag: "Response", + value: { + success: true, + value: { productId: "truapi-playground.paseo" }, + }, }, }), }, @@ -322,8 +315,7 @@ describe("generated client transport", () => { requestId: "p:1", payload: { traitId: W.ACCOUNT_GET_ACCOUNT.trait, - - methodId: W.ACCOUNT_GET_ACCOUNT.response, + methodId: W.ACCOUNT_GET_ACCOUNT.method, value: accountGetResponsePayload({ success: false, value: { tag: "Domain", value: reason }, @@ -343,7 +335,7 @@ describe("generated client transport", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const response = transport.request>({ - ids: { trait: 200, request: 194, response: 195 }, + ids: { trait: 200, method: 194 }, payload: new Uint8Array(), decodeResponse: () => { throw new Error("protocol errors must bypass the method response decoder"); @@ -364,7 +356,7 @@ describe("generated client transport", () => { requestId: "p:2", payload: { traitId: W.LOCAL_STORAGE_READ.trait, - methodId: W.LOCAL_STORAGE_READ.response, + methodId: W.LOCAL_STORAGE_READ.method, value: new Uint8Array(), }, }), @@ -380,7 +372,7 @@ describe("generated client transport", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const response = transport.request>({ - ids: { trait: 200, request: 194, response: 195 }, + ids: { trait: 200, method: 194 }, payload: new Uint8Array(), decodeResponse: () => ({ success: true, value: "supported" }), }); @@ -399,7 +391,7 @@ describe("generated client transport", () => { unwrap( encodeWireMessage({ requestId: "p:1", - payload: { traitId: 200, methodId: 195, value: new Uint8Array() }, + payload: { traitId: 200, methodId: 194, value: new Uint8Array() }, }), "encode supported response", ), @@ -414,13 +406,13 @@ describe("generated client transport", () => { const transport = createTransport(fixture.provider); const errors: Error[] = []; const subscription = transport.subscribeRaw({ - ids: { trait: 7, start: 194, stop: 195, interrupt: 196, receive: 197 }, + ids: { trait: 7, method: 194 }, payload: new Uint8Array(), onReceive: () => {}, onClose: (error) => errors.push(error), }); - // Right trait, wrong method: an error about our stop id is not about our - // start, so it must not end the subscription. + // Right trait, wrong method: an error about a different method must + // not end this subscription. fixture.receive(unsupportedMessage(subscription.subscriptionId, 7, 195)); // Right METHOD, wrong trait. Under a one-byte discriminant these two // were indistinguishable; the pair is the whole point, so a trait-8 @@ -459,7 +451,7 @@ describe("generated client transport", () => { unsupportedMessage( subscription.subscriptionId, W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.start, + W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, ), ); @@ -470,7 +462,7 @@ describe("generated client transport", () => { const cause = errors[0].cause as UnsupportedMessageError; expect([cause.traitId, cause.methodId]).toEqual([ W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.start, + W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, ]); expect(fixture.sent).toHaveLength(1); }); @@ -493,7 +485,7 @@ describe("generated client transport", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const response = transport.request>({ - ids: { trait: 200, request: 194, response: 195 }, + ids: { trait: 200, method: 194 }, payload: new Uint8Array(), decodeResponse: () => ({ success: true, value: undefined }), }); @@ -533,7 +525,11 @@ describe("generated client transport", () => { requestId: "h:known", payload: { traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, - methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.start, value: new Uint8Array(), + methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.method, + // [version=0, direction=Start=0]: no handler is ever + // registered in this test (no client is created), so + // this never reaches a typed decode of the rest. + value: new Uint8Array([0, 0]), }, }), "encode known unhandled host start", @@ -545,7 +541,7 @@ describe("generated client transport", () => { unsupportedMessage( "h:known", W.CHAT_CUSTOM_MESSAGE_RENDER.trait, - W.CHAT_CUSTOM_MESSAGE_RENDER.start, + W.CHAT_CUSTOM_MESSAGE_RENDER.method, ), ), ]); @@ -580,7 +576,7 @@ describe("generated client transport", () => { requestId: "p:1", payload: { traitId: W.LOCAL_STORAGE_READ.trait, - methodId: W.LOCAL_STORAGE_READ.response, + methodId: W.LOCAL_STORAGE_READ.method, value: new Uint8Array(), }, }), @@ -603,7 +599,7 @@ describe("generated client transport", () => { requestId: "p:1", payload: { traitId: W.LOCAL_STORAGE_READ.trait, - methodId: W.LOCAL_STORAGE_READ.response, + methodId: W.LOCAL_STORAGE_READ.method, value: new Uint8Array(), }, }), @@ -623,18 +619,17 @@ describe("generated client transport", () => { onReceive: (payload) => received.push(payload), }); subscription.unsubscribe(); - for (const methodId of [ - W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, - W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.interrupt, - ]) { + // Receive (direction=3) and Interrupt(None) (direction=2) now share one + // address; both are distinguished by the direction byte in `value`. + for (const value of [new Uint8Array([0, 3]), new Uint8Array([0, 2, 0])]) { subscriptionFixture.receive( unwrap( encodeWireMessage({ requestId: subscription.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - methodId, - value: new Uint8Array(), + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + value, }, }), "encode stale subscription frame", @@ -645,18 +640,53 @@ describe("generated client transport", () => { expect(subscriptionFixture.sent).toHaveLength(2); }); + it("logs a protocol violation for a known pair's out-of-range direction tag", () => { + const fixture = providerFixture(); + createTransport(fixture.provider); + + const warnings: unknown[][] = []; + const originalWarn = console.warn; + console.warn = (...args: unknown[]) => { + warnings.push(args); + }; + try { + fixture.receive( + unwrap( + encodeWireMessage({ + requestId: "unrelated:1", + payload: { + traitId: W.LOCAL_STORAGE_READ.trait, + methodId: W.LOCAL_STORAGE_READ.method, + value: new Uint8Array([0, 99]), + }, + }), + "encode malformed-direction frame", + ), + ); + } finally { + console.warn = originalWarn; + } + + expect(fixture.sent).toHaveLength(0); + expect( + warnings.some((args) => + String(args[0]).includes("unexpected direction tag 99"), + ), + ).toBe(true); + }); + it("auto-responds to an inbound handshake with the versioned-result shape", () => { const fixture = providerFixture(); createTransport(fixture.provider); - const requestPayload = T.VersionedHostHandshakeRequest.enc({ + const requestPayload = T.HostHandshakeVersion.enc({ tag: "V1", - value: { codecVersion: 2 }, + value: { tag: "Request", value: { codecVersion: 2 } }, }); const requestFrame = unwrap( encodeWireMessage({ requestId: "h:1", - payload: { traitId: W.SYSTEM_HANDSHAKE.trait, methodId: W.SYSTEM_HANDSHAKE.request, value: requestPayload }, + payload: { traitId: W.SYSTEM_HANDSHAKE.trait, methodId: W.SYSTEM_HANDSHAKE.method, value: requestPayload }, }), "encode inbound handshake_request", ); @@ -667,8 +697,7 @@ describe("generated client transport", () => { requestId: "h:1", payload: { traitId: W.SYSTEM_HANDSHAKE.trait, - - methodId: W.SYSTEM_HANDSHAKE.response, + methodId: W.SYSTEM_HANDSHAKE.method, value: handshakeResponsePayload({ success: true, value: undefined }), }, }), @@ -717,7 +746,7 @@ describe("generated client transport", () => { requestId: "p:1", payload: { traitId: W.ACCOUNT_GET_ACCOUNT.trait + 1, - methodId: W.ACCOUNT_GET_ACCOUNT.response, + methodId: W.ACCOUNT_GET_ACCOUNT.method, value: accountGetResponsePayload({ success: false, value: { @@ -754,11 +783,10 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - - methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, - value: T.VersionedHostAccountConnectionStatusSubscribeItem.enc({ + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + value: T.HostAccountConnectionStatusSubscribeVersion.enc({ tag: "V1", - value: "Connected", + value: { tag: "Receive", value: "Connected" }, }), }, }), @@ -943,9 +971,11 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - - methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.interrupt, - value: _void.enc(undefined), + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + value: T.HostAccountConnectionStatusSubscribeVersion.enc({ + tag: "V1", + value: { tag: "Interrupt", value: undefined }, + }), }, }), "encode interrupt", @@ -977,11 +1007,13 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.PAYMENT_BALANCE_SUBSCRIBE.trait, - - methodId: W.PAYMENT_BALANCE_SUBSCRIBE.interrupt, - value: versionedV1(CallError(T.VersionedHostPaymentBalanceSubscribeError)).enc({ + methodId: W.PAYMENT_BALANCE_SUBSCRIBE.method, + // The merged envelope carries the bare domain error under + // one shared version tag; the public `reason` above adds + // back the per-error `V1` tag this test asserts against. + value: T.HostPaymentBalanceSubscribeVersion.enc({ tag: "V1", - value: callError, + value: { tag: "Interrupt", value: { tag: "Domain", value: reason } }, }), }, }), @@ -1016,11 +1048,11 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.COIN_PAYMENT_REBALANCE_PURSE.trait, - - methodId: W.COIN_PAYMENT_REBALANCE_PURSE.interrupt, - value: versionedV1( - CallError(T.VersionedHostCoinPaymentRebalancePurseError), - ).enc({ tag: "V1", value: callError }), + methodId: W.COIN_PAYMENT_REBALANCE_PURSE.method, + value: T.HostCoinPaymentRebalancePurseVersion.enc({ + tag: "V1", + value: { tag: "Interrupt", value: { tag: "Domain", value: reason } }, + }), }, }), "encode typed coin payment interrupt", @@ -1051,9 +1083,11 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - - methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, - value: _void.enc(undefined), + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + // [version=0, direction=Receive=3, item disc=0xff]: a + // well-formed envelope prefix with an out-of-range item + // discriminant, so decoding fails past the direction tag. + value: new Uint8Array([0, 3, 0xff]), }, }), "encode malformed receive", @@ -1071,9 +1105,8 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - - methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.stop, - value: _void.enc(undefined), + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + value: new Uint8Array([0, 1]), }, }), "encode stop after malformed receive", @@ -1085,11 +1118,10 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - - methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.receive, - value: T.VersionedHostAccountConnectionStatusSubscribeItem.enc({ + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + value: T.HostAccountConnectionStatusSubscribeVersion.enc({ tag: "V1", - value: "Connected", + value: { tag: "Receive", value: "Connected" }, }), }, }), @@ -1118,9 +1150,8 @@ describe("generated client transport", () => { requestId: sub.subscriptionId, payload: { traitId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.trait, - - methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.stop, - value: _void.enc(undefined), + methodId: W.ACCOUNT_CONNECTION_STATUS_SUBSCRIBE.method, + value: new Uint8Array([0, 1]), }, }), "encode explicit unsubscribe stop", diff --git a/js/packages/truapi/src/client.ts b/js/packages/truapi/src/client.ts index e263c8f2c..f2cf9011b 100644 --- a/js/packages/truapi/src/client.ts +++ b/js/packages/truapi/src/client.ts @@ -7,12 +7,11 @@ import { PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, type HostInitiatedSubscriptionHandler, + type MethodIds, type ObservableSource, type ProtocolMessage, type RegisterHostInitiatedSubscriptionParams, - type RequestFrameIds, type RequestParams, - type SubscriptionFrameIds, type SubscribeRawParams, type Subscription, type TrUApiTransport, @@ -20,33 +19,53 @@ import { UnsupportedMessageError, type WireProvider, } from "./transport.js"; -import { - CallError, - indexedTaggedUnion, - Result, - _void, - type CallErrorValue, - type Codec, - type ResultPayload, -} from "./scale.js"; +import { type ResultPayload } from "./scale.js"; import { TRUAPI_CODEC_VERSION } from "./generated/client.js"; import * as T from "./generated/types.js"; import * as W from "./generated/wire-table.js"; export type { Subscription, TrUApiTransport }; -const UNANSWERED_WIRE_IDS = new Set( - Object.values(W).flatMap((ids) => - "response" in ids - ? [`${ids.trait}:${ids.response}`] - : [ - `${ids.trait}:${ids.stop}`, - `${ids.trait}:${ids.interrupt}`, - `${ids.trait}:${ids.receive}`, - ], - ), +// Every method's request/response (or start/stop/interrupt/receive) frames +// now share one (trait, method) address (RFC 0028): direction lives in the +// payload, not the id. A late or duplicate *answer*-direction frame +// (Response/Stop/Interrupt/Receive) for a known method can legitimately +// arrive with no matching pending call or subscription (e.g. after a +// request already timed out, or after `unsubscribe`), so those are ignored +// rather than reported as a protocol violation. A *request*-direction frame +// (Request/Start) with nothing to route to is never expected — it means +// this build genuinely doesn't implement the pair (no client was ever +// created, or the specific host-initiated method has no registration) — so +// it still earns the same reply as an unknown pair. +const KNOWN_WIRE_IDS = new Set( + Object.values(W).map((ids) => `${ids.trait}:${ids.method}`), ); +/** Direction tag byte (right after the envelope's own version byte). **/ +const DIRECTION_REQUEST = 0; +const DIRECTION_START = 0; +const DIRECTION_STOP = 1; +const DIRECTION_INTERRUPT = 2; +const DIRECTION_RECEIVE = 3; + +/** + * Peek a nested wire envelope's direction tag (the second byte, right after + * the envelope's own version tag), without needing the concrete + * `{Method}Version` codec. + **/ +function directionTag(payload: Uint8Array): number | undefined { + return payload[1]; +} + +/** + * Encode `{Method}Version::V1(Subscription::Stop)` — a subscription + * cancellation — without needing the concrete `{Method}Version` codec: + * `Stop` carries no payload, so the two bytes are fully determined by the + * envelope version (always `V1` today; every real method has exactly one + * version). + **/ +const STOP_FRAME = new Uint8Array([0, DIRECTION_STOP]); + /** * Version overrides used when constructing a transport. */ @@ -74,23 +93,6 @@ function reportProtocolViolation(detail: string): void { console.warn(`[truapi] ${detail}`); } -/** - * Convert a positive protocol version number into the generated version tag - * used by TrUAPI wire wrappers. - */ -function protocolVersionTag(version: number): `V${number}` { - if (!Number.isInteger(version) || version < 1) { - throw new Error(`Invalid TrUAPI protocol version: ${version}`); - } - return `V${version}` as `V${number}`; -} - -type HandshakeResponse = ResultPayload< - undefined, - CallErrorValue ->; -const HANDSHAKE_WIRE_VERSION = 1; - /** * How long a `system_handshake` call waits for the host's answer. Matches the * allowance the protocol spec gives the handshake. @@ -98,89 +100,37 @@ const HANDSHAKE_WIRE_VERSION = 1; const HANDSHAKE_TIMEOUT_MS = 10_000; /** - * Build the versioned handshake response codec for the selected wire version. - */ -function handshakeResponseCodec( - version: number, -): Codec<{ tag: `V${number}`; value: HandshakeResponse }> { - return indexedTaggedUnion({ - [protocolVersionTag(version)]: [ - version - 1, - Result(_void, CallError(T.VersionedHostHandshakeError)), - ] as const, - }) as Codec<{ tag: `V${number}`; value: HandshakeResponse }>; -} - -/** - * Encode a successful host-handshake response payload. + * Encode a successful host-handshake response frame: `HostHandshakeVersion:: + * V1(Request::Response(Ok(undefined)))`. */ -function encodeSuccessfulHandshakeResponse(version: number): Uint8Array { - return encodeHandshakeResponse(version, { - tag: protocolVersionTag(version), - value: { - success: true, - value: undefined, - }, +function encodeSuccessfulHandshakeResponse(): Uint8Array { + return T.HostHandshakeVersion.enc({ + tag: "V1", + value: { tag: "Response", value: { success: true, value: undefined } }, }); } /** - * Encode a host-handshake response that reports an unsupported codec version. + * Encode a host-handshake response frame reporting an unsupported codec + * version: `HostHandshakeVersion::V1(Request::Response(Err(CallError::Domain( + * HostHandshakeError::UnsupportedProtocolVersion))))`. */ -function encodeUnsupportedHandshakeResponse(version: number): Uint8Array { - return encodeHandshakeResponse(version, { - tag: protocolVersionTag(version), +function encodeUnsupportedHandshakeResponse(): Uint8Array { + return T.HostHandshakeVersion.enc({ + tag: "V1", value: { - success: false, + tag: "Response", value: { - tag: "Domain", + success: false, value: { - tag: "V1", - value: { - tag: "UnsupportedProtocolVersion", - value: undefined, - }, + tag: "Domain", + value: { tag: "UnsupportedProtocolVersion", value: undefined }, }, }, }, }); } -/** - * Encode a typed handshake response with the versioned response codec. - */ -function encodeHandshakeResponse( - version: number, - response: { tag: `V${number}`; value: HandshakeResponse }, -): Uint8Array { - return handshakeResponseCodec(version).enc(response); -} - -type VersionedWireValue = { tag: `V${number}`; value: unknown }; - -/** - * Check whether a decoded SCALE value has the generated `{ tag, value }` - * wrapper shape used for versioned wire payloads. - */ -function isVersionedWireValue(value: unknown): value is VersionedWireValue { - return ( - typeof value === "object" && - value !== null && - "tag" in value && - "value" in value && - typeof value.tag === "string" && - /^V\d+$/.test(value.tag) - ); -} - -/** - * Return the inner payload from a versioned wire wrapper, or the original - * value when the payload is already unwrapped. - */ -function unwrapVersionedWireValue(value: unknown): unknown { - return isVersionedWireValue(value) ? value.value : value; -} - /** * Map key for a `(trait, method)` wire discriminant pair. Both bytes together * identify a frame, so neither half alone is a usable key. @@ -230,7 +180,7 @@ export function createTransport( const pending = new Map< string, { - ids: RequestFrameIds; + ids: MethodIds; resolve: (value: Uint8Array) => void; resolveUnsupported: () => void; reject: (error: Error) => void; @@ -239,7 +189,7 @@ export function createTransport( const subscriptions = new Map< string, { - ids: SubscriptionFrameIds; + ids: MethodIds; onReceive: (payload: Uint8Array) => void; onInterrupt?: (payload: Uint8Array) => void; onClose?: (error: Error) => void; @@ -247,7 +197,7 @@ export function createTransport( >(); type BufferedHostStart = { requestId: string; payload: Uint8Array }; type HostRoute = { - ids: SubscriptionFrameIds; + ids: MethodIds; decodeRequest: (payload: Uint8Array) => unknown; encodeItem: (item: unknown) => Uint8Array; interruptPayload: Uint8Array; @@ -256,8 +206,8 @@ export function createTransport( handler?: (request: unknown) => ObservableSource; instances: Map; }; - // Keyed by the full (trait, method) start pair: a bare start id would - // collide the moment two traits both number a subscription the same. + // Keyed by the full (trait, method) pair: a bare method id would collide + // the moment two traits both number a subscription the same. const hostRoutes = new Map(); /** @@ -329,7 +279,7 @@ export function createTransport( const request = pending.get(requestId); if ( request?.ids.trait === unsupported.traitId && - request?.ids.request === unsupported.methodId + request?.ids.method === unsupported.methodId ) { pending.delete(requestId); request.resolveUnsupported(); @@ -339,7 +289,7 @@ export function createTransport( const subscription = subscriptions.get(requestId); if ( subscription?.ids.trait === unsupported.traitId && - subscription?.ids.start === unsupported.methodId + subscription?.ids.method === unsupported.methodId ) { subscriptions.delete(requestId); subscription.onClose?.( @@ -354,7 +304,8 @@ export function createTransport( if ( payload.traitId === W.SYSTEM_HANDSHAKE.trait && - payload.methodId === W.SYSTEM_HANDSHAKE.request + payload.methodId === W.SYSTEM_HANDSHAKE.method && + directionTag(payload.value) === DIRECTION_REQUEST ) { // Auto-respond to inbound `host_handshake_request` frames. Hosts ping // the product at startup and repeat until they see a matching response, @@ -362,6 +313,11 @@ export function createTransport( // down: a host whose codec this client cannot speak is exactly the peer // that needs an answer it can act on. // + // The direction check above matters: request and response now share + // this address (RFC 0028), and a `Response` frame arriving here is the + // host's answer to this client's own `system.handshake()` call, which + // must fall through to the `pending` lookup below instead. + // // Respond with the handshake method's selected wire version. The inner // request carries the wire codec version. A request body this client // cannot decode is itself a codec mismatch -- a codec 1 host's frame @@ -370,28 +326,29 @@ export function createTransport( // raw SCALE error. let response: Uint8Array; try { - const request = unwrapVersionedWireValue( - T.VersionedHostHandshakeRequest.dec(payload.value), - ) as T.HostHandshakeRequest; - const requestedCodecVersion = request.codecVersion; + const envelope = T.HostHandshakeVersion.dec(payload.value); + if (envelope.value.tag !== "Request") { + throw new Error(`expected Request direction, got ${envelope.value.tag}`); + } + const requestedCodecVersion = envelope.value.value.codecVersion; response = requestedCodecVersion === codecVersion - ? encodeSuccessfulHandshakeResponse(HANDSHAKE_WIRE_VERSION) - : encodeUnsupportedHandshakeResponse(HANDSHAKE_WIRE_VERSION); + ? encodeSuccessfulHandshakeResponse() + : encodeUnsupportedHandshakeResponse(); } catch (error) { reportProtocolViolation( `undecodable handshake request from the host (expected wire codec ${codecVersion}): ${ toError(error).message }`, ); - response = encodeUnsupportedHandshakeResponse(HANDSHAKE_WIRE_VERSION); + response = encodeUnsupportedHandshakeResponse(); } try { send({ requestId, payload: { traitId: W.SYSTEM_HANDSHAKE.trait, - methodId: W.SYSTEM_HANDSHAKE.response, + methodId: W.SYSTEM_HANDSHAKE.method, value: response, }, }); @@ -405,33 +362,30 @@ export function createTransport( pairKey(payload.traitId, payload.methodId), ); if (hostRoute) { - startHostSubscription(hostRoute, requestId, payload.value); - return; - } - for (const candidate of hostRoutes.values()) { - if ( - payload.traitId !== candidate.ids.trait || - payload.methodId !== candidate.ids.stop - ) - continue; - const bufferedIndex = candidate.buffered.findIndex( - (start) => start.requestId === requestId, - ); - if (bufferedIndex >= 0) candidate.buffered.splice(bufferedIndex, 1); - const instance = candidate.instances.get(requestId); - if (instance) { - candidate.instances.delete(requestId); - instance.unsubscribe(); + const direction = directionTag(payload.value); + if (direction === DIRECTION_START) { + startHostSubscription(hostRoute, requestId, payload.value); + } else if (direction === DIRECTION_STOP) { + const bufferedIndex = hostRoute.buffered.findIndex( + (start) => start.requestId === requestId, + ); + if (bufferedIndex >= 0) hostRoute.buffered.splice(bufferedIndex, 1); + const instance = hostRoute.instances.get(requestId); + if (instance) { + hostRoute.instances.delete(requestId); + instance.unsubscribe(); + } + } else { + reportProtocolViolation( + `ignoring host-initiated frame for (${payload.traitId}, ${payload.methodId}): unexpected direction tag ${direction}, expected Start (${DIRECTION_START}) or Stop (${DIRECTION_STOP})`, + ); } return; } const p = pending.get(requestId); if (p) { - if ( - payload.traitId !== p.ids.trait || - payload.methodId !== p.ids.response - ) { + if (payload.traitId !== p.ids.trait || payload.methodId !== p.ids.method) { // The host answered this request id on a discriminant the method does // not own. Dropping it unreported leaves the caller waiting forever // with no clue why, and a whole-trait skew is what a codec mismatch @@ -440,10 +394,10 @@ export function createTransport( // Report it, then fall through rather than returning: the request stays // pending (this frame is not its answer), and the frame itself is one // this build cannot route, so it earns the same protocol-error reply as - // any other unroutable pair. A known client-bound pair is still filtered - // out by `UNANSWERED_WIRE_IDS` below. + // any other unroutable pair. A known method's answer-direction frame is + // still filtered out below. reportProtocolViolation( - `ignoring frame for request ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}), expected (${p.ids.trait}, ${p.ids.response})`, + `ignoring frame for request ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}), expected (${p.ids.trait}, ${p.ids.method})`, ); } else { pending.delete(requestId); @@ -458,9 +412,11 @@ export function createTransport( const subscription = subscriptions.get(requestId); if (subscription) { + const direction = directionTag(payload.value); if ( payload.traitId === subscription.ids.trait && - payload.methodId === subscription.ids.receive + payload.methodId === subscription.ids.method && + direction === DIRECTION_RECEIVE ) { try { subscription.onReceive(payload.value); @@ -474,24 +430,47 @@ export function createTransport( } } else if ( payload.traitId === subscription.ids.trait && - payload.methodId === subscription.ids.interrupt + payload.methodId === subscription.ids.method && + direction === DIRECTION_INTERRUPT ) { subscriptions.delete(requestId); subscription.onInterrupt?.(payload.value); } else { reportProtocolViolation( - `ignoring frame for subscription ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}), expected receive (${subscription.ids.trait}, ${subscription.ids.receive}) or interrupt (${subscription.ids.trait}, ${subscription.ids.interrupt})`, + `ignoring frame for subscription ${requestId}: got discriminant (${payload.traitId}, ${payload.methodId}) direction ${direction}, expected receive (${DIRECTION_RECEIVE}) or interrupt (${DIRECTION_INTERRUPT}) on (${subscription.ids.trait}, ${subscription.ids.method})`, ); } return; } - if (UNANSWERED_WIRE_IDS.has(`${payload.traitId}:${payload.methodId}`)) { - return; + if (KNOWN_WIRE_IDS.has(`${payload.traitId}:${payload.methodId}`)) { + const direction = directionTag(payload.value); + if ( + direction === DIRECTION_STOP || + direction === DIRECTION_INTERRUPT || + direction === DIRECTION_RECEIVE + ) { + // A known method's answer-direction frame (Response/Stop/Interrupt/ + // Receive) with nothing to route to: a normal late/stale frame, not + // a protocol violation. + return; + } + if (direction !== DIRECTION_REQUEST) { + // Neither a plausible late answer nor a valid Request/Start: the + // direction byte itself is out of range or missing. Still dropped + // (there is nothing to route it to), but logged rather than + // silently swallowed, matching the analogous case in the + // `hostRoute` branch above. + reportProtocolViolation( + `ignoring frame for known pair (${payload.traitId}, ${payload.methodId}): unexpected direction tag ${direction}`, + ); + return; + } } - // Not pending, no subscription, and not a client-bound frame we ignore by - // design: this build does not implement the pair. + // Either an unknown pair, or a known method's request-direction frame + // (Request/Start) with nothing to route to: this build does not + // implement the pair. reportProtocolViolation( `unsupported frame with discriminant (${payload.traitId}, ${payload.methodId}): request ${requestId} is not pending and has no subscription`, ); @@ -551,7 +530,7 @@ export function createTransport( requestId, payload: { traitId: route.ids.trait, - methodId: route.ids.interrupt, + methodId: route.ids.method, value: route.interruptPayload, }, }); @@ -608,7 +587,7 @@ export function createTransport( requestId, payload: { traitId: route.ids.trait, - methodId: route.ids.receive, + methodId: route.ids.method, value: route.encodeItem(item), }, }); @@ -658,7 +637,7 @@ export function createTransport( // so the call that exists to detect the mismatch cannot hang on it. const deadline = ids.trait === W.SYSTEM_HANDSHAKE.trait && - ids.request === W.SYSTEM_HANDSHAKE.request + ids.method === W.SYSTEM_HANDSHAKE.method ? setTimeout(() => { if (!pending.delete(requestId)) { return; @@ -697,7 +676,7 @@ export function createTransport( requestId, payload: { traitId: ids.trait, - methodId: ids.request, + methodId: ids.method, value: payload, }, }); @@ -739,7 +718,7 @@ export function createTransport( requestId, payload: { traitId: ids.trait, - methodId: ids.start, + methodId: ids.method, value: payload, }, }); @@ -760,8 +739,8 @@ export function createTransport( requestId, payload: { traitId: ids.trait, - methodId: ids.stop, - value: _void.enc(undefined), + methodId: ids.method, + value: STOP_FRAME, }, }); } catch { @@ -777,10 +756,10 @@ export function createTransport( interruptPayload, bufferCapacity, }: RegisterHostInitiatedSubscriptionParams) { - const key = pairKey(ids.trait, ids.start); + const key = pairKey(ids.trait, ids.method); if (hostRoutes.has(key)) { throw new Error( - `host-initiated subscription (${ids.trait}, ${ids.start}) is already registered`, + `host-initiated subscription (${ids.trait}, ${ids.method}) is already registered`, ); } const route: HostRoute = { diff --git a/js/packages/truapi/src/index.ts b/js/packages/truapi/src/index.ts index 4321399f5..1d9cb72b5 100644 --- a/js/packages/truapi/src/index.ts +++ b/js/packages/truapi/src/index.ts @@ -1,12 +1,11 @@ export type { + MethodIds, ObservableLike, ObservableSource, Observer, Payload, ProtocolMessage, - RequestFrameIds, RequestParams, - SubscriptionFrameIds, Subscription, SubscribeRawParams, TrUApiTransport, diff --git a/js/packages/truapi/src/scale.ts b/js/packages/truapi/src/scale.ts index c9a09a1c0..a54a04a7f 100644 --- a/js/packages/truapi/src/scale.ts +++ b/js/packages/truapi/src/scale.ts @@ -8,6 +8,7 @@ import { Bytes, Enum, + Option, Struct, createCodec, createDecoder, @@ -16,6 +17,7 @@ import { u8, _void, type Codec, + type ResultPayload, } from "scale-ts"; import { bytesToHex as encodeHex, @@ -25,6 +27,14 @@ import { export type { Codec }; export type { ResultPayload } from "scale-ts"; +/** + * Bare-named type alias matching generated codegen's naming convention for + * generic wire types: `Result` is used as both a value (the codec + * builder re-exported below) and a type (this alias for scale-ts's own + * `ResultPayload`) in generated `types.ts`. + */ +export type Result = ResultPayload; + export { Bytes, Enum, @@ -142,6 +152,63 @@ export function CallError(domain: Codec): Codec> { }) as Codec>; } +/** + * Public TS value for `truapi::versioned::Request`. Named to match + * the value-space codec builder below: generated codegen references + * generic wire types under one bare name in both type and value position + * (e.g. `Request` as a type, `Request(a, b)` as a codec), and + * TypeScript's separate type/value namespaces make that legal here. + */ +export type Request = + | { tag: "Request"; value: Req } + | { tag: "Response"; value: Res }; + +/** + * SCALE codec for `truapi::versioned::Request`: the nested wire + * envelope's direction tag for a request/response method (RFC 0028). + * `Request`=0, `Response`=1, fixed by the Rust enum's declaration order. + */ +export function Request( + req: Codec, + res: Codec, +): Codec> { + return indexedTaggedUnion({ + Request: [0, req], + Response: [1, res], + } as const) as unknown as Codec>; +} + +/** + * Public TS value for `truapi::versioned::Subscription`. + * Named to match the value-space codec builder below (see {@link Request}'s + * doc for why the type and value share one bare name). + */ +export type Subscription = + | { tag: "Start"; value: Start } + | { tag: "Stop"; value?: undefined } + | { tag: "Interrupt"; value: Err | undefined } + | { tag: "Receive"; value: Item }; + +/** + * SCALE codec for `truapi::versioned::Subscription`: the + * nested wire envelope's direction tag for a subscription method (RFC 0028). + * `Start`=0, `Stop`=1, `Interrupt`=2 (wrapping `Option` — `undefined` + * is a clean, error-free completion), `Receive`=3, fixed by the Rust enum's + * declaration order. + */ +export function Subscription( + start: Codec, + item: Codec, + err: Codec, +): Codec> { + return indexedTaggedUnion({ + Start: [0, start], + Stop: [1, _void], + Interrupt: [2, Option(err)], + Receive: [3, item], + } as const) as unknown as Codec>; +} + type TaggedUnionCodecs = { [Sym: symbol]: never; [Num: number]: never; diff --git a/js/packages/truapi/src/transport.ts b/js/packages/truapi/src/transport.ts index e50316c9f..eb1e420ec 100644 --- a/js/packages/truapi/src/transport.ts +++ b/js/packages/truapi/src/transport.ts @@ -157,53 +157,21 @@ export interface ObservableSource { } /** - * Numeric frame ids for a one-shot request method. + * Wire discriminant pair addressing a method. One id addresses a method + * regardless of shape (request/response, or a subscription's four phases): + * direction and version are carried inside the payload (RFC 0028), not by a + * separate id per direction. **/ -export interface RequestFrameIds { +export interface MethodIds { /** - * Wire trait discriminant carried by both frames. + * Wire trait discriminant. **/ trait: number; /** - * Wire method discriminant for the outbound request frame. + * Wire method discriminant within the trait. **/ - request: number; - - /** - * Wire method discriminant for the inbound response frame. - **/ - response: number; -} - -/** - * Numeric frame ids for a subscription method. - **/ -export interface SubscriptionFrameIds { - /** - * Wire trait discriminant carried by all four frames. - **/ - trait: number; - - /** - * Wire method discriminant for the outbound start frame. - **/ - start: number; - - /** - * Wire method discriminant for the outbound stop frame. - **/ - stop: number; - - /** - * Wire method discriminant for the inbound interrupt frame. - **/ - interrupt: number; - - /** - * Wire method discriminant for the inbound receive frame. - **/ - receive: number; + method: number; } /** @@ -213,17 +181,19 @@ export interface RequestParams { /** * Wire discriminants for this request method. **/ - ids: RequestFrameIds; + ids: MethodIds; /** - * SCALE-encoded request payload bytes. + * SCALE-encoded envelope payload bytes (`[version, direction=Request, + * ...request]`), constructed by the generated caller. **/ payload: Uint8Array; /** - * Decode SCALE response payload bytes into the wire `ResultPayload` - * envelope. The transport unwraps the envelope into - * `ResultAsync`. + * Decode the full raw envelope payload bytes into the typed Ok/Err + * outcome. Implementations decode the method's `{Method}Version` envelope + * and reject any direction other than `Response`. The transport unwraps + * the result into `ResultAsync`. **/ decodeResponse: (payload: Uint8Array) => ResultPayload; } @@ -235,20 +205,23 @@ export interface SubscribeRawParams { /** * Wire discriminants for this subscription method. **/ - ids: SubscriptionFrameIds; + ids: MethodIds; /** - * SCALE-encoded subscription start payload bytes. + * SCALE-encoded envelope payload bytes (`[version, direction=Start, + * ...start]`), constructed by the generated caller. **/ payload: Uint8Array; /** - * Called with raw SCALE receive payload bytes. + * Called with the full raw envelope payload bytes when the peer sends a + * `Receive` frame. **/ onReceive: (payload: Uint8Array) => void; /** - * Called with raw SCALE interrupt payload bytes when the peer interrupts the subscription. + * Called with the full raw envelope payload bytes when the peer sends an + * `Interrupt` frame. **/ onInterrupt?: (payload: Uint8Array) => void; @@ -277,10 +250,16 @@ export interface HostInitiatedSubscriptionRegistration { /** Options used to register a host-initiated subscription method. **/ export interface RegisterHostInitiatedSubscriptionParams { /** Wire discriminants for the host-initiated subscription. **/ - ids: SubscriptionFrameIds; - /** Decode the host's start payload. **/ + ids: MethodIds; + /** + * Decode the host's full raw envelope payload bytes (a `Start` frame) into + * the typed request. + **/ decodeRequest(payload: Uint8Array): Request; - /** Encode one product renderer emission. **/ + /** + * Encode one product renderer emission as the full envelope payload bytes + * (a `Receive` frame). + **/ encodeItem(item: Item): Uint8Array; /** Exact payload used when the product declines a render instance. **/ interruptPayload: Uint8Array; diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts index 985d9efba..81e306a9f 100644 --- a/js/packages/truapi/src/wire-equality.test.ts +++ b/js/packages/truapi/src/wire-equality.test.ts @@ -46,7 +46,7 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { // the handshake is the first frame either side sends, so its envelope // must never drift, and a codec 1 peer's frame must never reach it. expect(W.SYSTEM_HANDSHAKE.trait).toBe(193); - expect(W.SYSTEM_HANDSHAKE.request).toBe(0); + expect(W.SYSTEM_HANDSHAKE.method).toBe(0); const inner = new Uint8Array([0x00, 0x02]); // V1 variant + codec_version=2 const encoded = unwrap( @@ -54,7 +54,7 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { requestId: "p:1", payload: { traitId: W.SYSTEM_HANDSHAKE.trait, - methodId: W.SYSTEM_HANDSHAKE.request, + methodId: W.SYSTEM_HANDSHAKE.method, value: inner, }, }), @@ -70,19 +70,22 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); - it("encodes account_get_request (pair (194, 4)) to match the golden fixture", () => { + it("encodes account_get_request (pair (194, 1)) to match the golden fixture", () => { // Same vector as the Rust golden fixture // (`truapi-server/tests/snapshots/golden-account-get.bin`). Encoded // through the generated codec rather than assembled byte by byte: a // hand-rolled payload keeps encoding the layout it was written against // long after the type has moved on, which is exactly how the Rust // fixture went stale across the 0.6.0 `DerivationIndex` change. - const inner = T.VersionedHostAccountGetRequest.enc({ + const inner = T.HostAccountGetVersion.enc({ tag: "V1", value: { - productAccountId: { - dotNsIdentifier: "foo", - derivationIndex: { tag: "Index", value: 0 }, + tag: "Request", + value: { + productAccountId: { + dotNsIdentifier: "foo", + derivationIndex: { tag: "Index", value: 0 }, + }, }, }, }); @@ -91,16 +94,16 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { requestId: "p:1", payload: { traitId: W.ACCOUNT_GET_ACCOUNT.trait, - methodId: W.ACCOUNT_GET_ACCOUNT.request, + methodId: W.ACCOUNT_GET_ACCOUNT.method, value: inner, }, }), "encode account_get_request", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(194, 4, inner))); - // [0c 70 3a 31] "p:1" + [c2 04] pair + [00] V1 + [0c 66 6f 6f] "foo" - // + [00] DerivationIndex::Index + [00 00 00 00] u32 = 0. - expect(toHex(encoded)).toBe("0c703a31c204000c666f6f0000000000"); + expect(toHex(encoded)).toBe(toHex(expectedWire(194, 1, inner))); + // [0c 70 3a 31] "p:1" + [c2 01] pair + [00] V1 + [00] direction=Request + // + [0c 66 6f 6f] "foo" + [00] DerivationIndex::Index + [00 00 00 00] u32 = 0. + expect(toHex(encoded)).toBe("0c703a31c20100000c666f6f0000000000"); }); it("round-trips a local_storage_read frame through encode + decode", () => { @@ -110,7 +113,7 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { requestId: "p:1", payload: { traitId: W.LOCAL_STORAGE_READ.trait, - methodId: W.LOCAL_STORAGE_READ.request, + methodId: W.LOCAL_STORAGE_READ.method, value: inner, }, }), @@ -119,7 +122,7 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { const decoded = unwrap(decodeWireMessage(encoded), "decode local_storage_read_request"); expect(decoded.requestId).toBe("p:1"); expect(decoded.payload.traitId).toBe(W.LOCAL_STORAGE_READ.trait); - expect(decoded.payload.methodId).toBe(W.LOCAL_STORAGE_READ.request); + expect(decoded.payload.methodId).toBe(W.LOCAL_STORAGE_READ.method); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); @@ -168,7 +171,7 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { requestId: longId, payload: { traitId: W.ACCOUNT_GET_ACCOUNT.trait, - methodId: W.ACCOUNT_GET_ACCOUNT.request, + methodId: W.ACCOUNT_GET_ACCOUNT.method, value: inner, }, }), @@ -180,7 +183,7 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { const decoded = unwrap(decodeWireMessage(encoded), "decode long-id account_get_request"); expect(decoded.requestId).toBe(longId); expect(decoded.payload.traitId).toBe(W.ACCOUNT_GET_ACCOUNT.trait); - expect(decoded.payload.methodId).toBe(W.ACCOUNT_GET_ACCOUNT.request); + expect(decoded.payload.methodId).toBe(W.ACCOUNT_GET_ACCOUNT.method); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); }); From 9e19716ffc8a9ef1aff746d49cd6a1aadce8fe1e Mon Sep 17 00:00:00 2001 From: Nidish Date: Tue, 1 Sep 2026 14:42:32 +0530 Subject: [PATCH 14/16] fix(ios): recompute wire byte layouts for the nested envelope (RFC 0028) --- .../Tests/TrUAPIWsBridgeTests.swift | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift index 0cef3725f..9170349df 100644 --- a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift +++ b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift @@ -33,8 +33,9 @@ struct TrUAPIWsBridgeTests { Issue.record("expected binary frame, got \(message)") return } - // Frame tail is the SCALE Result payload: Ok(0x00), V1(0x00), supported(0x01). - #expect(response.suffix(3) == Data([0x00, 0x00, 0x01])) + // Frame tail is the merged wire envelope: version(0x00), direction= + // Response(0x01), Result::Ok(0x00), supported(0x01). + #expect(response.suffix(4) == Data([0x00, 0x01, 0x00, 0x01])) } /// An iOS host must classify itself as `Ios` without the embedding app @@ -82,27 +83,28 @@ private extension TrUAPIWsBridgeTests { ) } - // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 192, request_id: 2 }. + // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 193, method_id: 1 }. // Both bytes are load-bearing: a lone method byte is read as the trait and // routes into a different trait's method 0 rather than failing. - static let featureSupportedRequestDiscriminant = Data([0xC0, 0x02]) + static let featureSupportedDiscriminant = Data([0xC1, 0x01]) - // wire_table.rs: SYSTEM_HOST_INFO.request_id = 192 - static let hostInfoRequestDiscriminant = Data([0xC0]) + // wire_table.rs: SYSTEM_HOST_INFO { trait_id: 193, method_id: 3 }. + static let hostInfoDiscriminant = Data([0xC1, 0x03]) static func hostInfoRequestFrame() -> Data { var frame = Data() frame.append(contentsOf: [0x0C]) // compact length 3 frame.append("p:1".data(using: .utf8)!) - frame.append(hostInfoRequestDiscriminant) // from wire_table.rs - frame.append(contentsOf: [0x00]) // V1 + frame.append(hostInfoDiscriminant) // from wire_table.rs + frame.append(contentsOf: [0x00, 0x00]) // version=V1, direction=Request return frame } - // SCALE Result payload: Ok(0x00), V1(0x00), then HostInfo as + // The merged wire envelope's response tail: version(0x00), + // direction=Response(0x01), Result::Ok(0x00), then HostInfo as // platform(Ios = 0x02), name, version (empty, hostVersion is unset). static var hostInfoResponseTail: Data { - var tail = Data([0x00, 0x00, 0x02, 0x44]) // 0x44 is compact length 17 + var tail = Data([0x00, 0x01, 0x00, 0x02, 0x44]) // 0x44 is compact length 17 tail.append("truapi-host-tests".data(using: .utf8)!) tail.append(contentsOf: [0x00]) return tail @@ -112,8 +114,9 @@ private extension TrUAPIWsBridgeTests { var frame = Data() frame.append(contentsOf: [0x0C]) // compact length 3 frame.append("p:1".data(using: .utf8)!) - frame.append(featureSupportedRequestDiscriminant) // from wire_table.rs - frame.append(contentsOf: [0x00, 0x00, 0x80]) // V1, Chain, compact(32) + frame.append(featureSupportedDiscriminant) // from wire_table.rs + // version=V1, direction=Request, Chain, compact(32) + frame.append(contentsOf: [0x00, 0x00, 0x00, 0x80]) frame.append(Data(repeating: 0, count: 32)) return frame } From 9e6db282ff9c91fbfc48546965b258d712eff649 Mon Sep 17 00:00:00 2001 From: Nidish Date: Tue, 1 Sep 2026 14:42:32 +0530 Subject: [PATCH 15/16] docs: catch up the protocol design doc and READMEs to RFC 0028 --- README.md | 2 +- docs/design/truapi-protocol.md | 377 ++++++++++++++++----------------- js/packages/truapi/README.md | 2 +- rust/crates/truapi/README.md | 2 +- 4 files changed, 183 insertions(+), 200 deletions(-) diff --git a/README.md b/README.md index e4de8d6a5..8b6d99707 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ dependency on the crate: ## How it works -1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each trait tagged `#[wire_trait(id = N)]` and each method tagged `#[wire(request_id = N)]` for a stable byte-level `(trait, method)` dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. +1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each trait tagged `#[wire_trait(id = N)]` and each method tagged `#[wire(id = N)]` for a stable byte-level `(trait, method)` dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. 2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`. 3. Higher-level SDKs wrap the typed client; the transport encodes SCALE frames and ships them over `MessagePort` (or `postMessage` in iframe mode) to the host. 4. The host decodes the frame, dispatches to the matching trait method, encodes the response, and ships it back. diff --git a/docs/design/truapi-protocol.md b/docs/design/truapi-protocol.md index 77521ebde..7681bfa43 100644 --- a/docs/design/truapi-protocol.md +++ b/docs/design/truapi-protocol.md @@ -55,72 +55,53 @@ struct Message { [requestId: SCALE str][trait: u8][method: u8][payload bytes...] ``` -The two bytes after the `requestId` are the **`(trait, method)` discriminant pair**. The first byte identifies the API trait (`System`, `Account`, `Chain`, ...); the second identifies the action within that trait. The payload bytes are the SCALE-encoded action value, inlined without a length prefix — the receiver reads to the end of the transport frame. Conceptually, `Payload` is a per-trait enum whose variants are the **actions** — the individual things a Host and Product can say to each other. +The two bytes after the `requestId` are the **`(trait, method)` discriminant pair**. The first byte identifies the API trait (`System`, `Account`, `Chain`, ...); the second identifies a method within it: exactly one id per method, regardless of that method's shape. The payload bytes are the SCALE-encoded value for that method's own nested envelope (below), inlined without a length prefix; the receiver reads to the end of the transport frame. -Actions are not written by hand. They are derived mechanically from the TrUAPI methods, so the high-level method signature and the wire format can never drift apart. One method expands into several actions depending on its shape: a plain call becomes a request/response pair, while a subscription becomes a small lifecycle of start, stop, interrupt, and receive messages. +Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `193` (the lowest id the codec permits, see the appendix), so a handshake request frame always starts `[requestId][0xC1][0x00]`. Each method carries an explicit discriminant within its trait, assigned via the `#[wire(id = N)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. -Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `193` — the lowest id the codec permits (see the appendix) — so a handshake request frame always starts `[requestId][0xC1][0x00]`. Each action carries an explicit method discriminant within its trait — its `request_id`, `response_id`, `start_id`, `stop_id`, `interrupt_id`, or `receive_id` — assigned per method via the `#[wire(...)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. +#### The nested envelope -Payloads are versioned independently of the discriminant pair, so a single message can evolve without renumbering anything around it. The current version `V1` encodes as discriminant `0`: +A `(trait, method)` pair names a method, not a direction: request and response share it, and so do a subscription's four phases. Direction, and the payload version, both live inside the payload bytes as a small nested envelope instead: ```rust -enum Versioned { - V1(T), +enum Versioned { + V1(Shape), // ... } -``` -Actions are derived from the TrUAPI methods using the following algorithm: - -- For request functions, actions are derived as follows: - - Request - - Name: `method_name + '_request'` - - Argument: `Versioned<(arg1, arg2, ...)>` - - Discriminant: `request_id` - - Response - - Name: `method_name + '_response'` - - Argument: `Versioned>` - - Discriminant: `response_id` -- For subscriptions, there are four messages: - - Subscribe - - Name: `method_name + '_start'` - - Argument: tuple of all arguments except the callback `Versioned<(arg1, arg2, ...)>` - - Discriminant: `start_id` - - Unsubscribe - - Name: `method_name + '_stop'` - - Argument: none - - Discriminant: `stop_id` - - Interrupt - - Name: `method_name + '_interrupt'` - - Argument: none - - Discriminant: `interrupt_id` - - Receive - - Name: `method_name + '_receive'` - - Argument: the versioned callback argument `Versioned` - - Discriminant: `receive_id` - -Put together, a slice of one trait's `Payload` actions looks like this (the payload types are illustrative; see the `truapi` crate for the real ones): +enum Request { + Request(Req), + Response(Res), +} -```rust -enum Payload { - host_handshake_request(Versioned::V1(HandshakeVersion)), - host_handshake_response(Versioned::V1(Result<(), GenericErr>)), +enum Subscription { + Start(Start), + Stop, + Interrupt(Option), // None = clean completion, Some(err) = failure + Receive(Item), +} +``` - // ... - // imaginary subscription method +`Shape` is `Request>` for a plain call, or `Subscription` for a subscription, whichever the method's own return type calls for. The version tag therefore selects a method's *shape*: today every method has exactly one version and one shape, but a later version of the same method could switch a call to a subscription (or vice versa) without needing a new `(trait, method)` pair. On the wire, `Versioned`'s tag and `Request`/`Subscription`'s own direction tag are two consecutive SCALE enum discriminant bytes: `[version][direction][...direction's own payload]`. + +For example, a `system_feature_supported` request/response pair (trait `193`, method `1`) is carried entirely by the payload bytes at that one address: - message_send_request(Versioned::V1((ChainId, str))), - message_send_response(Versioned::V1(Result<(), GenericErr>)), +```text +outbound: [0xC1][0x01][0x00 V1][0x00 Request][...request fields] +inbound: [0xC1][0x01][0x00 V1][0x01 Response][0x00 Ok][...response fields] +``` - message_subscribe_start(Versioned::V1(ChainId)), - message_subscribe_stop, - message_subscribe_interrupt, - message_subscribe_receive(Versioned::V1(str)), +and a subscription's four phases (start, stop, interrupt, receive) all address the same `(trait, method)` pair, distinguished only by which `Subscription` variant tag follows the version byte: - // ... -} +```text +start: [trait][method][0x00 V1][0x00 Start][...start fields] +stop: [trait][method][0x00 V1][0x01 Stop] +interrupt: [trait][method][0x00 V1][0x02 Interrupt][...Option] +receive: [trait][method][0x00 V1][0x03 Receive][...item fields] ``` +Request/response and subscription methods are both derived mechanically from the TrUAPI trait methods, so the high-level method signature and the wire format can never drift apart; nothing is written by hand. + ### Rules A single byte channel carries every call in both directions at once, so the two sides need a way to tell which message belongs to which exchange. That is what `requestId` is for. @@ -137,7 +118,7 @@ Hosts and Products released before this control frame was introduced still silen #### Subscription -A subscription is not a one-shot call but an ongoing stream: the consumer asks once and then receives updates until it stops listening. Its four messages — `start`, `stop`, `interrupt`, and `receive` — MUST all share the same `requestId`, so a subscription handler can route every update and teardown signal to the right place. +A subscription is not a one-shot call but an ongoing stream: the consumer asks once and then receives updates until it stops listening. Its four messages (`start`, `stop`, `interrupt`, and `receive`) all address the same `(trait, method)` pair (distinguished by the `Subscription` direction tag inside the payload) and MUST all share the same `requestId`, so a subscription handler can route every update and teardown signal to the right place. Each message has a defined role: @@ -174,6 +155,8 @@ The concrete handshake request, response, and error types are defined in the `tr Codec version 1 used a single flat `u8` discriminant shared across all traits. Codec version 2 replaces it with the `(trait, method)` pair. This table is the one-time mapping between the two numberings; it exists only to interpret captured codec-1 traffic and old fixtures, and is never extended — new methods only ever get codec-2 pairs. +Codec 1 spent a separate discriminant per action (request and response, or each of a subscription's four phases), where codec 2 spends one `(trait, method)` pair per *method* and folds direction into the payload (see [above](#the-nested-envelope)). The table below therefore still lists one row per historical codec-1 action, but every action belonging to the same method now maps to the same codec-2 pair. + Trait id assignment. Ids start at 193 (`truapi::MIN_TRAIT_ID`), one past `truapi::MAX_CODEC_1_METHOD_ID`: `triangle-js-sdks` `host-api` allocated 166..=171 to the RFC-0024 ring VRF methods, and this crate's own flat numbering @@ -207,162 +190,162 @@ Per-action mapping (codec-1 flat id → codec-2 `(trait, method)` pair): | Action | Codec-1 id | Codec-2 (trait, method) | | --- | --- | --- | | `system_handshake_request` | 0 | (193, 0) | -| `system_handshake_response` | 1 | (193, 1) | -| `system_feature_supported_request` | 2 | (193, 2) | -| `system_feature_supported_response` | 3 | (193, 3) | -| `system_navigate_to_request` | 6 | (193, 4) | -| `system_navigate_to_response` | 7 | (193, 5) | +| `system_handshake_response` | 1 | (193, 0) | +| `system_feature_supported_request` | 2 | (193, 1) | +| `system_feature_supported_response` | 3 | (193, 1) | +| `system_navigate_to_request` | 6 | (193, 2) | +| `system_navigate_to_response` | 7 | (193, 2) | | `account_connection_status_subscribe_start` | 18 | (194, 0) | -| `account_connection_status_subscribe_stop` | 19 | (194, 1) | -| `account_connection_status_subscribe_interrupt` | 20 | (194, 2) | -| `account_connection_status_subscribe_receive` | 21 | (194, 3) | -| `account_get_account_request` | 22 | (194, 4) | -| `account_get_account_response` | 23 | (194, 5) | -| `account_get_account_alias_request` | 24 | (194, 6) | -| `account_get_account_alias_response` | 25 | (194, 7) | -| `account_create_account_proof_request` | 26 | (194, 8) | -| `account_create_account_proof_response` | 27 | (194, 9) | -| `account_get_legacy_accounts_request` | 28 | (194, 10) | -| `account_get_legacy_accounts_response` | 29 | (194, 11) | -| `account_get_user_id_request` | 110 | (194, 12) | -| `account_get_user_id_response` | 111 | (194, 13) | -| `account_request_login_request` | 112 | (194, 14) | -| `account_request_login_response` | 113 | (194, 15) | -| `account_sign_vrf_request` | 164 | (194, 16) | -| `account_sign_vrf_response` | 165 | (194, 17) | +| `account_connection_status_subscribe_stop` | 19 | (194, 0) | +| `account_connection_status_subscribe_interrupt` | 20 | (194, 0) | +| `account_connection_status_subscribe_receive` | 21 | (194, 0) | +| `account_get_account_request` | 22 | (194, 1) | +| `account_get_account_response` | 23 | (194, 1) | +| `account_get_account_alias_request` | 24 | (194, 2) | +| `account_get_account_alias_response` | 25 | (194, 2) | +| `account_create_account_proof_request` | 26 | (194, 3) | +| `account_create_account_proof_response` | 27 | (194, 3) | +| `account_get_legacy_accounts_request` | 28 | (194, 4) | +| `account_get_legacy_accounts_response` | 29 | (194, 4) | +| `account_get_user_id_request` | 110 | (194, 5) | +| `account_get_user_id_response` | 111 | (194, 5) | +| `account_request_login_request` | 112 | (194, 6) | +| `account_request_login_response` | 113 | (194, 6) | +| `account_sign_vrf_request` | 164 | (194, 7) | +| `account_sign_vrf_response` | 165 | (194, 7) | | `chain_follow_head_subscribe_start` | 76 | (195, 0) | -| `chain_follow_head_subscribe_stop` | 77 | (195, 1) | -| `chain_follow_head_subscribe_interrupt` | 78 | (195, 2) | -| `chain_follow_head_subscribe_receive` | 79 | (195, 3) | -| `chain_get_head_header_request` | 80 | (195, 4) | -| `chain_get_head_header_response` | 81 | (195, 5) | -| `chain_get_head_body_request` | 82 | (195, 6) | -| `chain_get_head_body_response` | 83 | (195, 7) | -| `chain_get_head_storage_request` | 84 | (195, 8) | -| `chain_get_head_storage_response` | 85 | (195, 9) | -| `chain_call_head_request` | 86 | (195, 10) | -| `chain_call_head_response` | 87 | (195, 11) | -| `chain_unpin_head_request` | 88 | (195, 12) | -| `chain_unpin_head_response` | 89 | (195, 13) | -| `chain_continue_head_request` | 90 | (195, 14) | -| `chain_continue_head_response` | 91 | (195, 15) | -| `chain_stop_head_operation_request` | 92 | (195, 16) | -| `chain_stop_head_operation_response` | 93 | (195, 17) | -| `chain_get_spec_genesis_hash_request` | 94 | (195, 18) | -| `chain_get_spec_genesis_hash_response` | 95 | (195, 19) | -| `chain_get_spec_chain_name_request` | 96 | (195, 20) | -| `chain_get_spec_chain_name_response` | 97 | (195, 21) | -| `chain_get_spec_properties_request` | 98 | (195, 22) | -| `chain_get_spec_properties_response` | 99 | (195, 23) | -| `chain_broadcast_transaction_request` | 100 | (195, 24) | -| `chain_broadcast_transaction_response` | 101 | (195, 25) | -| `chain_stop_transaction_request` | 102 | (195, 26) | -| `chain_stop_transaction_response` | 103 | (195, 27) | +| `chain_follow_head_subscribe_stop` | 77 | (195, 0) | +| `chain_follow_head_subscribe_interrupt` | 78 | (195, 0) | +| `chain_follow_head_subscribe_receive` | 79 | (195, 0) | +| `chain_get_head_header_request` | 80 | (195, 1) | +| `chain_get_head_header_response` | 81 | (195, 1) | +| `chain_get_head_body_request` | 82 | (195, 2) | +| `chain_get_head_body_response` | 83 | (195, 2) | +| `chain_get_head_storage_request` | 84 | (195, 3) | +| `chain_get_head_storage_response` | 85 | (195, 3) | +| `chain_call_head_request` | 86 | (195, 4) | +| `chain_call_head_response` | 87 | (195, 4) | +| `chain_unpin_head_request` | 88 | (195, 5) | +| `chain_unpin_head_response` | 89 | (195, 5) | +| `chain_continue_head_request` | 90 | (195, 6) | +| `chain_continue_head_response` | 91 | (195, 6) | +| `chain_stop_head_operation_request` | 92 | (195, 7) | +| `chain_stop_head_operation_response` | 93 | (195, 7) | +| `chain_get_spec_genesis_hash_request` | 94 | (195, 8) | +| `chain_get_spec_genesis_hash_response` | 95 | (195, 8) | +| `chain_get_spec_chain_name_request` | 96 | (195, 9) | +| `chain_get_spec_chain_name_response` | 97 | (195, 9) | +| `chain_get_spec_properties_request` | 98 | (195, 10) | +| `chain_get_spec_properties_response` | 99 | (195, 10) | +| `chain_broadcast_transaction_request` | 100 | (195, 11) | +| `chain_broadcast_transaction_response` | 101 | (195, 11) | +| `chain_stop_transaction_request` | 102 | (195, 12) | +| `chain_stop_transaction_response` | 103 | (195, 12) | | `chat_create_room_request` | 38 | (196, 0) | -| `chat_create_room_response` | 39 | (196, 1) | -| `chat_register_bot_request` | 40 | (196, 2) | -| `chat_register_bot_response` | 41 | (196, 3) | -| `chat_list_subscribe_start` | 42 | (196, 4) | -| `chat_list_subscribe_stop` | 43 | (196, 5) | -| `chat_list_subscribe_interrupt` | 44 | (196, 6) | -| `chat_list_subscribe_receive` | 45 | (196, 7) | -| `chat_post_message_request` | 46 | (196, 8) | -| `chat_post_message_response` | 47 | (196, 9) | -| `chat_action_subscribe_start` | 48 | (196, 10) | -| `chat_action_subscribe_stop` | 49 | (196, 11) | -| `chat_action_subscribe_interrupt` | 50 | (196, 12) | -| `chat_action_subscribe_receive` | 51 | (196, 13) | -| `chat_custom_message_render_start` | 52 | (196, 14) | -| `chat_custom_message_render_stop` | 53 | (196, 15) | -| `chat_custom_message_render_interrupt` | 54 | (196, 16) | -| `chat_custom_message_render_receive` | 55 | (196, 17) | +| `chat_create_room_response` | 39 | (196, 0) | +| `chat_register_bot_request` | 40 | (196, 1) | +| `chat_register_bot_response` | 41 | (196, 1) | +| `chat_list_subscribe_start` | 42 | (196, 2) | +| `chat_list_subscribe_stop` | 43 | (196, 2) | +| `chat_list_subscribe_interrupt` | 44 | (196, 2) | +| `chat_list_subscribe_receive` | 45 | (196, 2) | +| `chat_post_message_request` | 46 | (196, 3) | +| `chat_post_message_response` | 47 | (196, 3) | +| `chat_action_subscribe_start` | 48 | (196, 4) | +| `chat_action_subscribe_stop` | 49 | (196, 4) | +| `chat_action_subscribe_interrupt` | 50 | (196, 4) | +| `chat_action_subscribe_receive` | 51 | (196, 4) | +| `chat_custom_message_render_start` | 52 | (196, 5) | +| `chat_custom_message_render_stop` | 53 | (196, 5) | +| `chat_custom_message_render_interrupt` | 54 | (196, 5) | +| `chat_custom_message_render_receive` | 55 | (196, 5) | | `coin_payment_create_purse_request` | 136 | (197, 0) | -| `coin_payment_create_purse_response` | 137 | (197, 1) | -| `coin_payment_query_purse_request` | 138 | (197, 2) | -| `coin_payment_query_purse_response` | 139 | (197, 3) | -| `coin_payment_rebalance_purse_start` | 140 | (197, 4) | -| `coin_payment_rebalance_purse_stop` | 141 | (197, 5) | -| `coin_payment_rebalance_purse_interrupt` | 142 | (197, 6) | -| `coin_payment_rebalance_purse_receive` | 143 | (197, 7) | -| `coin_payment_delete_purse_start` | 144 | (197, 8) | -| `coin_payment_delete_purse_stop` | 145 | (197, 9) | -| `coin_payment_delete_purse_interrupt` | 146 | (197, 10) | -| `coin_payment_delete_purse_receive` | 147 | (197, 11) | -| `coin_payment_create_receivable_request` | 148 | (197, 12) | -| `coin_payment_create_receivable_response` | 149 | (197, 13) | -| `coin_payment_create_cheque_request` | 150 | (197, 14) | -| `coin_payment_create_cheque_response` | 151 | (197, 15) | -| `coin_payment_deposit_start` | 152 | (197, 16) | -| `coin_payment_deposit_stop` | 153 | (197, 17) | -| `coin_payment_deposit_interrupt` | 154 | (197, 18) | -| `coin_payment_deposit_receive` | 155 | (197, 19) | -| `coin_payment_refund_start` | 156 | (197, 20) | -| `coin_payment_refund_stop` | 157 | (197, 21) | -| `coin_payment_refund_interrupt` | 158 | (197, 22) | -| `coin_payment_refund_receive` | 159 | (197, 23) | -| `coin_payment_listen_for_payment_start` | 160 | (197, 24) | -| `coin_payment_listen_for_payment_stop` | 161 | (197, 25) | -| `coin_payment_listen_for_payment_interrupt` | 162 | (197, 26) | -| `coin_payment_listen_for_payment_receive` | 163 | (197, 27) | +| `coin_payment_create_purse_response` | 137 | (197, 0) | +| `coin_payment_query_purse_request` | 138 | (197, 1) | +| `coin_payment_query_purse_response` | 139 | (197, 1) | +| `coin_payment_rebalance_purse_start` | 140 | (197, 2) | +| `coin_payment_rebalance_purse_stop` | 141 | (197, 2) | +| `coin_payment_rebalance_purse_interrupt` | 142 | (197, 2) | +| `coin_payment_rebalance_purse_receive` | 143 | (197, 2) | +| `coin_payment_delete_purse_start` | 144 | (197, 3) | +| `coin_payment_delete_purse_stop` | 145 | (197, 3) | +| `coin_payment_delete_purse_interrupt` | 146 | (197, 3) | +| `coin_payment_delete_purse_receive` | 147 | (197, 3) | +| `coin_payment_create_receivable_request` | 148 | (197, 4) | +| `coin_payment_create_receivable_response` | 149 | (197, 4) | +| `coin_payment_create_cheque_request` | 150 | (197, 5) | +| `coin_payment_create_cheque_response` | 151 | (197, 5) | +| `coin_payment_deposit_start` | 152 | (197, 6) | +| `coin_payment_deposit_stop` | 153 | (197, 6) | +| `coin_payment_deposit_interrupt` | 154 | (197, 6) | +| `coin_payment_deposit_receive` | 155 | (197, 6) | +| `coin_payment_refund_start` | 156 | (197, 7) | +| `coin_payment_refund_stop` | 157 | (197, 7) | +| `coin_payment_refund_interrupt` | 158 | (197, 7) | +| `coin_payment_refund_receive` | 159 | (197, 7) | +| `coin_payment_listen_for_payment_start` | 160 | (197, 8) | +| `coin_payment_listen_for_payment_stop` | 161 | (197, 8) | +| `coin_payment_listen_for_payment_interrupt` | 162 | (197, 8) | +| `coin_payment_listen_for_payment_receive` | 163 | (197, 8) | | `entropy_derive_request` | 108 | (198, 0) | -| `entropy_derive_response` | 109 | (198, 1) | +| `entropy_derive_response` | 109 | (198, 0) | | `local_storage_read_request` | 12 | (199, 0) | -| `local_storage_read_response` | 13 | (199, 1) | -| `local_storage_write_request` | 14 | (199, 2) | -| `local_storage_write_response` | 15 | (199, 3) | -| `local_storage_clear_request` | 16 | (199, 4) | -| `local_storage_clear_response` | 17 | (199, 5) | +| `local_storage_read_response` | 13 | (199, 0) | +| `local_storage_write_request` | 14 | (199, 1) | +| `local_storage_write_response` | 15 | (199, 1) | +| `local_storage_clear_request` | 16 | (199, 2) | +| `local_storage_clear_response` | 17 | (199, 2) | | `notifications_send_push_notification_request` | 4 | (200, 0) | -| `notifications_send_push_notification_response` | 5 | (200, 1) | -| `notifications_cancel_push_notification_request` | 134 | (200, 2) | -| `notifications_cancel_push_notification_response` | 135 | (200, 3) | +| `notifications_send_push_notification_response` | 5 | (200, 0) | +| `notifications_cancel_push_notification_request` | 134 | (200, 1) | +| `notifications_cancel_push_notification_response` | 135 | (200, 1) | | `payment_balance_subscribe_start` | 118 | (201, 0) | -| `payment_balance_subscribe_stop` | 119 | (201, 1) | -| `payment_balance_subscribe_interrupt` | 120 | (201, 2) | -| `payment_balance_subscribe_receive` | 121 | (201, 3) | -| `payment_top_up_request` | 122 | (201, 4) | -| `payment_top_up_response` | 123 | (201, 5) | -| `payment_request_request` | 124 | (201, 6) | -| `payment_request_response` | 125 | (201, 7) | -| `payment_status_subscribe_start` | 126 | (201, 8) | -| `payment_status_subscribe_stop` | 127 | (201, 9) | -| `payment_status_subscribe_interrupt` | 128 | (201, 10) | -| `payment_status_subscribe_receive` | 129 | (201, 11) | +| `payment_balance_subscribe_stop` | 119 | (201, 0) | +| `payment_balance_subscribe_interrupt` | 120 | (201, 0) | +| `payment_balance_subscribe_receive` | 121 | (201, 0) | +| `payment_top_up_request` | 122 | (201, 1) | +| `payment_top_up_response` | 123 | (201, 1) | +| `payment_request_request` | 124 | (201, 2) | +| `payment_request_response` | 125 | (201, 2) | +| `payment_status_subscribe_start` | 126 | (201, 3) | +| `payment_status_subscribe_stop` | 127 | (201, 3) | +| `payment_status_subscribe_interrupt` | 128 | (201, 3) | +| `payment_status_subscribe_receive` | 129 | (201, 3) | | `permissions_request_device_permission_request` | 8 | (202, 0) | -| `permissions_request_device_permission_response` | 9 | (202, 1) | -| `permissions_request_remote_permission_request` | 10 | (202, 2) | -| `permissions_request_remote_permission_response` | 11 | (202, 3) | +| `permissions_request_device_permission_response` | 9 | (202, 0) | +| `permissions_request_remote_permission_request` | 10 | (202, 1) | +| `permissions_request_remote_permission_response` | 11 | (202, 1) | | `preimage_lookup_subscribe_start` | 64 | (203, 0) | -| `preimage_lookup_subscribe_stop` | 65 | (203, 1) | -| `preimage_lookup_subscribe_interrupt` | 66 | (203, 2) | -| `preimage_lookup_subscribe_receive` | 67 | (203, 3) | -| `preimage_submit_request` | 68 | (203, 4) | -| `preimage_submit_response` | 69 | (203, 5) | +| `preimage_lookup_subscribe_stop` | 65 | (203, 0) | +| `preimage_lookup_subscribe_interrupt` | 66 | (203, 0) | +| `preimage_lookup_subscribe_receive` | 67 | (203, 0) | +| `preimage_submit_request` | 68 | (203, 1) | +| `preimage_submit_response` | 69 | (203, 1) | | `resource_allocation_request_request` | 130 | (204, 0) | -| `resource_allocation_request_response` | 131 | (204, 1) | +| `resource_allocation_request_response` | 131 | (204, 0) | | `signing_create_transaction_request` | 30 | (205, 0) | -| `signing_create_transaction_response` | 31 | (205, 1) | -| `signing_create_transaction_with_legacy_account_request` | 32 | (205, 2) | -| `signing_create_transaction_with_legacy_account_response` | 33 | (205, 3) | -| `signing_sign_raw_with_legacy_account_request` | 34 | (205, 4) | -| `signing_sign_raw_with_legacy_account_response` | 35 | (205, 5) | -| `signing_sign_payload_with_legacy_account_request` | 36 | (205, 6) | -| `signing_sign_payload_with_legacy_account_response` | 37 | (205, 7) | -| `signing_sign_raw_request` | 114 | (205, 8) | -| `signing_sign_raw_response` | 115 | (205, 9) | -| `signing_sign_payload_request` | 116 | (205, 10) | -| `signing_sign_payload_response` | 117 | (205, 11) | +| `signing_create_transaction_response` | 31 | (205, 0) | +| `signing_create_transaction_with_legacy_account_request` | 32 | (205, 1) | +| `signing_create_transaction_with_legacy_account_response` | 33 | (205, 1) | +| `signing_sign_raw_with_legacy_account_request` | 34 | (205, 2) | +| `signing_sign_raw_with_legacy_account_response` | 35 | (205, 2) | +| `signing_sign_payload_with_legacy_account_request` | 36 | (205, 3) | +| `signing_sign_payload_with_legacy_account_response` | 37 | (205, 3) | +| `signing_sign_raw_request` | 114 | (205, 4) | +| `signing_sign_raw_response` | 115 | (205, 4) | +| `signing_sign_payload_request` | 116 | (205, 5) | +| `signing_sign_payload_response` | 117 | (205, 5) | | `statement_store_subscribe_start` | 56 | (206, 0) | -| `statement_store_subscribe_stop` | 57 | (206, 1) | -| `statement_store_subscribe_interrupt` | 58 | (206, 2) | -| `statement_store_subscribe_receive` | 59 | (206, 3) | -| `statement_store_create_proof_request` | 60 | (206, 4) | -| `statement_store_create_proof_response` | 61 | (206, 5) | -| `statement_store_submit_request` | 62 | (206, 6) | -| `statement_store_submit_response` | 63 | (206, 7) | -| `statement_store_create_proof_authorized_request` | 132 | (206, 8) | -| `statement_store_create_proof_authorized_response` | 133 | (206, 9) | +| `statement_store_subscribe_stop` | 57 | (206, 0) | +| `statement_store_subscribe_interrupt` | 58 | (206, 0) | +| `statement_store_subscribe_receive` | 59 | (206, 0) | +| `statement_store_create_proof_request` | 60 | (206, 1) | +| `statement_store_create_proof_response` | 61 | (206, 1) | +| `statement_store_submit_request` | 62 | (206, 2) | +| `statement_store_submit_response` | 63 | (206, 2) | +| `statement_store_create_proof_authorized_request` | 132 | (206, 3) | +| `statement_store_create_proof_authorized_response` | 133 | (206, 3) | | `theme_subscribe_start` | 104 | (207, 0) | -| `theme_subscribe_stop` | 105 | (207, 1) | -| `theme_subscribe_interrupt` | 106 | (207, 2) | -| `theme_subscribe_receive` | 107 | (207, 3) | +| `theme_subscribe_stop` | 105 | (207, 0) | +| `theme_subscribe_interrupt` | 106 | (207, 0) | +| `theme_subscribe_receive` | 107 | (207, 0) | diff --git a/js/packages/truapi/README.md b/js/packages/truapi/README.md index e6b38e12e..02b8a0a79 100644 --- a/js/packages/truapi/README.md +++ b/js/packages/truapi/README.md @@ -135,7 +135,7 @@ Frames are SCALE encoded: [requestId: SCALE str][trait: u8][method: u8][payload bytes...] ``` -The discriminant is a `(trait, method)` pair: the trait byte names the API trait and the method byte addresses a method within it, so method ids restart at 0 in every trait. The table is generated from the Rust trait-level `#[wire_trait(id = N)]` annotation plus the method-level `#[wire(request_id = N)]` and `#[wire(start_id = N)]` annotations, and is written to `src/generated/wire-table.ts`. +The discriminant is a `(trait, method)` pair: the trait byte names the API trait and the method byte addresses a method within it, so method ids restart at 0 in every trait. Direction (request vs. response, or a subscription's start/stop/interrupt/receive) is carried inside the payload rather than by a separate id, so one method occupies exactly one id regardless of shape. The table is generated from the Rust trait-level `#[wire_trait(id = N)]` annotation plus the method-level `#[wire(id = N)]` annotation, and is written to `src/generated/wire-table.ts`. This layout is wire codec version 2 and is not compatible with codec version 1, which addressed methods with a single flat byte. diff --git a/rust/crates/truapi/README.md b/rust/crates/truapi/README.md index b4b1a63e6..3f3d45b0a 100644 --- a/rust/crates/truapi/README.md +++ b/rust/crates/truapi/README.md @@ -10,7 +10,7 @@ It defines: - **Versioned data types** under `v01` and `versioned`. - **Domain API traits** under `api/`, plus the composed `TrUApi` trait. -- **Wire ids** via trait-level `#[wire_trait(id = N)]` and per-method `#[wire(request_id = N)]` annotations that pin the byte-level `(trait, method)` dispatch table. +- **Wire ids** via trait-level `#[wire_trait(id = N)]` and per-method `#[wire(id = N)]` annotations that pin the byte-level `(trait, method)` dispatch table. - **Subscription primitives** through `Subscription` for streamed host responses. - **Authoring types** like `CallContext`, `CallError`, and `CancellationToken`. From 6ebb401fe51088b75ad9a502dabd109f21a44d24 Mon Sep 17 00:00:00 2001 From: Nidish Date: Wed, 2 Sep 2026 14:50:30 +0530 Subject: [PATCH 16/16] fix(wire): decode interrupt errors, encode declines as failures, drop the trait-id floor --- docs/design/truapi-protocol.md | 207 +----- .../Tests/TrUAPIWsBridgeTests.swift | 8 +- js/packages/truapi/src/client.test.ts | 68 +- js/packages/truapi/src/client.ts | 10 - js/packages/truapi/src/transport.ts | 8 - js/packages/truapi/src/wire-equality.test.ts | 26 +- rust/crates/truapi-codegen/src/rust.rs | 173 +---- .../truapi-codegen/src/rust/dispatcher.rs | 626 +++--------------- .../truapi-codegen/src/rust/wire_table.rs | 17 +- rust/crates/truapi-codegen/src/rustdoc.rs | 2 - rust/crates/truapi-codegen/src/ts.rs | 614 ++++++----------- .../truapi-codegen/src/ts/playground.rs | 30 +- .../truapi-codegen/tests/golden/wire_table.rs | 144 ++-- .../truapi-codegen/tests/golden_rust_emit.rs | 49 +- rust/crates/truapi-server/src/dispatcher.rs | 20 - rust/crates/truapi-server/src/frame.rs | 30 +- .../truapi-server/src/generated/wire_table.rs | 144 ++-- .../truapi-server/tests/golden_frame.rs | 2 +- .../tests/snapshots/golden-account-get.bin | Bin 17 -> 17 bytes .../tests/wire_table_ts_parity.rs | 10 +- rust/crates/truapi/src/api/account.rs | 2 +- rust/crates/truapi/src/api/chain.rs | 2 +- rust/crates/truapi/src/api/chat.rs | 2 +- rust/crates/truapi/src/api/coin_payment.rs | 2 +- rust/crates/truapi/src/api/entropy.rs | 2 +- rust/crates/truapi/src/api/local_storage.rs | 2 +- rust/crates/truapi/src/api/locale.rs | 2 +- rust/crates/truapi/src/api/notifications.rs | 2 +- rust/crates/truapi/src/api/payment.rs | 2 +- rust/crates/truapi/src/api/permissions.rs | 2 +- rust/crates/truapi/src/api/preimage.rs | 2 +- .../truapi/src/api/resource_allocation.rs | 2 +- rust/crates/truapi/src/api/signing.rs | 2 +- rust/crates/truapi/src/api/statement_store.rs | 2 +- rust/crates/truapi/src/api/system.rs | 2 +- rust/crates/truapi/src/api/theme.rs | 2 +- rust/crates/truapi/src/lib.rs | 16 - 37 files changed, 600 insertions(+), 1636 deletions(-) diff --git a/docs/design/truapi-protocol.md b/docs/design/truapi-protocol.md index 7681bfa43..1f34e8770 100644 --- a/docs/design/truapi-protocol.md +++ b/docs/design/truapi-protocol.md @@ -57,7 +57,7 @@ struct Message { The two bytes after the `requestId` are the **`(trait, method)` discriminant pair**. The first byte identifies the API trait (`System`, `Account`, `Chain`, ...); the second identifies a method within it: exactly one id per method, regardless of that method's shape. The payload bytes are the SCALE-encoded value for that method's own nested envelope (below), inlined without a length prefix; the receiver reads to the end of the transport frame. -Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `193` (the lowest id the codec permits, see the appendix), so a handshake request frame always starts `[requestId][0xC1][0x00]`. Each method carries an explicit discriminant within its trait, assigned via the `#[wire(id = N)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. +Trait discriminants are assigned per trait in the `truapi` crate via the trait-level `#[wire_trait(id = N)]` annotation, with the `System` trait fixed at `1`, so a handshake request frame always starts `[requestId][0x01][0x00]`. Each method carries an explicit discriminant within its trait, assigned via the `#[wire(id = N)]` annotation and numbered from `0` independently inside every trait. Ids are **append-only per trait and never reused**: once a `(trait, method)` pair ships it keeps its meaning forever, which is what lets a newer Host and an older Product still understand each other, and adding methods to one trait never disturbs the ids of any other trait. The crate is the source of truth for all values. Trait discriminant `255` is permanently reserved for protocol errors and cannot be assigned to an API trait, so no method can ever be addressed there; a protocol error travels on the pair `(255, 255)`. #### The nested envelope @@ -84,11 +84,11 @@ enum Subscription { `Shape` is `Request>` for a plain call, or `Subscription` for a subscription, whichever the method's own return type calls for. The version tag therefore selects a method's *shape*: today every method has exactly one version and one shape, but a later version of the same method could switch a call to a subscription (or vice versa) without needing a new `(trait, method)` pair. On the wire, `Versioned`'s tag and `Request`/`Subscription`'s own direction tag are two consecutive SCALE enum discriminant bytes: `[version][direction][...direction's own payload]`. -For example, a `system_feature_supported` request/response pair (trait `193`, method `1`) is carried entirely by the payload bytes at that one address: +For example, a `system_feature_supported` request/response pair (trait `1`, method `1`) is carried entirely by the payload bytes at that one address: ```text -outbound: [0xC1][0x01][0x00 V1][0x00 Request][...request fields] -inbound: [0xC1][0x01][0x00 V1][0x01 Response][0x00 Ok][...response fields] +outbound: [0x01][0x01][0x00 V1][0x00 Request][...request fields] +inbound: [0x01][0x01][0x00 V1][0x01 Response][0x00 Ok][...response fields] ``` and a subscription's four phases (start, stop, interrupt, receive) all address the same `(trait, method)` pair, distinguished only by which `Subscription` variant tag follows the version byte: @@ -150,202 +150,3 @@ The handshake request carries the protocol (codec) version as a `u8`. On receivi The concrete handshake request, response, and error types are defined in the `truapi` crate. - -## Appendix: codec-1 → codec-2 discriminant mapping - -Codec version 1 used a single flat `u8` discriminant shared across all traits. Codec version 2 replaces it with the `(trait, method)` pair. This table is the one-time mapping between the two numberings; it exists only to interpret captured codec-1 traffic and old fixtures, and is never extended — new methods only ever get codec-2 pairs. - -Codec 1 spent a separate discriminant per action (request and response, or each of a subscription's four phases), where codec 2 spends one `(trait, method)` pair per *method* and folds direction into the payload (see [above](#the-nested-envelope)). The table below therefore still lists one row per historical codec-1 action, but every action belonging to the same method now maps to the same codec-2 pair. - -Trait id assignment. Ids start at 193 (`truapi::MIN_TRAIT_ID`), one past -`truapi::MAX_CODEC_1_METHOD_ID`: `triangle-js-sdks` `host-api` allocated -166..=171 to the RFC-0024 ring VRF methods, and this crate's own flat numbering -later reached 192 via `System::host_info`, added on `main` while codec 2 was -still unmerged. 192 is therefore the highest known codec-1 discriminant across -both, so no codec-1 frame's first byte can name a codec-2 trait, and such a -frame is reported as unroutable instead of decoding into whichever trait would -otherwise share its old id. Codegen rejects any `#[wire_trait(id = N)]` below -the floor. - -| Trait | Trait id | -| --- | --- | -| `System` | 193 | -| `Account` | 194 | -| `Chain` | 195 | -| `Chat` | 196 | -| `CoinPayment` | 197 | -| `Entropy` | 198 | -| `LocalStorage` | 199 | -| `Notifications` | 200 | -| `Payment` | 201 | -| `Permissions` | 202 | -| `Preimage` | 203 | -| `ResourceAllocation` | 204 | -| `Signing` | 205 | -| `StatementStore` | 206 | -| `Theme` | 207 | - -Per-action mapping (codec-1 flat id → codec-2 `(trait, method)` pair): - -| Action | Codec-1 id | Codec-2 (trait, method) | -| --- | --- | --- | -| `system_handshake_request` | 0 | (193, 0) | -| `system_handshake_response` | 1 | (193, 0) | -| `system_feature_supported_request` | 2 | (193, 1) | -| `system_feature_supported_response` | 3 | (193, 1) | -| `system_navigate_to_request` | 6 | (193, 2) | -| `system_navigate_to_response` | 7 | (193, 2) | -| `account_connection_status_subscribe_start` | 18 | (194, 0) | -| `account_connection_status_subscribe_stop` | 19 | (194, 0) | -| `account_connection_status_subscribe_interrupt` | 20 | (194, 0) | -| `account_connection_status_subscribe_receive` | 21 | (194, 0) | -| `account_get_account_request` | 22 | (194, 1) | -| `account_get_account_response` | 23 | (194, 1) | -| `account_get_account_alias_request` | 24 | (194, 2) | -| `account_get_account_alias_response` | 25 | (194, 2) | -| `account_create_account_proof_request` | 26 | (194, 3) | -| `account_create_account_proof_response` | 27 | (194, 3) | -| `account_get_legacy_accounts_request` | 28 | (194, 4) | -| `account_get_legacy_accounts_response` | 29 | (194, 4) | -| `account_get_user_id_request` | 110 | (194, 5) | -| `account_get_user_id_response` | 111 | (194, 5) | -| `account_request_login_request` | 112 | (194, 6) | -| `account_request_login_response` | 113 | (194, 6) | -| `account_sign_vrf_request` | 164 | (194, 7) | -| `account_sign_vrf_response` | 165 | (194, 7) | -| `chain_follow_head_subscribe_start` | 76 | (195, 0) | -| `chain_follow_head_subscribe_stop` | 77 | (195, 0) | -| `chain_follow_head_subscribe_interrupt` | 78 | (195, 0) | -| `chain_follow_head_subscribe_receive` | 79 | (195, 0) | -| `chain_get_head_header_request` | 80 | (195, 1) | -| `chain_get_head_header_response` | 81 | (195, 1) | -| `chain_get_head_body_request` | 82 | (195, 2) | -| `chain_get_head_body_response` | 83 | (195, 2) | -| `chain_get_head_storage_request` | 84 | (195, 3) | -| `chain_get_head_storage_response` | 85 | (195, 3) | -| `chain_call_head_request` | 86 | (195, 4) | -| `chain_call_head_response` | 87 | (195, 4) | -| `chain_unpin_head_request` | 88 | (195, 5) | -| `chain_unpin_head_response` | 89 | (195, 5) | -| `chain_continue_head_request` | 90 | (195, 6) | -| `chain_continue_head_response` | 91 | (195, 6) | -| `chain_stop_head_operation_request` | 92 | (195, 7) | -| `chain_stop_head_operation_response` | 93 | (195, 7) | -| `chain_get_spec_genesis_hash_request` | 94 | (195, 8) | -| `chain_get_spec_genesis_hash_response` | 95 | (195, 8) | -| `chain_get_spec_chain_name_request` | 96 | (195, 9) | -| `chain_get_spec_chain_name_response` | 97 | (195, 9) | -| `chain_get_spec_properties_request` | 98 | (195, 10) | -| `chain_get_spec_properties_response` | 99 | (195, 10) | -| `chain_broadcast_transaction_request` | 100 | (195, 11) | -| `chain_broadcast_transaction_response` | 101 | (195, 11) | -| `chain_stop_transaction_request` | 102 | (195, 12) | -| `chain_stop_transaction_response` | 103 | (195, 12) | -| `chat_create_room_request` | 38 | (196, 0) | -| `chat_create_room_response` | 39 | (196, 0) | -| `chat_register_bot_request` | 40 | (196, 1) | -| `chat_register_bot_response` | 41 | (196, 1) | -| `chat_list_subscribe_start` | 42 | (196, 2) | -| `chat_list_subscribe_stop` | 43 | (196, 2) | -| `chat_list_subscribe_interrupt` | 44 | (196, 2) | -| `chat_list_subscribe_receive` | 45 | (196, 2) | -| `chat_post_message_request` | 46 | (196, 3) | -| `chat_post_message_response` | 47 | (196, 3) | -| `chat_action_subscribe_start` | 48 | (196, 4) | -| `chat_action_subscribe_stop` | 49 | (196, 4) | -| `chat_action_subscribe_interrupt` | 50 | (196, 4) | -| `chat_action_subscribe_receive` | 51 | (196, 4) | -| `chat_custom_message_render_start` | 52 | (196, 5) | -| `chat_custom_message_render_stop` | 53 | (196, 5) | -| `chat_custom_message_render_interrupt` | 54 | (196, 5) | -| `chat_custom_message_render_receive` | 55 | (196, 5) | -| `coin_payment_create_purse_request` | 136 | (197, 0) | -| `coin_payment_create_purse_response` | 137 | (197, 0) | -| `coin_payment_query_purse_request` | 138 | (197, 1) | -| `coin_payment_query_purse_response` | 139 | (197, 1) | -| `coin_payment_rebalance_purse_start` | 140 | (197, 2) | -| `coin_payment_rebalance_purse_stop` | 141 | (197, 2) | -| `coin_payment_rebalance_purse_interrupt` | 142 | (197, 2) | -| `coin_payment_rebalance_purse_receive` | 143 | (197, 2) | -| `coin_payment_delete_purse_start` | 144 | (197, 3) | -| `coin_payment_delete_purse_stop` | 145 | (197, 3) | -| `coin_payment_delete_purse_interrupt` | 146 | (197, 3) | -| `coin_payment_delete_purse_receive` | 147 | (197, 3) | -| `coin_payment_create_receivable_request` | 148 | (197, 4) | -| `coin_payment_create_receivable_response` | 149 | (197, 4) | -| `coin_payment_create_cheque_request` | 150 | (197, 5) | -| `coin_payment_create_cheque_response` | 151 | (197, 5) | -| `coin_payment_deposit_start` | 152 | (197, 6) | -| `coin_payment_deposit_stop` | 153 | (197, 6) | -| `coin_payment_deposit_interrupt` | 154 | (197, 6) | -| `coin_payment_deposit_receive` | 155 | (197, 6) | -| `coin_payment_refund_start` | 156 | (197, 7) | -| `coin_payment_refund_stop` | 157 | (197, 7) | -| `coin_payment_refund_interrupt` | 158 | (197, 7) | -| `coin_payment_refund_receive` | 159 | (197, 7) | -| `coin_payment_listen_for_payment_start` | 160 | (197, 8) | -| `coin_payment_listen_for_payment_stop` | 161 | (197, 8) | -| `coin_payment_listen_for_payment_interrupt` | 162 | (197, 8) | -| `coin_payment_listen_for_payment_receive` | 163 | (197, 8) | -| `entropy_derive_request` | 108 | (198, 0) | -| `entropy_derive_response` | 109 | (198, 0) | -| `local_storage_read_request` | 12 | (199, 0) | -| `local_storage_read_response` | 13 | (199, 0) | -| `local_storage_write_request` | 14 | (199, 1) | -| `local_storage_write_response` | 15 | (199, 1) | -| `local_storage_clear_request` | 16 | (199, 2) | -| `local_storage_clear_response` | 17 | (199, 2) | -| `notifications_send_push_notification_request` | 4 | (200, 0) | -| `notifications_send_push_notification_response` | 5 | (200, 0) | -| `notifications_cancel_push_notification_request` | 134 | (200, 1) | -| `notifications_cancel_push_notification_response` | 135 | (200, 1) | -| `payment_balance_subscribe_start` | 118 | (201, 0) | -| `payment_balance_subscribe_stop` | 119 | (201, 0) | -| `payment_balance_subscribe_interrupt` | 120 | (201, 0) | -| `payment_balance_subscribe_receive` | 121 | (201, 0) | -| `payment_top_up_request` | 122 | (201, 1) | -| `payment_top_up_response` | 123 | (201, 1) | -| `payment_request_request` | 124 | (201, 2) | -| `payment_request_response` | 125 | (201, 2) | -| `payment_status_subscribe_start` | 126 | (201, 3) | -| `payment_status_subscribe_stop` | 127 | (201, 3) | -| `payment_status_subscribe_interrupt` | 128 | (201, 3) | -| `payment_status_subscribe_receive` | 129 | (201, 3) | -| `permissions_request_device_permission_request` | 8 | (202, 0) | -| `permissions_request_device_permission_response` | 9 | (202, 0) | -| `permissions_request_remote_permission_request` | 10 | (202, 1) | -| `permissions_request_remote_permission_response` | 11 | (202, 1) | -| `preimage_lookup_subscribe_start` | 64 | (203, 0) | -| `preimage_lookup_subscribe_stop` | 65 | (203, 0) | -| `preimage_lookup_subscribe_interrupt` | 66 | (203, 0) | -| `preimage_lookup_subscribe_receive` | 67 | (203, 0) | -| `preimage_submit_request` | 68 | (203, 1) | -| `preimage_submit_response` | 69 | (203, 1) | -| `resource_allocation_request_request` | 130 | (204, 0) | -| `resource_allocation_request_response` | 131 | (204, 0) | -| `signing_create_transaction_request` | 30 | (205, 0) | -| `signing_create_transaction_response` | 31 | (205, 0) | -| `signing_create_transaction_with_legacy_account_request` | 32 | (205, 1) | -| `signing_create_transaction_with_legacy_account_response` | 33 | (205, 1) | -| `signing_sign_raw_with_legacy_account_request` | 34 | (205, 2) | -| `signing_sign_raw_with_legacy_account_response` | 35 | (205, 2) | -| `signing_sign_payload_with_legacy_account_request` | 36 | (205, 3) | -| `signing_sign_payload_with_legacy_account_response` | 37 | (205, 3) | -| `signing_sign_raw_request` | 114 | (205, 4) | -| `signing_sign_raw_response` | 115 | (205, 4) | -| `signing_sign_payload_request` | 116 | (205, 5) | -| `signing_sign_payload_response` | 117 | (205, 5) | -| `statement_store_subscribe_start` | 56 | (206, 0) | -| `statement_store_subscribe_stop` | 57 | (206, 0) | -| `statement_store_subscribe_interrupt` | 58 | (206, 0) | -| `statement_store_subscribe_receive` | 59 | (206, 0) | -| `statement_store_create_proof_request` | 60 | (206, 1) | -| `statement_store_create_proof_response` | 61 | (206, 1) | -| `statement_store_submit_request` | 62 | (206, 2) | -| `statement_store_submit_response` | 63 | (206, 2) | -| `statement_store_create_proof_authorized_request` | 132 | (206, 3) | -| `statement_store_create_proof_authorized_response` | 133 | (206, 3) | -| `theme_subscribe_start` | 104 | (207, 0) | -| `theme_subscribe_stop` | 105 | (207, 0) | -| `theme_subscribe_interrupt` | 106 | (207, 0) | -| `theme_subscribe_receive` | 107 | (207, 0) | diff --git a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift index 9170349df..3edc255ed 100644 --- a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift +++ b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift @@ -83,13 +83,13 @@ private extension TrUAPIWsBridgeTests { ) } - // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 193, method_id: 1 }. + // wire_table.rs: SYSTEM_FEATURE_SUPPORTED { trait_id: 1, method_id: 1 }. // Both bytes are load-bearing: a lone method byte is read as the trait and // routes into a different trait's method 0 rather than failing. - static let featureSupportedDiscriminant = Data([0xC1, 0x01]) + static let featureSupportedDiscriminant = Data([0x01, 0x01]) - // wire_table.rs: SYSTEM_HOST_INFO { trait_id: 193, method_id: 3 }. - static let hostInfoDiscriminant = Data([0xC1, 0x03]) + // wire_table.rs: SYSTEM_HOST_INFO { trait_id: 1, method_id: 3 }. + static let hostInfoDiscriminant = Data([0x01, 0x03]) static func hostInfoRequestFrame() -> Data { var frame = Data() diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 4c789e160..8f6043dbe 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -135,8 +135,9 @@ function rendererReceive(requestId: string, node: T.CustomRendererNode): Uint8Ar /** * The fixed frame `transport.ts` sends to decline a host-initiated render: - * `[version=V1, direction=Interrupt, Option::None]`. The host only reads the - * direction byte for this flow, so one constant frame covers every method. + * `[version=V1, direction=Interrupt, Some(CallError::HostFailure{reason: + * "unavailable"})]`. `HostFailure`'s payload doesn't depend on the method's + * own domain error type, so one constant frame covers every method. */ function rendererInterrupt(requestId: string): Uint8Array { return unwrap( @@ -145,7 +146,9 @@ function rendererInterrupt(requestId: string): Uint8Array { payload: { traitId: W.CHAT_CUSTOM_MESSAGE_RENDER.trait, methodId: W.CHAT_CUSTOM_MESSAGE_RENDER.method, - value: new Uint8Array([0, 2, 0]), + value: new Uint8Array([ + 0, 2, 1, 4, 44, 117, 110, 97, 118, 97, 105, 108, 97, 98, 108, 101, + ]), }, }), "encode renderer interrupt", @@ -216,7 +219,7 @@ describe("generated client transport", () => { }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 194; // account trait + expectedFrame[str.enc("p:1").length] = 2; // account trait expectedFrame[str.enc("p:1").length + 1] = 1; // get_account expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); @@ -236,7 +239,7 @@ describe("generated client transport", () => { }); const expectedFrame = new Uint8Array(str.enc("p:1").length + 2 + expectedPayload.length); expectedFrame.set(str.enc("p:1"), 0); - expectedFrame[str.enc("p:1").length] = 193; // system trait + expectedFrame[str.enc("p:1").length] = 1; // system trait expectedFrame[str.enc("p:1").length + 1] = 0; // handshake expectedFrame.set(expectedPayload, str.enc("p:1").length + 2); @@ -706,16 +709,17 @@ describe("generated client transport", () => { expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); }); - it("refuses a codec 1 handshake ping and stays usable", () => { + it("answers a codec 1 handshake ping with a protocol error and stays usable", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); const client = createClient(transport); // A codec 1 host frames its ping as [requestId][u8 id=0][V1][codec=1]. // Read against the two-byte discriminant that is trait 0, method 0 -- - // and trait 0 is below the codec 2 floor, so it can never name a real - // trait. The ping is refused rather than answered on a wire the peer - // cannot parse anyway. + // trait 0 is unassigned (real traits start at 1), so this is an + // ordinary unknown pair, answered with a protocol error like any + // other, rather than silently dropped on a guess about the sender's + // codec version. const legacyFrame = new Uint8Array([ ...str.enc("h:1"), 0x00, // old flat discriminant, read as the trait byte @@ -724,12 +728,13 @@ describe("generated client transport", () => { ]); fixture.receive(legacyFrame); - expect(fixture.sent.length).toBe(0); + expect(fixture.sent.length).toBe(1); + expect(toHex(fixture.sent[0])).toBe(toHex(unsupportedMessage("h:1", 0, 0))); // The transport must survive: a ping it cannot parse is a peer // problem, not grounds for tearing down every pending call. void client.account.getAccount({ productAccountId: { dotNsIdentifier: "foo", derivationIndex: { tag: "Index", value: 0 } } }); - expect(fixture.sent.length).toBe(1); + expect(fixture.sent.length).toBe(2); }); it("ignores a response whose trait does not match the pending request", async () => { @@ -985,6 +990,47 @@ describe("generated client transport", () => { expect(completions).toEqual([[]]); }); + it("surfaces a framework-level interrupt as an observable error on a plain subscription", () => { + // `chat.listSubscribe` has no domain error of its own (unlike + // `payment.balanceSubscribe` below): its Interrupt carries a bare + // `CallErrorValue`. A worker-only subscription like this + // one denied to an app connection, or a malformed start frame, both + // arrive this way and must surface as a real error, not a silent + // `.complete()`. + const fixture = providerFixture(); + const transport = createTransport(fixture.provider); + const client = createClient(transport); + const completions: unknown[][] = []; + const errors: Error[] = []; + + const sub = client.chat.listSubscribe().subscribe({ + complete: (...args) => completions.push(args), + error: (error) => errors.push(error), + }); + + const callError: CallErrorValue = { tag: "Denied" }; + const frame = unwrap( + encodeWireMessage({ + requestId: sub.subscriptionId, + payload: { + traitId: W.CHAT_LIST_SUBSCRIBE.trait, + methodId: W.CHAT_LIST_SUBSCRIBE.method, + value: T.HostChatListSubscribeVersion.enc({ + tag: "V1", + value: { tag: "Interrupt", value: callError }, + }), + }, + }), + "encode denied interrupt", + ); + fixture.receive(frame); + + expect(completions).toEqual([]); + expect(errors).toHaveLength(1); + expect(errors[0]).toBeInstanceOf(SubscriptionError); + expect((errors[0] as SubscriptionError).reason).toEqual(callError); + }); + it("surfaces a typed payment interrupt as an observable error", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); diff --git a/js/packages/truapi/src/client.ts b/js/packages/truapi/src/client.ts index f2cf9011b..04b196d40 100644 --- a/js/packages/truapi/src/client.ts +++ b/js/packages/truapi/src/client.ts @@ -3,7 +3,6 @@ import { errAsync, okAsync, ResultAsync } from "neverthrow"; import { decodeWireMessage, encodeWireMessage, - MIN_TRAIT_ID, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, type HostInitiatedSubscriptionHandler, @@ -474,15 +473,6 @@ export function createTransport( reportProtocolViolation( `unsupported frame with discriminant (${payload.traitId}, ${payload.methodId}): request ${requestId} is not pending and has no subscription`, ); - // Answer only a peer that could read the answer. A trait byte below the - // floor is not a trait at all - it is a codec 1 peer's flat method id - and - // such a peer would read our `(255, 255)` reply as codec 1 discriminant 255 - // with a payload it cannot decode, and tear its own transport down over a - // malformed-protocol-error that says nothing about the real problem. The - // log above is the diagnostic for that case. - if (payload.traitId < MIN_TRAIT_ID) { - return; - } try { send({ requestId, diff --git a/js/packages/truapi/src/transport.ts b/js/packages/truapi/src/transport.ts index eb1e420ec..8e0fa65be 100644 --- a/js/packages/truapi/src/transport.ts +++ b/js/packages/truapi/src/transport.ts @@ -12,14 +12,6 @@ export const PROTOCOL_ERROR_TRAIT_ID = 255 as const; /** Wire method discriminant reserved for method-independent protocol errors. **/ export const PROTOCOL_ERROR_METHOD_ID = 255 as const; -/** - * Lowest trait id the codec permits, mirroring `truapi::MIN_TRAIT_ID`. A frame - * whose trait byte falls below it is not naming a trait at all: that is where a - * codec 1 peer's single flat method byte lands. Kept in step with the Rust - * constant by `wire_table_ts_parity`. - **/ -export const MIN_TRAIT_ID = 193 as const; - /** The peer rejected an outbound frame because it does not support its API. **/ export class UnsupportedMessageError extends Error { /** Trait discriminant of the unsupported outbound frame. **/ diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts index 81e306a9f..c27173d87 100644 --- a/js/packages/truapi/src/wire-equality.test.ts +++ b/js/packages/truapi/src/wire-equality.test.ts @@ -40,12 +40,10 @@ function unwrap(result: Result, message: string): T { } describe("encodeWireMessage / decodeWireMessage wire equality", () => { - it("pins the handshake frame end-to-end: requestId + 0xc1 0x00 + payload", () => { - // Trait 193 = system, method 0 = handshake request. This locks the - // system trait to the first id above the codec 1 flat-method range: - // the handshake is the first frame either side sends, so its envelope - // must never drift, and a codec 1 peer's frame must never reach it. - expect(W.SYSTEM_HANDSHAKE.trait).toBe(193); + it("pins the handshake frame end-to-end: requestId + 0x01 0x00 + payload", () => { + // Trait 1 = system, method 0 = handshake request. The handshake is the + // first frame either side sends, so its envelope must never drift. + expect(W.SYSTEM_HANDSHAKE.trait).toBe(1); expect(W.SYSTEM_HANDSHAKE.method).toBe(0); const inner = new Uint8Array([0x00, 0x02]); // V1 variant + codec_version=2 @@ -60,17 +58,17 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }), "encode handshake_request", ); - // [0c 70 3a 31] "p:1" + [c1] system trait + [00] handshake request + payload. - expect(toHex(encoded)).toBe("0c703a31c1000002"); - expect(toHex(encoded)).toBe(toHex(expectedWire(193, 0, inner))); + // [0c 70 3a 31] "p:1" + [01] system trait + [00] handshake request + payload. + expect(toHex(encoded)).toBe("0c703a3101000002"); + expect(toHex(encoded)).toBe(toHex(expectedWire(1, 0, inner))); const decoded = unwrap(decodeWireMessage(encoded), "decode handshake_request"); - expect(decoded.payload.traitId).toBe(193); + expect(decoded.payload.traitId).toBe(1); expect(decoded.payload.methodId).toBe(0); expect(toHex(decoded.payload.value)).toBe(toHex(inner)); }); - it("encodes account_get_request (pair (194, 1)) to match the golden fixture", () => { + it("encodes account_get_request (pair (2, 1)) to match the golden fixture", () => { // Same vector as the Rust golden fixture // (`truapi-server/tests/snapshots/golden-account-get.bin`). Encoded // through the generated codec rather than assembled byte by byte: a @@ -100,10 +98,10 @@ describe("encodeWireMessage / decodeWireMessage wire equality", () => { }), "encode account_get_request", ); - expect(toHex(encoded)).toBe(toHex(expectedWire(194, 1, inner))); - // [0c 70 3a 31] "p:1" + [c2 01] pair + [00] V1 + [00] direction=Request + expect(toHex(encoded)).toBe(toHex(expectedWire(2, 1, inner))); + // [0c 70 3a 31] "p:1" + [02 01] pair + [00] V1 + [00] direction=Request // + [0c 66 6f 6f] "foo" + [00] DerivationIndex::Index + [00 00 00 00] u32 = 0. - expect(toHex(encoded)).toBe("0c703a31c20100000c666f6f0000000000"); + expect(toHex(encoded)).toBe("0c703a31020100000c666f6f0000000000"); }); it("round-trips a local_storage_read frame through encode + decode", () => { diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index a0ef1f503..c0e3109bd 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -268,25 +268,42 @@ mod tests { /// `preimage_submit`). #[test] fn collision_safe_when_two_traits_share_method_name() { + let mut statement_store_submit = make_request_method("submit", 62); + statement_store_submit.params[0].type_ref = TypeRef::Named { + name: "StatementStoreSubmitRequest".to_string(), + args: vec![], + }; + let mut preimage_submit = make_request_method("submit", 68); + preimage_submit.params[0].type_ref = TypeRef::Named { + name: "PreimageSubmitRequest".to_string(), + args: vec![], + }; let api = ApiDefinition { traits: vec![ TraitDef { name: "StatementStore".to_string(), module_path: Vec::new(), wire_trait_id: Some(193), - methods: vec![make_request_method("submit", 62)], + methods: vec![statement_store_submit], docs: None, }, TraitDef { name: "Preimage".to_string(), module_path: Vec::new(), wire_trait_id: Some(194), - methods: vec![make_request_method("submit", 68)], + methods: vec![preimage_submit], docs: None, }, ], public_trait_order: vec!["StatementStore".to_string(), "Preimage".to_string()], - types: versioned_request_test_types(), + types: { + let mut types = versioned_request_test_types(); + types.push(versioned_test_type("StatementStoreSubmitRequest")); + types.push(versioned_test_type("StatementStoreSubmitVersion")); + types.push(versioned_test_type("PreimageSubmitRequest")); + types.push(versioned_test_type("PreimageSubmitVersion")); + types + }, }; let dispatcher = generate_dispatcher(&api).expect("dispatcher"); @@ -359,16 +376,26 @@ mod tests { /// same API produces byte-identical output. #[test] fn idempotent_emission() { + let mut method = make_request_method("request_device_permission", 8); + method.params[0].type_ref = TypeRef::Named { + name: "RequestDevicePermissionRequest".to_string(), + args: vec![], + }; let api = ApiDefinition { traits: vec![TraitDef { name: "Permissions".to_string(), module_path: Vec::new(), wire_trait_id: Some(197), - methods: vec![make_request_method("request_device_permission", 8)], + methods: vec![method], docs: None, }], public_trait_order: vec!["Permissions".to_string()], - types: versioned_request_test_types(), + types: { + let mut types = versioned_request_test_types(); + types.push(versioned_test_type("RequestDevicePermissionRequest")); + types.push(versioned_test_type("RequestDevicePermissionVersion")); + types + }, }; let dispatcher_a = generate_dispatcher(&api).expect("dispatcher a"); @@ -527,32 +554,6 @@ mod tests { ); } - /// A trait id inside the range codec 1 could address must be refused. - /// Codec 2 reads that byte as the trait, so a low id would let a codec 1 - /// frame decode into a registered trait and execute the wrong method - /// instead of being reported as unroutable. - #[test] - fn wire_table_rejects_trait_id_below_the_codec_1_floor() { - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Permissions".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(MIN_TRAIT_ID - 1), - methods: vec![make_request_method("request_device_permission", 8)], - docs: None, - }], - public_trait_order: vec!["Permissions".to_string()], - types: vec![], - }; - - let err = generate_wire_table(&api).expect_err("a below-floor trait id must error"); - let msg = format!("{err}"); - assert!( - msg.contains("below the minimum"), - "unexpected error message: {msg}", - ); - } - /// The other half of the reservation: it must not have grown. A method id of /// 255 inside an ordinary trait is a legal address under a two-byte /// envelope, and refusing it would silently cost every trait its last slot. @@ -563,7 +564,7 @@ mod tests { traits: vec![TraitDef { name: "Example".to_string(), module_path: Vec::new(), - wire_trait_id: Some(MIN_TRAIT_ID), + wire_trait_id: Some(1), methods: vec![method], docs: None, }], @@ -571,7 +572,7 @@ mod tests { types: vec![], }; - generate_wire_table(&api).expect("(MIN_TRAIT_ID, 255) is an ordinary address"); + generate_wire_table(&api).expect("(1, 255) is an ordinary address"); } /// Pin `wire_const_name`'s `convert_case::Case::UpperSnake` behavior: @@ -695,110 +696,4 @@ mod tests { "unexpected error message: {msg}", ); } - - #[test] - fn dispatcher_versioned_request_with_raw_error_errors() { - let mut method = make_request_method("alpha", 10); - method.return_type = ReturnType::Result { - ok: TypeRef::Named { - name: "RespWrapper".to_string(), - args: vec![], - }, - err: TypeRef::Named { - name: "CallError".to_string(), - args: vec![TypeRef::Named { - name: "RawError".to_string(), - args: vec![], - }], - }, - }; - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Permissions".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(197), - methods: vec![method], - docs: None, - }], - public_trait_order: vec!["Permissions".to_string()], - types: vec![ - versioned_test_type("ReqWrapper"), - versioned_test_type("RespWrapper"), - ], - }; - - let err = generate_dispatcher(&api).expect_err("raw error wrapper must error"); - let msg = format!("{err}"); - assert!( - msg.contains("versioned request methods must use versioned errors"), - "unexpected error message: {msg}", - ); - } - - #[test] - fn dispatcher_raw_request_with_versioned_response_errors() { - let mut method = make_request_method("alpha", 10); - method.params[0].type_ref = TypeRef::Named { - name: "RawRequest".to_string(), - args: vec![], - }; - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Permissions".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(197), - methods: vec![method], - docs: None, - }], - public_trait_order: vec!["Permissions".to_string()], - types: vec![ - versioned_test_type("RespWrapper"), - versioned_test_type("ErrWrapper"), - ], - }; - - let err = generate_dispatcher(&api).expect_err("missing target version must error"); - let msg = format!("{err}"); - assert!( - msg.contains("versioned responses require a target version"), - "unexpected error message: {msg}", - ); - } - - #[test] - fn dispatcher_result_subscription_with_raw_error_errors() { - let mut method = make_subscription_method("alpha_subscribe", 20); - method.kind = MethodKind::ResultSubscription; - method.return_type = ReturnType::ResultSubscription { - item: TypeRef::Named { - name: "ItemWrapper".to_string(), - args: vec![], - }, - err: TypeRef::Named { - name: "CallError".to_string(), - args: vec![TypeRef::Named { - name: "RawError".to_string(), - args: vec![], - }], - }, - }; - let api = ApiDefinition { - traits: vec![TraitDef { - name: "Account".to_string(), - module_path: Vec::new(), - wire_trait_id: Some(192), - methods: vec![method], - docs: None, - }], - public_trait_order: vec!["Account".to_string()], - types: vec![versioned_test_type("ItemWrapper")], - }; - - let err = generate_dispatcher(&api).expect_err("raw result subscription error must error"); - let msg = format!("{err}"); - assert!( - msg.contains("result subscription methods must have an error wrapper"), - "unexpected error message: {msg}", - ); - } } diff --git a/rust/crates/truapi-codegen/src/rust/dispatcher.rs b/rust/crates/truapi-codegen/src/rust/dispatcher.rs index 8856346f3..583bf400b 100644 --- a/rust/crates/truapi-codegen/src/rust/dispatcher.rs +++ b/rust/crates/truapi-codegen/src/rust/dispatcher.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fmt::Write; -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use indoc::{formatdoc, indoc, writedoc}; use crate::rustdoc::*; @@ -45,26 +45,13 @@ pub fn generate_dispatcher(api: &ApiDefinition) -> Result { } let mut modules = Vec::with_capacity(traits.len()); - let mut uses_raw_err_payload = false; - let mut uses_raw_unit_ok_payload = false; - let mut uses_legacy_versioned_helpers = false; for trait_def in &traits { - let module = build_module(api, trait_def)?; - uses_raw_err_payload |= module.uses_raw_err_payload; - uses_raw_unit_ok_payload |= module.uses_raw_unit_ok_payload; - uses_legacy_versioned_helpers |= module.uses_legacy_versioned_helpers; - modules.push(module.code); + modules.push(build_module(api, trait_def)?); } let mut out = String::new(); write_header(&mut out); - write_imports( - &mut out, - &traits, - uses_raw_err_payload, - uses_raw_unit_ok_payload, - uses_legacy_versioned_helpers, - ); + write_imports(&mut out, &traits); writeln!(out).unwrap(); write_top_register(&mut out, &traits); write_host_initiated_callers(&mut out, api, &traits)?; @@ -98,15 +85,8 @@ fn order_traits(api: &ApiDefinition) -> Result> { Ok(ordered) } -struct ModuleEmission { - code: String, - uses_raw_err_payload: bool, - uses_raw_unit_ok_payload: bool, - uses_legacy_versioned_helpers: bool, -} - /// Emit the `register_{module}` function for a single trait. -fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result { +fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result { let module = module_for_trait(&trait_def.name); let mut methods = Vec::with_capacity(trait_def.methods.len()); @@ -124,14 +104,6 @@ fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result>>()? - .into_iter() - .any(|uses| uses); let fn_name = format!("register_{module}"); let trait_name = &trait_def.name; @@ -153,12 +125,7 @@ fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result { - let version_variant = single_variant(api, &version_type)?; - let request_variant = single_variant(api, request)?; - let envelope_path = format!("versioned::{module}::{version_type}"); - let bind = envelope_bind_name(request_variant); - let version_number: u8 = version_variant - .name - .strip_prefix('V') - .and_then(|n| n.parse().ok()) - .ok_or_else(|| { - anyhow::anyhow!( - "Host-initiated method `{}`: envelope variant `{}` is not named `V`", - method.name, - version_variant.name - ) - })?; - formatdoc! {r#" - let envelope = match request {{ - {request_pat} => {envelope_path}::{ev_name}(truapi::versioned::Subscription::Start({bind})), - }}; - subscriptions.start( - wire_table::{ids}, - {version_number}, - parity_scale_codec::Encode::encode(&envelope), - transport, - ) - "#, - request_pat = variant_expr(&request_path, request_variant, bind), - ev_name = version_variant.name, - } - } - None => formatdoc! {r#" - subscriptions.start( - wire_table::{ids}, - 1, - parity_scale_codec::Encode::encode(&request), - transport, + let version_type = envelope_type_name(Some(request), Some(item)).ok_or_else(|| { + anyhow::anyhow!( + "Host-initiated method `{}`: request/item wrapper name does not follow the \ + {{Base}}Request/{{Base}}Item convention, so no wire envelope can be derived \ + for it", + method.name + ) + })?; + let version_variant = single_variant(api, &version_type)?; + let request_variant = single_variant(api, request)?; + let envelope_path = format!("versioned::{module}::{version_type}"); + let bind = envelope_bind_name(request_variant); + let version_number: u8 = version_variant + .name + .strip_prefix('V') + .and_then(|n| n.parse().ok()) + .ok_or_else(|| { + anyhow::anyhow!( + "Host-initiated method `{}`: envelope variant `{}` is not named `V`", + method.name, + version_variant.name ) - "# - }, + })?; + let start_body = formatdoc! {r#" + let envelope = match request {{ + {request_pat} => {envelope_path}::{ev_name}(truapi::versioned::Subscription::Start({bind})), + }}; + subscriptions.start( + wire_table::{ids}, + {version_number}, + parity_scale_codec::Encode::encode(&envelope), + transport, + ) + "#, + request_pat = variant_expr(&request_path, request_variant, bind), + ev_name = version_variant.name, }; writedoc!( @@ -281,7 +243,11 @@ struct MethodEmission { #[derive(Clone)] enum WirePayload { Versioned(String), - Raw(TypeRef), + /// Not a recognized versioned wrapper: a method's param, or error type, + /// that doesn't follow the codec-2 authoring convention. The nested + /// envelope has no representable shape for this, so every path that + /// reaches it errors rather than falling back to a legacy encoding. + Raw, } impl MethodEmission { @@ -301,7 +267,7 @@ impl MethodEmission { { Some(WirePayload::Versioned(name.clone())) } - _ => Some(WirePayload::Raw(param.type_ref.clone())), + _ => Some(WirePayload::Raw), }, _ => bail!( "Method `{}`: expected at most one request parameter (got {})", @@ -313,7 +279,7 @@ impl MethodEmission { ReturnType::Result { err, .. } | ReturnType::ResultSubscription { err, .. } => { wire_payload_for_error(&method.name, err, &versioned_wrappers)? } - ReturnType::Subscription(_) => WirePayload::Raw(TypeRef::Unit), + ReturnType::Subscription(_) => WirePayload::Raw, }; let (response_wrapper, item_wrapper) = match &method.return_type { @@ -378,237 +344,35 @@ impl MethodEmission { } } - /// The merged `{Method}Version` wire-envelope type this method uses, if - /// one exists. Derived from the request or item wrapper's name (stripping - /// its `Request`/`Item` suffix, per the authoring convention every real - /// method follows); returns `None` when no such name can be derived - /// (synthetic/test methods whose wrapper names don't follow the - /// convention), in which case the method falls back to the legacy - /// (pre-nested-envelope) wire shape entirely unchanged. When a name *can* - /// be derived but doesn't resolve to a real single-version wrapper, that - /// is treated as an authoring bug and fails loudly instead of silently - /// falling back. - fn envelope<'a>(&self, api: &'a ApiDefinition) -> Result>> { + /// The merged `{Method}Version` wire-envelope type this method uses. + /// Derived from the request or item wrapper's name (stripping its + /// `Request`/`Item` suffix, per the authoring convention every real + /// method follows). Either failure mode here — a wrapper name outside + /// that convention, or a derived name that doesn't resolve to a real + /// single-version wrapper — means the method has no valid codec-2 + /// payload shape, so this errors instead of silently falling back to a + /// directionless payload. + fn envelope<'a>(&self, api: &'a ApiDefinition) -> Result> { let request_name = match &self.request_payload { Some(WirePayload::Versioned(name)) => Some(name.as_str()), _ => None, }; - let Some(type_name) = envelope_type_name(request_name, self.item_wrapper.as_deref()) else { - return Ok(None); - }; + let method = &self.name; + let type_name = + envelope_type_name(request_name, self.item_wrapper.as_deref()).ok_or_else(|| { + anyhow::anyhow!( + "Method `{method}`: request/item wrapper name does not follow the \ + {{Base}}Request/{{Base}}Item convention, so no wire envelope can be \ + derived for it" + ) + })?; let variant = single_variant(api, &type_name)?; - Ok(Some(EnvelopeInfo { type_name, variant })) - } - - fn uses_raw_err_payload(&self) -> bool { - matches!(self.request_payload, Some(WirePayload::Raw(_))) || self.uses_raw_unit_ok_payload() - } - - /// Whether this method's *legacy* (non-nested-envelope) codegen path - /// calls any of `encode_versioned_{ok,err,unit_ok,interrupt}_payload`. - /// Every real method resolves to the nested envelope (see [`Self::envelope`]), - /// so these helpers end up unused there; this lets the emitted `use`s stay - /// conditional on some method actually needing them. - fn uses_legacy_versioned_helpers(&self, api: &ApiDefinition) -> Result { - if self.envelope(api)?.is_some() { - return Ok(false); - } - Ok(match self.kind { - MethodKind::Request => { - self.response_wrapper.is_some() - || matches!(self.error_payload, WirePayload::Versioned(_)) - } - MethodKind::Subscription | MethodKind::ResultSubscription => { - matches!(self.error_payload, WirePayload::Versioned(_)) - } - }) - } - - fn uses_raw_unit_ok_payload(&self) -> bool { - matches!(self.kind, MethodKind::Request) - && self.response_wrapper.is_none() - && matches!(self.error_payload, WirePayload::Raw(_)) + Ok(EnvelopeInfo { type_name, variant }) } fn write_request(&self, out: &mut String, api: &ApiDefinition, host_expr: &str) -> Result<()> { - match self.envelope(api)? { - Some(env) => self.write_request_envelope(out, api, host_expr, &env), - None => self.write_request_legacy(out, host_expr), - } - } - - fn write_request_legacy(&self, out: &mut String, host_expr: &str) -> Result<()> { - let module = &self.module; - let method = &self.name; - let ids = const_name(&self.wire_name); - - writeln!(out, " {{").unwrap(); - self.write_execution_binding(out); - write_indented( - out, - 8, - &formatdoc! { - r#" - let host = {host_expr}; - dispatcher.on_request(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ - let host = host.clone(); - Box::pin(async move {{ - "# - }, - ); - let (call_args, target_version_expr) = match &self.request_payload { - Some(WirePayload::Versioned(request)) => { - let Some(error) = self.error_payload.versioned_name() else { - bail!("Method `{method}`: versioned request methods must use versioned errors"); - }; - write_indented( - out, - 16, - &formatdoc! { - r#" - let request: versioned::{module}::{request} = match Decode::decode(&mut &bytes[..]) {{ - Ok(request) => request, - Err(err) => {{ - let error: truapi::CallError = - truapi::CallError::MalformedFrame {{ reason: err.to_string() }}; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); - }} - }}; - let target_version = request.version(); - "# - }, - ); - ( - "&cx, request".to_string(), - Some("target_version".to_string()), - ) - } - Some(WirePayload::Raw(request)) => { - let request_ty = rust_type_ref(request).with_context(|| { - format!("Method `{method}`: raw request type cannot be emitted") - })?; - let error_ty = self - .error_payload - .rust_error_type(module) - .with_context(|| { - format!("Method `{method}`: raw request methods must have error type") - })?; - write_indented( - out, - 16, - &formatdoc! { - r#" - let request: {request_ty} = match Decode::decode(&mut &bytes[..]) {{ - Ok(request) => request, - Err(err) => {{ - let error: truapi::CallError<{error_ty}> = - truapi::CallError::MalformedFrame {{ reason: err.to_string() }}; - return Ok(encode_raw_err_payload(error)); - }} - }}; - "# - }, - ); - ("&cx, request".to_string(), None) - } - None => { - writeln!(out, " let _ = bytes;").unwrap(); - let target = self - .error_payload - .versioned_name() - .map(|error| format!("::LATEST")); - ("&cx".to_string(), target) - } - }; - writeln!( - out, - " let cx = CallContext::with_request_id(request_id.clone());" - ) - .unwrap(); - self.write_request_execution_check(out, target_version_expr.as_deref())?; - match &self.response_wrapper { - Some(response) => { - let Some(target_version_expr) = target_version_expr.as_deref() else { - bail!("Method `{method}`: versioned responses require a target version"); - }; - write_indented( - out, - 16, - &formatdoc! { - r#" - let response: versioned::{module}::{response} = match host.{method}({call_args}).await {{ - Ok(value) => value, - Err(err) => {{ - return Ok(encode_versioned_err_payload( - downgrade_call_error(err, {target_version_expr}), - {target_version_expr}, - )); - }} - }}; - // Downgraded to the caller's version: a handler answers in - // latest terms, and a peer that asked in an older version - // cannot decode a newer variant. - Ok(encode_versioned_ok_payload( - ::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - {target_version_expr}, - ), - )) - "# - }, - ); - } - None => match (&self.error_payload, target_version_expr.as_deref()) { - (WirePayload::Versioned(_), Some(target_version_expr)) => { - write_indented( - out, - 16, - &formatdoc! { - r#" - match host.{method}({call_args}).await {{ - Ok(()) => Ok(encode_versioned_unit_ok_payload({target_version_expr})), - Err(err) => {{ - Ok(encode_versioned_err_payload(err, {target_version_expr})) - }} - }} - "# - }, - ); - } - (WirePayload::Raw(_), _) => { - write_indented( - out, - 16, - &formatdoc! { - r#" - match host.{method}({call_args}).await {{ - Ok(()) => Ok(encode_raw_unit_ok_payload()), - Err(err) => Ok(encode_raw_err_payload(err)), - }} - "# - }, - ); - } - (WirePayload::Versioned(_), None) => { - bail!("Method `{method}`: versioned unit responses require a target version") - } - }, - } - write_indented( - out, - 4, - indoc! { - r#" - }) - }); - } - "# - }, - ); - Ok(()) + let env = self.envelope(api)?; + self.write_request_envelope(out, api, host_expr, &env) } fn write_subscription( @@ -617,169 +381,10 @@ impl MethodEmission { api: &ApiDefinition, host_expr: &str, ) -> Result<()> { - match self.envelope(api)? { - Some(env) => self.write_subscription_envelope(out, api, host_expr, &env), - None => self.write_subscription_legacy(out, host_expr), - } - } - - fn write_subscription_legacy(&self, out: &mut String, host_expr: &str) -> Result<()> { - let module = &self.module; - let method = &self.name; - let ids = const_name(&self.wire_name); - let Some(item) = self.item_wrapper.as_deref() else { - bail!("Method `{method}`: subscription methods must have an item wrapper"); - }; - let error = self.error_payload.versioned_name(); - - let is_result_sub = matches!(self.kind, MethodKind::ResultSubscription); - - writeln!(out, " {{").unwrap(); - self.write_execution_binding(out); - write_indented( - out, - 8, - &formatdoc! { - r#" - let host = {host_expr}; - dispatcher.on_subscription(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ - let host = host.clone(); - Box::pin(async move {{ - "# - }, - ); - let (call_args, target_version_expr) = if let Some(WirePayload::Versioned(request)) = - &self.request_payload - { - let decode_error = match error { - Some(error) => { - let block = formatdoc! { - r#" - Err(err) => {{ - let error: truapi::CallError = - truapi::CallError::MalformedFrame {{ - reason: err.to_string(), - }}; - return Err(encode_versioned_interrupt_payload( - error, - ::LATEST, - )); - }} - "# - }; - block - .lines() - .map(|line| format!(" {line}")) - .collect::>() - .join("\n") - } - None => " Err(_) => return Err(Vec::new()),".to_string(), - }; - write_indented( - out, - 16, - &formatdoc! { - r#" - let request: versioned::{module}::{request} = match Decode::decode(&mut &bytes[..]) {{ - Ok(request) => request, - {decode_error} - }}; - "# - }, - ); - if is_result_sub { - writeln!( - out, - " let target_version = request.version();" - ) - .unwrap(); - } - ("&cx, request".to_string(), "target_version".to_string()) - } else { - writeln!(out, " let _ = bytes;").unwrap(); - let target_version = error - .map(|error| format!("::LATEST")) - .unwrap_or_else(|| "1".to_string()); - ("&cx".to_string(), target_version) - }; - writeln!( - out, - " let cx = CallContext::with_request_id(request_id.clone());" - ) - .unwrap(); - if self.required_execution.is_some() && is_result_sub { - let error = error.expect("result subscription error checked above"); - write_indented( - out, - 16, - &formatdoc! { - r#" - if !execution_allowed {{ - let error: truapi::CallError = - truapi::CallError::Denied; - return Err(encode_versioned_interrupt_payload(error, {target_version_expr})); - }} - "# - }, - ); - } else if self.required_execution.is_some() { - writeln!( - out, - " if !execution_allowed {{ return Err(Vec::new()); }}" - ) - .unwrap(); - } - if is_result_sub { - if error.is_none() { - bail!("Method `{method}`: result subscription methods must have an error wrapper"); - } - write_indented( - out, - 16, - &formatdoc! { - r#" - let stream = match host.{method}({call_args}).await {{ - Ok(sub) => sub, - Err(err) => {{ - return Err(encode_versioned_interrupt_payload(err, {target_version_expr})); - }} - }}; - "# - }, - ); - } else { - writeln!( - out, - " let stream = host.{method}({call_args}).await;" - ) - .unwrap(); - } - writeln!( - out, - " Ok(({target_version_expr}, subscription_stream::(stream)))" - ) - .unwrap(); - write_indented( - out, - 4, - indoc! { - r#" - }) - }); - } - "# - }, - ); - Ok(()) + let env = self.envelope(api)?; + self.write_subscription_envelope(out, api, host_expr, &env) } - /// Generates a request/response handler that decodes and encodes through - /// the nested wire envelope: incoming bytes are the merged - /// `{Method}Version` type, matched for the `Request` direction; outgoing - /// bytes are constructed as that same type's `Response` direction, - /// wrapping `Result>`. The trait method itself is - /// unchanged — it still takes/returns the original bare versioned - /// request/response/error types; only the wire shape nests differently. fn write_request_envelope( &self, out: &mut String, @@ -1149,51 +754,6 @@ impl MethodEmission { .unwrap(); } } - - fn write_request_execution_check( - &self, - out: &mut String, - target_version_expr: Option<&str>, - ) -> Result<()> { - if self.required_execution.is_none() { - return Ok(()); - } - let module = &self.module; - match (&self.error_payload, target_version_expr) { - (WirePayload::Versioned(error), Some(target)) => write_indented( - out, - 16, - &formatdoc! { - r#" - if !execution_allowed {{ - let error: truapi::CallError = - truapi::CallError::Denied; - return Ok(encode_versioned_err_payload(error, {target})); - }} - "# - }, - ), - (WirePayload::Raw(error), _) => { - let error = rust_type_ref(error)?; - write_indented( - out, - 16, - &formatdoc! { - r#" - if !execution_allowed {{ - let error: truapi::CallError<{error}> = truapi::CallError::Denied; - return Ok(encode_raw_err_payload(error)); - }} - "# - }, - ); - } - (WirePayload::Versioned(_), None) => { - bail!("execution-filtered request has no target wire version") - } - } - Ok(()) - } } /// The merged wire-envelope type this method's frames nest into @@ -1208,8 +768,8 @@ struct EnvelopeInfo<'a> { /// method's request or item wrapper name, stripping its `Request`/`Item` /// suffix — the naming convention every hand-authored envelope type follows. /// Returns `None` when neither name is present or neither ends in the -/// expected suffix (synthetic/test methods opt out of the nested envelope -/// entirely this way, falling back to the legacy wire shape). +/// expected suffix; every real method's wrapper follows the convention, so +/// callers turn `None` into a hard codegen error instead of falling back. fn envelope_type_name(request: Option<&str>, item: Option<&str>) -> Option { if let Some(base) = request.and_then(|name| name.strip_suffix("Request")) { return Some(format!("{base}Version")); @@ -1321,14 +881,7 @@ impl WirePayload { fn versioned_name(&self) -> Option<&str> { match self { Self::Versioned(name) => Some(name), - Self::Raw(_) => None, - } - } - - fn rust_error_type(&self, module: &str) -> Result { - match self { - Self::Versioned(name) => Ok(format!("versioned::{module}::{name}")), - Self::Raw(ty) => rust_type_ref(ty), + Self::Raw => None, } } } @@ -1347,7 +900,7 @@ fn wire_payload_for_error( if matches!(inner, TypeRef::Unit) { bail!("Method `{method}`: error type cannot be unit") } - Ok(WirePayload::Raw(inner.clone())) + Ok(WirePayload::Raw) } } } @@ -1479,13 +1032,7 @@ fn write_header(out: &mut String) { .unwrap(); } -fn write_imports( - out: &mut String, - traits: &[&TraitDef], - uses_raw_err_payload: bool, - uses_raw_unit_ok_payload: bool, - uses_legacy_versioned_helpers: bool, -) { +fn write_imports(out: &mut String, traits: &[&TraitDef]) { writedoc!( out, r#" @@ -1516,29 +1063,6 @@ fn write_imports( "# ) .unwrap(); - if uses_raw_err_payload { - writeln!(out, "use crate::frame::encode_raw_err_payload;").unwrap(); - } - if uses_raw_unit_ok_payload { - writeln!(out, "use crate::frame::encode_raw_unit_ok_payload;").unwrap(); - } - // Every real method resolves to the nested wire envelope (see - // `MethodEmission::envelope`), which encodes by constructing and - // encoding its envelope value directly rather than through these - // helpers. They remain for the legacy (non-nested-envelope) codegen - // path, imported only when some method actually falls back to it. - if uses_legacy_versioned_helpers { - writedoc!( - out, - r#" - use crate::frame::encode_versioned_err_payload; - use crate::frame::encode_versioned_interrupt_payload; - use crate::frame::encode_versioned_ok_payload; - use crate::frame::encode_versioned_unit_ok_payload; - "# - ) - .unwrap(); - } } fn write_top_register(out: &mut String, traits: &[&TraitDef]) { diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs index 6798de788..a6a4aeea0 100644 --- a/rust/crates/truapi-codegen/src/rust/wire_table.rs +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -101,27 +101,16 @@ pub fn generate_wire_table(api: &ApiDefinition) -> Result { } /// The trait's wire discriminant. Every API trait must carry a -/// `#[wire_trait(id = N)]` annotation whose id is at least -/// [`MIN_TRAIT_ID`] and is not 255, which is reserved for protocol errors +/// `#[wire_trait(id = N)]` annotation; 255 is reserved for protocol errors /// (that one is caught as a collision against the seeded reservation, not /// here). fn trait_wire_id(trait_def: &TraitDef) -> Result { - let id = trait_def.wire_trait_id.ok_or_else(|| { + trait_def.wire_trait_id.ok_or_else(|| { anyhow::anyhow!( "trait `{}` is missing #[wire_trait(id = N)] annotation", trait_def.name ) - })?; - if id < MIN_TRAIT_ID { - bail!( - "trait `{}` has wire trait id {id}, below the minimum {MIN_TRAIT_ID}: \ - ids under {MIN_TRAIT_ID} are reserved so that a codec 1 frame, whose \ - single flat method byte never exceeded {MAX_CODEC_1_METHOD_ID}, can \ - never be mistaken for a codec 2 trait", - trait_def.name - ); - } - Ok(id) + }) } fn method_entry(trait_def: &TraitDef, trait_id: u8, method: &MethodDef) -> Result { diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index ab6453ce2..1c23d87a1 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -6,8 +6,6 @@ use std::collections::{BTreeMap, HashMap}; use anyhow::{Context, Result, bail}; use serde::Deserialize; -pub use truapi::{MAX_CODEC_1_METHOD_ID, MIN_TRAIT_ID}; - /// Minimum rustdoc JSON `format_version` the extractors are tested against. /// Emitted by nightly 2026-02-23 (rustc 1.95.0-nightly); older formats may /// encode item shapes differently and are rejected outright. diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index 24bf5ca0a..73b0f404d 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -391,7 +391,8 @@ fn versioned_wrapper_for<'a>( /// `Request`/`Item` suffix off the request (or subscription item) wrapper's /// own name and append `Version`. Mirrors `envelope_type_name` in /// `rust/dispatcher.rs` so both languages name the same Rust type the same -/// way. +/// way. `None` only for a request/item wrapper whose name doesn't follow the +/// `{Base}Request`/`{Base}Item` convention; every real method's wrapper does. fn envelope_type_name(request: Option<&str>, item: Option<&str>) -> Option { if let Some(base) = request.and_then(|name| name.strip_suffix("Request")) { return Some(format!("{base}Version")); @@ -407,24 +408,70 @@ fn find_type<'a>(api: &'a ApiDefinition, name: &str) -> Option<&'a TypeDef> { api.types.iter().find(|type_def| type_def.name == name) } -/// Resolve a method's merged wire-envelope type name, if one was extracted -/// for it. `None` when the method's request/item wrapper doesn't follow the -/// `{Base}Request`/`{Base}Item` naming convention (synthetic test fixtures -/// only; every real method resolves), or when the derived name isn't -/// actually present in the extracted API (an authoring bug: the wrapper -/// exists but its merged envelope type doesn't). +/// Resolve a method's merged wire-envelope type name. Every codec-2 method +/// has one: its request (or subscription item) wrapper follows the +/// `{Base}Request`/`{Base}Item` convention and codegen always emits the +/// matching `{Base}Version` type alongside it. Either failure here — a +/// wrapper name outside that convention, or a derived name absent from the +/// extracted API — means the method has no valid codec-2 payload shape, so +/// this errors instead of silently falling back to a directionless payload. fn method_envelope_name( api: &ApiDefinition, request_wrapper: Option<&str>, item_wrapper: Option<&str>, -) -> Result> { - let Some(name) = envelope_type_name(request_wrapper, item_wrapper) else { - return Ok(None); - }; +) -> Result { + let name = envelope_type_name(request_wrapper, item_wrapper).ok_or_else(|| { + anyhow::anyhow!( + "wrapper name `{:?}`/`{:?}` does not follow the {{Base}}Request/{{Base}}Item \ + convention, so no wire envelope can be derived for it", + request_wrapper, + item_wrapper + ) + })?; if find_type(api, &name).is_none() { - return Ok(None); + bail!("derived wire envelope `{name}` is not present in the extracted API"); } - Ok(Some(name)) + Ok(name) +} + +/// Extracts a subscription envelope's declared `Err` type: the third generic +/// argument of its `Subscription` payload for the given +/// wire version. Every subscription's envelope carries one, even a plain +/// (non-`ResultSubscription`) method — its own `Err` is `CallError` +/// rather than a domain-specific error, but the framework-level `CallError` +/// still rides along on every interrupt frame and must be decodable. +fn subscription_envelope_err_ty<'a>( + api: &'a ApiDefinition, + envelope_name: &str, + version: u32, +) -> Result<&'a TypeRef> { + let envelope = find_type(api, envelope_name) + .ok_or_else(|| anyhow::anyhow!("envelope `{envelope_name}` not found in extracted API"))?; + let TypeDefKind::Enum(variants) = &envelope.kind else { + bail!("envelope `{envelope_name}` is not an enum"); + }; + let variant_name = format!("V{version}"); + let variant = variants + .iter() + .find(|v| v.name == variant_name) + .ok_or_else(|| { + anyhow::anyhow!("envelope `{envelope_name}` has no `{variant_name}` variant") + })?; + let VariantFields::Unnamed(fields) = &variant.fields else { + bail!("envelope `{envelope_name}` variant `{variant_name}` is not a tuple variant"); + }; + let [TypeRef::Named { name, args }] = fields.as_slice() else { + bail!( + "envelope `{envelope_name}` variant `{variant_name}` does not wrap a single named type" + ); + }; + if name != "Subscription" || args.len() != 3 { + bail!( + "envelope `{envelope_name}` variant `{variant_name}` does not wrap \ + Subscription" + ); + } + Ok(&args[2]) } /// Emits a JSDoc block for `docs` at the given indent. No-op when `docs` is @@ -650,27 +697,16 @@ fn generate_wire_table(api: &ApiDefinition, target_version: u32) -> Result Result { - let id = trait_def.wire_trait_id.ok_or_else(|| { + trait_def.wire_trait_id.ok_or_else(|| { anyhow::anyhow!( "trait `{}` is missing #[wire_trait(id = N)] annotation", trait_def.name ) - })?; - if id < MIN_TRAIT_ID { - bail!( - "trait `{}` has wire trait id {id}, below the minimum {MIN_TRAIT_ID}: \ - ids under {MIN_TRAIT_ID} are reserved so that a codec 1 frame, whose \ - single flat method byte never exceeded {MAX_CODEC_1_METHOD_ID}, can \ - never be mistaken for a codec 2 trait", - trait_def.name - ); - } - Ok(id) + }) } fn method_is_included( @@ -906,11 +942,14 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) }} // Interrupt payload sent when a host-initiated render arrives with no - // registered handler, declining the start: `[version=V1, direction= - // Interrupt, Option::None]`. The host only inspects the direction - // byte for this flow, so this fixed frame (matching a clean, - // error-free interrupt) is valid for every method. - const HOST_INITIATED_DECLINE_PAYLOAD = new Uint8Array([0, 2, 0]); + // registered handler, declining the start: version=V1, direction= + // Interrupt, Some(CallError::HostFailure with reason "unavailable"). + // HostFailure's payload doesn't depend on the method's own domain + // error type, so this fixed frame is valid for every method's + // envelope regardless of what D in CallError decodes to. + const HOST_INITIATED_DECLINE_PAYLOAD = new Uint8Array([ + 0, 2, 1, 4, 44, 117, 110, 97, 118, 97, 105, 108, 97, 98, 108, 101, + ]); // Items buffered per host-initiated stream while the product's handler // observable has no subscriber yet. const HOST_INITIATED_BUFFER_CAPACITY = 64; @@ -920,7 +959,6 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) .unwrap(); write_observable_helper(&mut out); - let ctx = codec_context(&[]); let wrappers = collect_versioned_wrappers(api); let services = public_services(api)?; @@ -939,7 +977,7 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) .copied() .filter(|method| method.wire.host_initiated) { - emit_host_initiated_field(&mut out, method, &wrappers, &ctx, target_version)?; + emit_host_initiated_field(&mut out, method, &wrappers, target_version)?; } writeln!( out, @@ -957,22 +995,13 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) trait_def, method, &wrappers, - &ctx, target_version, )?; } writeln!(out, " }}\n").unwrap(); for method in methods { - emit_method( - &mut out, - api, - trait_def, - method, - &wrappers, - &ctx, - target_version, - )?; + emit_method(&mut out, api, trait_def, method, &wrappers, target_version)?; writeln!(out).unwrap(); } @@ -1184,24 +1213,10 @@ fn included_methods<'a>( .collect() } -fn write_payload_field( - out: &mut String, - indent: &str, - codec_expr: &str, - wire_version: Option, - value_expr: &str, -) { - let arg = match wire_version { - Some(version) => format!("{{ tag: \"V{version}\", value: {value_expr} }}"), - None => value_expr.to_string(), - }; - writeln!(out, "{indent}payload: {codec_expr}.enc({arg}),").unwrap(); -} - -/// Lowered method payload: the TS param list, the inner value expression, and -/// the wire codec/version used by the generated client to produce payload -/// bytes. The public method signature stays ergonomic (inner version types), -/// while the generated client owns versioned wrapper encoding. +/// Lowered method payload: the TS param list and the inner value expression +/// the generated client wraps in the method's envelope. The public method +/// signature stays ergonomic (inner version types), while the generated +/// client owns versioned wrapper encoding. struct PayloadEmission { /// Comma-separated `name: Type` entries used as the body of the user-facing /// object argument type. Empty when the method takes no input. @@ -1211,14 +1226,11 @@ struct PayloadEmission { param_names: Vec, inner_type_ts: String, value_expr: String, - wire_codec_expr: String, - wire_version: Option, } fn emit_payload( params: &[ParamDef], wrappers: &HashMap, - ctx: &CodecContext, wire_version: Option, ) -> Result { // The unified contract always takes a single versioned-wrapper arg. On the @@ -1240,8 +1252,6 @@ fn emit_payload( param_names: Vec::new(), inner_type_ts: "undefined".to_string(), value_expr: "undefined".to_string(), - wire_codec_expr: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), - wire_version: Some(wrapper.version), }), VersionedKind::Tuple(inner) => { let inner_ts = ts_type_qualified(inner)?; @@ -1250,27 +1260,17 @@ fn emit_payload( param_names: vec!["request".to_string()], inner_type_ts: inner_ts, value_expr: "request".to_string(), - wire_codec_expr: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), - wire_version: Some(wrapper.version), }) } }; } if params.is_empty() { - // No-param methods (subscribe-with-no-start-payload, etc.) still need - // a versioned envelope on the wire so legacy hosts that decode an - // `Enum({v1: _void})` payload receive at least the version byte. - let version = wire_version.unwrap_or(1); - let wire_codec_expr = - indexed_versioned_codec_expr(std::iter::once((version, "S._void".to_string())))?; return Ok(PayloadEmission { param_list: String::new(), param_names: Vec::new(), inner_type_ts: "undefined".to_string(), value_expr: "undefined".to_string(), - wire_codec_expr, - wire_version: Some(version), }); } @@ -1280,8 +1280,6 @@ fn emit_payload( param_names: vec!["request".to_string()], inner_type_ts: inner_type_ts.clone(), value_expr: "request".to_string(), - wire_codec_expr: method_payload_codec_expr(params, true, ctx)?, - wire_version: None, }) } @@ -1291,9 +1289,6 @@ fn emit_payload( #[derive(Clone)] struct ResponseEmission { inner_type_ts: String, - wire_type_ts: String, - wire_codec_expr: String, - inner_codec_expr: String, /// `Some(version)` when `inner_type_ts` is a domain error's own versioned /// wrapper (`S.CallErrorValue`) rather than its bare /// type. The merged wire envelope (RFC 0028) always carries the bare @@ -1303,27 +1298,9 @@ struct ResponseEmission { wrap_version: Option, } -fn versioned_value_cast(wire_type: &str, inner_type: &str, version: u32) -> String { - format!("{{ tag: \"V{version}\"; value: {inner_type} }} & {wire_type}") -} - -fn versioned_value_expr( - value_expr: &str, - wire_type: &str, - inner_type: &str, - version: u32, -) -> String { - format!( - "({} as {}).value", - value_expr, - versioned_value_cast(wire_type, inner_type, version) - ) -} - fn emit_response( ty: &TypeRef, wrappers: &HashMap, - ctx: &CodecContext, wire_version: Option, ) -> Result { if let Some((wrapper_name, wrapper)) = versioned_wrapper_for(ty, wrappers) { @@ -1336,16 +1313,10 @@ fn emit_response( return match &wrapper.kind { VersionedKind::Unit => Ok(ResponseEmission { inner_type_ts: "undefined".to_string(), - wire_type_ts: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), - wire_codec_expr: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), - inner_codec_expr: "S._void".to_string(), wrap_version: None, }), VersionedKind::Tuple(inner) => Ok(ResponseEmission { inner_type_ts: ts_type_qualified(inner)?, - wire_type_ts: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), - wire_codec_expr: format!("T.{}", versioned_wrapper_ts_name(wrapper_name)), - inner_codec_expr: codec_expr(inner, true, ctx)?, wrap_version: None, }), }; @@ -1353,9 +1324,6 @@ fn emit_response( Ok(ResponseEmission { inner_type_ts: ts_type_qualified(ty)?, - wire_type_ts: ts_type_qualified(ty)?, - wire_codec_expr: codec_expr(ty, true, ctx)?, - inner_codec_expr: codec_expr(ty, true, ctx)?, wrap_version: None, }) } @@ -1363,11 +1331,10 @@ fn emit_response( fn emit_error_response( ty: &TypeRef, wrappers: &HashMap, - ctx: &CodecContext, wire_version: Option, ) -> Result { let Some(error_wrapper_ty) = call_error_inner(ty) else { - return emit_response(ty, wrappers, ctx, wire_version); + return emit_response(ty, wrappers, wire_version); }; if let Some((wrapper_name, _wrapper)) = versioned_wrapper_for(error_wrapper_ty, wrappers) { @@ -1376,13 +1343,8 @@ fn emit_error_response( })?; let versioned_name = versioned_wrapper_ts_name(wrapper_name); let inner_type_ts = format!("S.CallErrorValue"); - let inner_codec_expr = format!("S.CallError(T.{versioned_name})"); - let wire_codec_expr = indexed_versioned_codec_expr([(version, inner_codec_expr.clone())])?; return Ok(ResponseEmission { - inner_type_ts: inner_type_ts.clone(), - wire_type_ts: format!("{{ tag: \"V{version}\"; value: {inner_type_ts} }}"), - wire_codec_expr, - inner_codec_expr, + inner_type_ts, wrap_version: Some(version), }); } @@ -1391,15 +1353,8 @@ fn emit_error_response( "S.CallErrorValue<{}>", ts_type_qualified_preserve(error_wrapper_ty)? ); - let inner_codec_expr = format!( - "S.CallError({})", - codec_expr_mode(error_wrapper_ty, true, ctx, NameMode::PreserveQualified)? - ); Ok(ResponseEmission { - inner_type_ts: inner_type_ts.clone(), - wire_type_ts: inner_type_ts, - wire_codec_expr: inner_codec_expr.clone(), - inner_codec_expr, + inner_type_ts, wrap_version: None, }) } @@ -1435,10 +1390,6 @@ fn indexed_versioned_codec_expr( )) } -fn versioned_result_codec_expr(version: u32, ok_codec: &str, err_codec: &str) -> Result { - indexed_versioned_codec_expr([(version, format!("S.Result({ok_codec}, {err_codec})"))]) -} - /// The request wrapper name for a method's single param, if its param shape /// is a recognized versioned wrapper (payloadless and multi/raw-param /// methods have no such wrapper to name). @@ -1458,24 +1409,23 @@ fn emit_method( trait_def: &TraitDef, method: &MethodDef, wrappers: &HashMap, - ctx: &CodecContext, target_version: u32, ) -> Result<()> { let ts_method_name = to_camel_case(&strip_prefix(&method.name)); let wire_const = wire_const_name(&trait_def.name, &method.name); let wire_version = method_wire_version(method, wrappers, target_version)?; - let payload = emit_payload(&method.params, wrappers, ctx, wire_version)?; + let payload = emit_payload(&method.params, wrappers, wire_version)?; write_jsdoc(out, " ", method.docs.as_deref()); if method.wire.host_initiated { - return emit_host_initiated_method(out, method, &payload, wrappers, ctx, wire_version); + return emit_host_initiated_method(out, method, &payload, wrappers, wire_version); } match (&method.kind, &method.return_type) { (MethodKind::Request, ReturnType::Result { ok, err }) => { let is_handshake = trait_def.name == "System" && method.name == "handshake"; - let response = emit_response(ok, wrappers, ctx, wire_version)?; - let error = emit_error_response(err, wrappers, ctx, wire_version)?; + let response = emit_response(ok, wrappers, wire_version)?; + let error = emit_error_response(err, wrappers, wire_version)?; let envelope = method_envelope_name(api, request_wrapper_name(method, wrappers), None)?; let arg_decl = if is_handshake || payload.param_list.is_empty() { @@ -1489,87 +1439,51 @@ fn emit_method( payload.value_expr.clone() }; + let version = wire_version.ok_or_else(|| { + anyhow::anyhow!( + "method `{}` resolved wire envelope `{envelope}` with no selected wire version", + method.name + ) + })?; + let method_name = &method.name; + writedoc!( out, " {ts_method_name}({arg_decl}): ResultAsync<{ok_type}, {err_type}> {{ return this.transport.request<{ok_type}, {err_type}>({{ ids: W.{wire_const}, + payload: T.{envelope}.enc({{ tag: \"V{version}\", value: {{ tag: \"Request\", value: {request_expr} }} }}), + decodeResponse: (payload) => {{ + const envelope = T.{envelope}.dec(payload); + if (envelope.value.tag !== \"Response\") {{ + throw new Error(`{method_name}: expected Response direction, got ${{envelope.value.tag}}`); + }} + const result = envelope.value.value; ", ok_type = response.inner_type_ts, err_type = error.inner_type_ts ) .unwrap(); - - match envelope { - Some(envelope_name) => { - let version = wire_version.ok_or_else(|| { - anyhow::anyhow!( - "method `{}` resolved wire envelope `{envelope_name}` with no selected wire version", - method.name - ) - })?; - let method_name = &method.name; - writedoc!( - out, - " - payload: T.{envelope_name}.enc({{ tag: \"V{version}\", value: {{ tag: \"Request\", value: {request_expr} }} }}), - decodeResponse: (payload) => {{ - const envelope = T.{envelope_name}.dec(payload); - if (envelope.value.tag !== \"Response\") {{ - throw new Error(`{method_name}: expected Response direction, got ${{envelope.value.tag}}`); - }} - const result = envelope.value.value; - " - ) - .unwrap(); - match error.wrap_version { - Some(err_version) => writedoc!( - out, - " - if (!result.success) {{ - return {{ - success: false, - value: result.value.tag === \"Domain\" - ? {{ tag: \"Domain\" as const, value: {{ tag: \"V{err_version}\", value: result.value.value }} }} - : result.value, - }}; - }} - return result; - " - ) - .unwrap(), - None => writeln!(out, " return result;").unwrap(), - } - writeln!(out, " }},").unwrap(); - } - None => { - let response_codec = match wire_version { - Some(version) => versioned_result_codec_expr( - version, - &response.inner_codec_expr, - &error.inner_codec_expr, - )?, - None => format!( - "S.Result({}, {})", - response.wire_codec_expr, error.wire_codec_expr - ), - }; - write_payload_field( - out, - " ", - &payload.wire_codec_expr, - payload.wire_version, - &request_expr, - ); - let value_suffix = if wire_version.is_some() { ".value" } else { "" }; - writeln!( - out, - " decodeResponse: (payload) => {response_codec}.dec(payload){value_suffix}," - ) - .unwrap(); - } + match error.wrap_version { + Some(err_version) => writedoc!( + out, + " + if (!result.success) {{ + return {{ + success: false, + value: result.value.tag === \"Domain\" + ? {{ tag: \"Domain\" as const, value: {{ tag: \"V{err_version}\", value: result.value.value }} }} + : result.value, + }}; + }} + return result; + " + ) + .unwrap(), + None => writeln!(out, " return result;").unwrap(), } + writeln!(out, " }},").unwrap(); writedoc!( out, @@ -1581,27 +1495,38 @@ fn emit_method( .unwrap(); } (MethodKind::Subscription, ReturnType::Subscription(ty)) => { - let response = emit_response(ty, wrappers, ctx, wire_version)?; + let response = emit_response(ty, wrappers, wire_version)?; let envelope = method_envelope_name( api, request_wrapper_name(method, wrappers), versioned_wrapper_for(ty, wrappers).map(|(name, _)| name), )?; + // A plain subscription has no domain error type, but its envelope's + // Err is still CallError: the framework-level failure + // modes (Denied, Unsupported, MalformedFrame, HostFailure) can + // interrupt any subscription regardless of what it declares. + let version = wire_version.ok_or_else(|| { + anyhow::anyhow!( + "method `{}` resolved wire envelope `{envelope}` with no selected wire version", + method.name + ) + })?; + let err_ty = subscription_envelope_err_ty(api, &envelope, version)?; + let error = emit_error_response(err_ty, wrappers, wire_version)?; emit_subscribe_method( out, &ts_method_name, &wire_const, &payload, - &response, response.inner_type_ts.clone(), - None, + error, wire_version, envelope, )?; } (MethodKind::ResultSubscription, ReturnType::ResultSubscription { item, err }) => { - let response = emit_response(item, wrappers, ctx, wire_version)?; - let error = emit_error_response(err, wrappers, ctx, wire_version)?; + let response = emit_response(item, wrappers, wire_version)?; + let error = emit_error_response(err, wrappers, wire_version)?; let envelope = method_envelope_name( api, request_wrapper_name(method, wrappers), @@ -1612,9 +1537,8 @@ fn emit_method( &ts_method_name, &wire_const, &payload, - &response, response.inner_type_ts.clone(), - Some(error), + error, wire_version, envelope, )?; @@ -1639,20 +1563,19 @@ fn host_registration_field(method: &MethodDef) -> String { fn emit_host_initiated_types( method: &MethodDef, wrappers: &HashMap, - ctx: &CodecContext, target_version: u32, ) -> Result<(PayloadEmission, ResponseEmission, u32)> { let wire_version = method_wire_version(method, wrappers, target_version)?.ok_or_else(|| { anyhow::anyhow!("host-initiated method `{}` is not versioned", method.name) })?; - let payload = emit_payload(&method.params, wrappers, ctx, Some(wire_version))?; + let payload = emit_payload(&method.params, wrappers, Some(wire_version))?; let ReturnType::Subscription(item) = &method.return_type else { bail!( "host-initiated method `{}` must return Subscription", method.name ); }; - let response = emit_response(item, wrappers, ctx, Some(wire_version))?; + let response = emit_response(item, wrappers, Some(wire_version))?; Ok((payload, response, wire_version)) } @@ -1660,10 +1583,9 @@ fn emit_host_initiated_field( out: &mut String, method: &MethodDef, wrappers: &HashMap, - ctx: &CodecContext, target_version: u32, ) -> Result<()> { - let (payload, response, _) = emit_host_initiated_types(method, wrappers, ctx, target_version)?; + let (payload, response, _) = emit_host_initiated_types(method, wrappers, target_version)?; writeln!( out, " private readonly {}: HostInitiatedSubscriptionRegistration<{}, {}>;", @@ -1681,11 +1603,9 @@ fn emit_host_initiated_registration( trait_def: &TraitDef, method: &MethodDef, wrappers: &HashMap, - ctx: &CodecContext, target_version: u32, ) -> Result<()> { - let (payload, response, version) = - emit_host_initiated_types(method, wrappers, ctx, target_version)?; + let (_, _, version) = emit_host_initiated_types(method, wrappers, target_version)?; let wire_const = wire_const_name(&trait_def.name, &method.name); let ReturnType::Subscription(item_ty) = &method.return_type else { bail!( @@ -1698,50 +1618,27 @@ fn emit_host_initiated_registration( request_wrapper_name(method, wrappers), versioned_wrapper_for(item_ty, wrappers).map(|(name, _)| name), )?; - - match envelope { - Some(envelope_name) => { - let method_name = &method.name; - writedoc!( - out, - " - this.{field} = transport.registerHostInitiatedSubscription({{ - ids: W.{wire_const}, - decodeRequest: (payload) => {{ - const envelope = T.{envelope_name}.dec(payload); - if (envelope.value.tag !== \"Start\") {{ - throw new Error(`{method_name}: expected Start direction, got ${{envelope.value.tag}}`); - }} - return envelope.value.value; - }}, - encodeItem: (item) => T.{envelope_name}.enc({{ tag: \"V{version}\", value: {{ tag: \"Receive\", value: item }} }}), - interruptPayload: HOST_INITIATED_DECLINE_PAYLOAD, - bufferCapacity: HOST_INITIATED_BUFFER_CAPACITY, - }}); - ", - field = host_registration_field(method), - ) - .unwrap(); - } - None => { - writedoc!( - out, - " - this.{field} = transport.registerHostInitiatedSubscription({{ - ids: W.{wire_const}, - decodeRequest: (payload) => {request_codec}.dec(payload).value, - encodeItem: (item) => {item_codec}.enc({{ tag: \"V{version}\", value: item }}), - interruptPayload: HOST_INITIATED_DECLINE_PAYLOAD, - bufferCapacity: HOST_INITIATED_BUFFER_CAPACITY, - }}); - ", - field = host_registration_field(method), - request_codec = payload.wire_codec_expr, - item_codec = response.wire_codec_expr, - ) - .unwrap(); - } - } + let method_name = &method.name; + writedoc!( + out, + " + this.{field} = transport.registerHostInitiatedSubscription({{ + ids: W.{wire_const}, + decodeRequest: (payload) => {{ + const envelope = T.{envelope}.dec(payload); + if (envelope.value.tag !== \"Start\") {{ + throw new Error(`{method_name}: expected Start direction, got ${{envelope.value.tag}}`); + }} + return envelope.value.value; + }}, + encodeItem: (item) => T.{envelope}.enc({{ tag: \"V{version}\", value: {{ tag: \"Receive\", value: item }} }}), + interruptPayload: HOST_INITIATED_DECLINE_PAYLOAD, + bufferCapacity: HOST_INITIATED_BUFFER_CAPACITY, + }}); + ", + field = host_registration_field(method), + ) + .unwrap(); Ok(()) } @@ -1750,7 +1647,6 @@ fn emit_host_initiated_method( method: &MethodDef, payload: &PayloadEmission, wrappers: &HashMap, - ctx: &CodecContext, wire_version: Option, ) -> Result<()> { let ReturnType::Subscription(item) = &method.return_type else { @@ -1759,7 +1655,7 @@ fn emit_host_initiated_method( method.name ); }; - let response = emit_response(item, wrappers, ctx, wire_version)?; + let response = emit_response(item, wrappers, wire_version)?; let name = format!("on{}", strip_prefix(&method.name).to_case(Case::Pascal)); writedoc!( out, @@ -1778,25 +1674,23 @@ fn emit_host_initiated_method( Ok(()) } -/// Emits a subscribe method body that returns an Observable-compatible object. -/// Payloadless `_interrupt` maps to `complete`; typed interrupt payloads map -/// to `error`. +/// Emits a subscribe method body that returns an Observable-compatible +/// object. Every subscription's envelope carries a real `Err` (a domain error +/// for `ResultSubscription`, `CallError` otherwise), so every +/// generated method gets a real `decodeInterrupt`: `undefined` maps to clean +/// completion, anything else maps to `error`. #[allow(clippy::too_many_arguments)] fn emit_subscribe_method( out: &mut String, ts_method_name: &str, wire_const: &str, payload: &PayloadEmission, - response: &ResponseEmission, item_type_ts: String, - err: Option, + err: ResponseEmission, wire_version: Option, - envelope: Option, + envelope: String, ) -> Result<()> { - let observable_args = match err.as_ref() { - Some(err) => format!("{item_type_ts}, {}", err.inner_type_ts), - None => item_type_ts.clone(), - }; + let observable_args = format!("{item_type_ts}, {}", err.inner_type_ts); let signature = if payload.param_list.is_empty() { format!(" {ts_method_name}(): ObservableLike<{observable_args}> {{") } else { @@ -1809,6 +1703,11 @@ fn emit_subscribe_method( ) }; + let version = wire_version.ok_or_else(|| { + anyhow::anyhow!( + "method `{ts_method_name}` resolved wire envelope `{envelope}` with no selected wire version" + ) + })?; writedoc!( out, " @@ -1816,94 +1715,37 @@ fn emit_subscribe_method( return createObservable<{observable_args}>({{ transport: this.transport, ids: W.{wire_const}, - " + payload: T.{envelope}.enc({{ tag: \"V{version}\", value: {{ tag: \"Start\", value: {value_expr} }} }}), + decodeItem: (payload) => {{ + const envelope = T.{envelope}.dec(payload); + if (envelope.value.tag !== \"Receive\") {{ + throw new Error(`{ts_method_name}: expected Receive direction, got ${{envelope.value.tag}}`); + }} + return envelope.value.value; + }}, + decodeInterrupt: (payload) => {{ + const envelope = T.{envelope}.dec(payload); + if (envelope.value.tag !== \"Interrupt\") {{ + throw new Error(`{ts_method_name}: expected Interrupt direction, got ${{envelope.value.tag}}`); + }} + const reason = envelope.value.value; + if (reason === undefined) return undefined; + ", + value_expr = payload.value_expr, ) .unwrap(); - - match envelope { - Some(envelope_name) => { - let version = wire_version.ok_or_else(|| { - anyhow::anyhow!( - "method `{ts_method_name}` resolved wire envelope `{envelope_name}` with no selected wire version" - ) - })?; - writedoc!( - out, - " - payload: T.{envelope_name}.enc({{ tag: \"V{version}\", value: {{ tag: \"Start\", value: {value_expr} }} }}), - decodeItem: (payload) => {{ - const envelope = T.{envelope_name}.dec(payload); - if (envelope.value.tag !== \"Receive\") {{ - throw new Error(`{ts_method_name}: expected Receive direction, got ${{envelope.value.tag}}`); - }} - return envelope.value.value; - }}, - ", - value_expr = payload.value_expr, - ) - .unwrap(); - if let Some(err) = &err { - writedoc!( - out, - " - decodeInterrupt: (payload) => {{ - const envelope = T.{envelope_name}.dec(payload); - if (envelope.value.tag !== \"Interrupt\") {{ - throw new Error(`{ts_method_name}: expected Interrupt direction, got ${{envelope.value.tag}}`); - }} - const reason = envelope.value.value; - if (reason === undefined) return undefined; - " - ) - .unwrap(); - match err.wrap_version { - Some(err_version) => writedoc!( - out, - " - return reason.tag === \"Domain\" - ? {{ tag: \"Domain\" as const, value: {{ tag: \"V{err_version}\", value: reason.value }} }} - : reason; - }}, - " - ) - .unwrap(), - None => writeln!(out, " return reason;\n }},").unwrap(), - } - } - } - None => { - write_payload_field( - out, - " ", - &payload.wire_codec_expr, - payload.wire_version, - &payload.value_expr, - ); - let item_value = if let Some(version) = wire_version { - versioned_value_expr( - &format!("{}.dec(payload)", response.wire_codec_expr), - &response.wire_type_ts, - &item_type_ts, - version, - ) - } else { - format!("{}.dec(payload)", response.wire_codec_expr) - }; - writeln!(out, " decodeItem: (payload) => {item_value},").unwrap(); - if let Some(err) = err { - let err_value = if let Some(version) = wire_version { - versioned_value_expr( - &format!("{}.dec(payload)", err.wire_codec_expr), - &err.wire_type_ts, - &err.inner_type_ts, - version, - ) - } else { - format!("{}.dec(payload)", err.wire_codec_expr) - }; - writeln!(out, " decodeInterrupt: (payload) => {err_value},").unwrap(); - } - } + match err.wrap_version { + Some(err_version) => writedoc!( + out, + " + return reason.tag === \"Domain\" + ? {{ tag: \"Domain\" as const, value: {{ tag: \"V{err_version}\", value: reason.value }} }} + : reason; + }}, + " + ) + .unwrap(), + None => writeln!(out, " return reason;\n }},").unwrap(), } writedoc!( @@ -2354,38 +2196,6 @@ fn inline_object_type_mode( )) } -fn method_payload_codec_expr( - params: &[ParamDef], - qualified: bool, - ctx: &CodecContext, -) -> Result { - method_payload_codec_expr_mode(params, qualified, ctx, NameMode::Public) -} - -fn method_payload_codec_expr_mode( - params: &[ParamDef], - qualified: bool, - ctx: &CodecContext, - mode: NameMode<'_>, -) -> Result { - match params.len() { - 0 => Ok("S._void".to_string()), - 1 => codec_expr_mode(¶ms[0].type_ref, qualified, ctx, mode), - _ => { - let codecs = params - .iter() - .map(|param| codec_expr_mode(¶m.type_ref, qualified, ctx, mode)) - .collect::>>()? - .join(", "); - Ok(format!("S.Tuple({codecs})")) - } - } -} - -fn codec_expr(ty: &TypeRef, qualified: bool, ctx: &CodecContext) -> Result { - codec_expr_mode(ty, qualified, ctx, NameMode::Public) -} - fn codec_expr_mode( ty: &TypeRef, qualified: bool, @@ -3240,6 +3050,8 @@ mod tests { versioned_tuple_wrapper_variants("FutureRequest", &[(2, "FutureRequestV2")]), versioned_tuple_wrapper_variants("FutureResponse", &[(2, "FutureResponseV2")]), versioned_tuple_wrapper_variants("FutureError", &[(2, "FutureErrorV2")]), + versioned_tuple_wrapper_variants("LegacyVersion", &[(1, "LegacyVersionV1")]), + versioned_tuple_wrapper_variants("FutureVersion", &[(2, "FutureVersionV2")]), ], }; @@ -3286,6 +3098,7 @@ mod tests { types: vec![ versioned_tuple_wrapper("ExampleRequest", "LegacyRequest", "LatestRequest"), versioned_tuple_wrapper("ExampleResponse", "LegacyResponse", "LatestResponse"), + versioned_tuple_wrapper_variants("ExampleVersion", &[(1, "ExampleVersionV1")]), ], }; @@ -3295,11 +3108,9 @@ mod tests { // target version. The codegen prefers the newest shared variant so // callers see the latest request/response shape the host advertises. assert!(client_source.contains("request: T.LatestRequest")); - assert!( - client_source.contains( - "payload: T.VersionedExampleRequest.enc({ tag: \"V2\", value: request })," - ) - ); + assert!(client_source.contains( + "payload: T.ExampleVersion.enc({ tag: \"V2\", value: { tag: \"Request\", value: request } })," + )); assert!(client_source.contains("ResultAsync")); } @@ -3404,17 +3215,16 @@ mod tests { types: vec![ versioned_tuple_wrapper_variants("ExampleRequest", &[(1, "LegacyRequest")]), versioned_tuple_wrapper("ExampleResponse", "LegacyResponse", "LatestResponse"), + versioned_tuple_wrapper_variants("ExampleVersion", &[(1, "ExampleVersionV1")]), ], }; let client_source = generate_client(&api, 2, 1).expect("generate client"); assert!(client_source.contains("request: T.LegacyRequest")); - assert!( - client_source.contains( - "payload: T.VersionedExampleRequest.enc({ tag: \"V1\", value: request })," - ) - ); + assert!(client_source.contains( + "payload: T.ExampleVersion.enc({ tag: \"V1\", value: { tag: \"Request\", value: request } })," + )); assert!(client_source.contains("ResultAsync")); } diff --git a/rust/crates/truapi-codegen/src/ts/playground.rs b/rust/crates/truapi-codegen/src/ts/playground.rs index 2ada755f8..dc6e88e27 100644 --- a/rust/crates/truapi-codegen/src/ts/playground.rs +++ b/rust/crates/truapi-codegen/src/ts/playground.rs @@ -28,7 +28,6 @@ fn generate_playground_services_code( let wrappers = collect_versioned_wrappers(api); let emit_versions = versioned_wrapper_emit_versions(api, &wrappers, target_version)?; let aliases = selected_public_aliases(api, &wrappers, &emit_versions, target_version); - let ctx = codec_context(&[]); let services = public_services(api)?; let explorer_type_ids = explorer_type_id_set(api, &aliases); @@ -73,14 +72,13 @@ fn generate_playground_services_code( for method in methods { let wire_version = method_wire_version(method, &wrappers, target_version)?; - let payload = emit_payload(&method.params, &wrappers, &ctx, wire_version)?; + let payload = emit_payload(&method.params, &wrappers, wire_version)?; let docs = split_playground_docs(method.docs.as_deref())?; let method_type = match method.kind { MethodKind::Request => "unary", MethodKind::Subscription | MethodKind::ResultSubscription => "subscription", }; - let signature = - build_method_signature(method, &payload, &wrappers, &ctx, wire_version)?; + let signature = build_method_signature(method, &payload, &wrappers, wire_version)?; let doc_url = build_doc_url(trait_def, method); writedoc!( @@ -136,7 +134,7 @@ fn generate_playground_services_code( writeln!(out, " requestType: {},", ts_string_literal(&id)).unwrap(); } let (response_inner, error_inner) = - method_response_inner_ts(method, &wrappers, &ctx, wire_version)?; + method_response_inner_ts(method, &wrappers, wire_version)?; if let Some(id) = response_inner.as_deref().and_then(data_type_id_from_ts) && explorer_type_ids.contains(&id) { @@ -319,22 +317,21 @@ pub fn explorer_type_id_set( pub(super) fn method_response_inner_ts( method: &MethodDef, wrappers: &HashMap, - ctx: &CodecContext, wire_version: Option, ) -> Result<(Option, Option)> { match &method.return_type { ReturnType::Result { ok, err } => { - let ok_resp = emit_response(ok, wrappers, ctx, wire_version)?; - let err_resp = emit_error_response(err, wrappers, ctx, wire_version)?; + let ok_resp = emit_response(ok, wrappers, wire_version)?; + let err_resp = emit_error_response(err, wrappers, wire_version)?; Ok((Some(ok_resp.inner_type_ts), Some(err_resp.inner_type_ts))) } ReturnType::Subscription(item) => { - let resp = emit_response(item, wrappers, ctx, wire_version)?; + let resp = emit_response(item, wrappers, wire_version)?; Ok((Some(resp.inner_type_ts), None)) } ReturnType::ResultSubscription { item, err } => { - let resp = emit_response(item, wrappers, ctx, wire_version)?; - let err_resp = emit_error_response(err, wrappers, ctx, wire_version)?; + let resp = emit_response(item, wrappers, wire_version)?; + let err_resp = emit_error_response(err, wrappers, wire_version)?; Ok((Some(resp.inner_type_ts), Some(err_resp.inner_type_ts))) } } @@ -364,7 +361,6 @@ fn build_method_signature( method: &MethodDef, payload: &PayloadEmission, wrappers: &HashMap, - ctx: &CodecContext, wire_version: Option, ) -> Result { let ts_method_name = to_camel_case(&strip_prefix(&method.name)); @@ -375,8 +371,8 @@ fn build_method_signature( }; let return_ts = match &method.return_type { ReturnType::Result { ok, err } => { - let ok_resp = emit_response(ok, wrappers, ctx, wire_version)?; - let err_resp = emit_error_response(err, wrappers, ctx, wire_version)?; + let ok_resp = emit_response(ok, wrappers, wire_version)?; + let err_resp = emit_error_response(err, wrappers, wire_version)?; format!( "Promise>", playground_type_name(&ok_resp.inner_type_ts), @@ -384,15 +380,15 @@ fn build_method_signature( ) } ReturnType::Subscription(item) => { - let response = emit_response(item, wrappers, ctx, wire_version)?; + let response = emit_response(item, wrappers, wire_version)?; format!( "ObservableLike<{}>", playground_type_name(&response.inner_type_ts), ) } ReturnType::ResultSubscription { item, err } => { - let response = emit_response(item, wrappers, ctx, wire_version)?; - let err_resp = emit_error_response(err, wrappers, ctx, wire_version)?; + let response = emit_response(item, wrappers, wire_version)?; + let err_resp = emit_error_response(err, wrappers, wire_version)?; format!( "ObservableLike<{}, {}>", playground_type_name(&response.inner_type_ts), diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs index c3d06f400..07cafa293 100644 --- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -40,433 +40,433 @@ pub enum WireKind { /// Wire discriminants for `system_handshake`. pub const SYSTEM_HANDSHAKE: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 0, }; /// Wire discriminants for `system_feature_supported`. pub const SYSTEM_FEATURE_SUPPORTED: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 1, }; /// Wire discriminants for `system_navigate_to`. pub const SYSTEM_NAVIGATE_TO: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 2, }; /// Wire discriminants for `system_host_info`. pub const SYSTEM_HOST_INFO: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 3, }; /// Wire discriminants for `system_get_product_context`. pub const SYSTEM_GET_PRODUCT_CONTEXT: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 4, }; /// Wire discriminants for `account_connection_status_subscribe`. pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 0, }; /// Wire discriminants for `account_get_account`. pub const ACCOUNT_GET_ACCOUNT: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 1, }; /// Wire discriminants for `account_get_account_alias`. pub const ACCOUNT_GET_ACCOUNT_ALIAS: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 2, }; /// Wire discriminants for `account_create_account_proof`. pub const ACCOUNT_CREATE_ACCOUNT_PROOF: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 3, }; /// Wire discriminants for `account_get_legacy_accounts`. pub const ACCOUNT_GET_LEGACY_ACCOUNTS: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 4, }; /// Wire discriminants for `account_get_user_id`. pub const ACCOUNT_GET_USER_ID: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 5, }; /// Wire discriminants for `account_request_login`. pub const ACCOUNT_REQUEST_LOGIN: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 6, }; /// Wire discriminants for `account_sign_vrf`. pub const ACCOUNT_SIGN_VRF: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 7, }; /// Wire discriminants for `account_register_ring_vrf_key`. pub const ACCOUNT_REGISTER_RING_VRF_KEY: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 8, }; /// Wire discriminants for `account_list_ring_vrf_keys`. pub const ACCOUNT_LIST_RING_VRF_KEYS: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 9, }; /// Wire discriminants for `account_ring_vrf_sign`. pub const ACCOUNT_RING_VRF_SIGN: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 10, }; /// Wire discriminants for `chain_follow_head_subscribe`. pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 0, }; /// Wire discriminants for `chain_get_head_header`. pub const CHAIN_GET_HEAD_HEADER: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 1, }; /// Wire discriminants for `chain_get_head_body`. pub const CHAIN_GET_HEAD_BODY: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 2, }; /// Wire discriminants for `chain_get_head_storage`. pub const CHAIN_GET_HEAD_STORAGE: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 3, }; /// Wire discriminants for `chain_call_head`. pub const CHAIN_CALL_HEAD: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 4, }; /// Wire discriminants for `chain_unpin_head`. pub const CHAIN_UNPIN_HEAD: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 5, }; /// Wire discriminants for `chain_continue_head`. pub const CHAIN_CONTINUE_HEAD: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 6, }; /// Wire discriminants for `chain_stop_head_operation`. pub const CHAIN_STOP_HEAD_OPERATION: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 7, }; /// Wire discriminants for `chain_get_spec_genesis_hash`. pub const CHAIN_GET_SPEC_GENESIS_HASH: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 8, }; /// Wire discriminants for `chain_get_spec_chain_name`. pub const CHAIN_GET_SPEC_CHAIN_NAME: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 9, }; /// Wire discriminants for `chain_get_spec_properties`. pub const CHAIN_GET_SPEC_PROPERTIES: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 10, }; /// Wire discriminants for `chain_broadcast_transaction`. pub const CHAIN_BROADCAST_TRANSACTION: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 11, }; /// Wire discriminants for `chain_stop_transaction`. pub const CHAIN_STOP_TRANSACTION: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 12, }; /// Wire discriminants for `chain_get_chain_info`. pub const CHAIN_GET_CHAIN_INFO: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 13, }; /// Wire discriminants for `chat_create_room`. pub const CHAT_CREATE_ROOM: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 0, }; /// Wire discriminants for `chat_register_bot`. pub const CHAT_REGISTER_BOT: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 1, }; /// Wire discriminants for `chat_list_subscribe`. pub const CHAT_LIST_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 2, }; /// Wire discriminants for `chat_post_message`. pub const CHAT_POST_MESSAGE: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 3, }; /// Wire discriminants for `chat_action_subscribe`. pub const CHAT_ACTION_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 4, }; /// Wire discriminants for `chat_custom_message_render`. pub const CHAT_CUSTOM_MESSAGE_RENDER: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 5, }; /// Wire discriminants for `coin_payment_create_purse`. pub const COIN_PAYMENT_CREATE_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 0, }; /// Wire discriminants for `coin_payment_query_purse`. pub const COIN_PAYMENT_QUERY_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 1, }; /// Wire discriminants for `coin_payment_rebalance_purse`. pub const COIN_PAYMENT_REBALANCE_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 2, }; /// Wire discriminants for `coin_payment_delete_purse`. pub const COIN_PAYMENT_DELETE_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 3, }; /// Wire discriminants for `coin_payment_create_receivable`. pub const COIN_PAYMENT_CREATE_RECEIVABLE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 4, }; /// Wire discriminants for `coin_payment_create_cheque`. pub const COIN_PAYMENT_CREATE_CHEQUE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 5, }; /// Wire discriminants for `coin_payment_deposit`. pub const COIN_PAYMENT_DEPOSIT: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 6, }; /// Wire discriminants for `coin_payment_refund`. pub const COIN_PAYMENT_REFUND: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 7, }; /// Wire discriminants for `coin_payment_listen_for_payment`. pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 8, }; /// Wire discriminants for `entropy_derive`. pub const ENTROPY_DERIVE: MethodIds = MethodIds { - trait_id: 198, + trait_id: 6, method_id: 0, }; /// Wire discriminants for `local_storage_read`. pub const LOCAL_STORAGE_READ: MethodIds = MethodIds { - trait_id: 199, + trait_id: 7, method_id: 0, }; /// Wire discriminants for `local_storage_write`. pub const LOCAL_STORAGE_WRITE: MethodIds = MethodIds { - trait_id: 199, + trait_id: 7, method_id: 1, }; /// Wire discriminants for `local_storage_clear`. pub const LOCAL_STORAGE_CLEAR: MethodIds = MethodIds { - trait_id: 199, + trait_id: 7, method_id: 2, }; /// Wire discriminants for `notifications_send_push_notification`. pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: MethodIds = MethodIds { - trait_id: 200, + trait_id: 8, method_id: 0, }; /// Wire discriminants for `notifications_cancel_push_notification`. pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: MethodIds = MethodIds { - trait_id: 200, + trait_id: 8, method_id: 1, }; /// Wire discriminants for `payment_balance_subscribe`. pub const PAYMENT_BALANCE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 0, }; /// Wire discriminants for `payment_top_up`. pub const PAYMENT_TOP_UP: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 1, }; /// Wire discriminants for `payment_request`. pub const PAYMENT_REQUEST: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 2, }; /// Wire discriminants for `payment_status_subscribe`. pub const PAYMENT_STATUS_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 3, }; /// Wire discriminants for `permissions_request_device_permission`. pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: MethodIds = MethodIds { - trait_id: 202, + trait_id: 10, method_id: 0, }; /// Wire discriminants for `permissions_request_remote_permission`. pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: MethodIds = MethodIds { - trait_id: 202, + trait_id: 10, method_id: 1, }; /// Wire discriminants for `preimage_lookup_subscribe`. pub const PREIMAGE_LOOKUP_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 203, + trait_id: 11, method_id: 0, }; /// Wire discriminants for `preimage_submit`. pub const PREIMAGE_SUBMIT: MethodIds = MethodIds { - trait_id: 203, + trait_id: 11, method_id: 1, }; /// Wire discriminants for `resource_allocation_request`. pub const RESOURCE_ALLOCATION_REQUEST: MethodIds = MethodIds { - trait_id: 204, + trait_id: 12, method_id: 0, }; /// Wire discriminants for `signing_create_transaction`. pub const SIGNING_CREATE_TRANSACTION: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 0, }; /// Wire discriminants for `signing_create_transaction_with_legacy_account`. pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 1, }; /// Wire discriminants for `signing_sign_raw_with_legacy_account`. pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 2, }; /// Wire discriminants for `signing_sign_payload_with_legacy_account`. pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 3, }; /// Wire discriminants for `signing_sign_raw`. pub const SIGNING_SIGN_RAW: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 4, }; /// Wire discriminants for `signing_sign_payload`. pub const SIGNING_SIGN_PAYLOAD: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 5, }; /// Wire discriminants for `statement_store_subscribe`. pub const STATEMENT_STORE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 0, }; /// Wire discriminants for `statement_store_create_proof`. pub const STATEMENT_STORE_CREATE_PROOF: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 1, }; /// Wire discriminants for `statement_store_submit`. pub const STATEMENT_STORE_SUBMIT: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 2, }; /// Wire discriminants for `statement_store_create_proof_authorized`. pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 3, }; /// Wire discriminants for `theme_subscribe`. pub const THEME_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 207, + trait_id: 15, method_id: 0, }; /// Wire discriminants for `locale_subscribe`. pub const LOCALE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 208, + trait_id: 16, method_id: 0, }; diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs index ff38d1738..a9c6b75bb 100644 --- a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -221,55 +221,18 @@ fn golden_dispatcher_and_wire_table() { dump.display() ); } - if output_name == "dispatcher.rs" { - // Every real method must resolve to the nested-envelope codegen - // path (RFC 0028): these markers belong only to the legacy - // (pre-RFC-0028) emission path, kept solely for truapi-codegen's - // own synthetic unit-test fixtures whose request/item wrapper - // names don't follow the `{Base}Request`/`{Base}Item` convention - // (see `envelope_type_name`). A real method's request or item - // wrapper always follows that convention, so a hit here means - // one silently fell through to the legacy path instead of - // failing loudly — most likely a renamed wrapper type no longer - // matching `envelope_type_name`'s expectations. - for marker in [ - "RequestFrameIds", - "SubscriptionFrameIds", - "encode_versioned_ok_payload", - "encode_versioned_err_payload", - "encode_versioned_unit_ok_payload", - "encode_versioned_interrupt_payload", - ] { - assert!( - !actual.contains(marker), - "generated dispatcher.rs contains `{marker}`, a legacy (pre-RFC-0028) \ - codegen marker; every real method should resolve to the nested-envelope \ - path instead — check which method's request/item wrapper name stopped \ - matching `envelope_type_name`'s `{{Base}}Request`/`{{Base}}Item` convention" - ); - } - } } - // `ts.rs` re-implements the same envelope/legacy fork independently of - // `rust/dispatcher.rs` (its own `envelope_type_name`/`method_envelope_name`), - // so a real method regressing to the legacy path there would slip past the - // Rust-side check above undetected. `S.indexedTaggedUnion(` is the codec - // shape the legacy fallback inlines directly into a method body - // (`versioned_result_codec_expr`/`write_payload_field`); the nested-envelope - // path only ever references a pre-built `T.{Method}Version` codec by name, - // so this string never appears in `client.ts` for a real method — it does - // appear throughout `types.ts`, where versioned wrapper *type* definitions - // legitimately use the same combinator, which is why only `client.ts` is - // scanned here. + // A method body should only ever reference its pre-built `T.{Method}Version` + // codec by name (see `method_envelope_name` in `ts.rs`); `types.ts` + // legitimately inlines `S.indexedTaggedUnion(` to define those wrapper + // codecs themselves, which is why only `client.ts` is scanned here. let client_ts = fs::read_to_string(tempdir.path().join("ts").join("client.ts")) .unwrap_or_else(|e| panic!("read generated client.ts: {e}")); assert!( !client_ts.contains("indexedTaggedUnion"), - "generated client.ts contains `S.indexedTaggedUnion(`, a legacy (pre-RFC-0028) \ - codegen marker; every real method should resolve to the nested-envelope path \ - instead — check which method's request/item wrapper name stopped matching \ - `method_envelope_name`'s expectations on the TS side" + "generated client.ts contains `S.indexedTaggedUnion(`; a method body should \ + reference its `T.{{Method}}Version` codec by name instead of inlining one" ); } diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index 92df40db0..ad5157482 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -12,7 +12,6 @@ use std::sync::Arc; use futures::future::BoxFuture; use parity_scale_codec::Encode; use tracing::instrument; -use truapi::{MIN_TRAIT_ID, WIRE_CODEC_VERSION}; use crate::frame::{ PROTOCOL_ERROR_KEY, PROTOCOL_ERROR_METHOD_ID, PROTOCOL_ERROR_TRAIT_ID, Payload, @@ -216,25 +215,6 @@ impl Dispatcher { // understood. No log - a peer speaking a wire we do not know could // otherwise flood the host's logs one frame at a time. let (trait_id, method_id) = key; - if trait_id < MIN_TRAIT_ID { - // No trait is addressed below this floor, so the first byte - // cannot be a trait id. A codec 1 peer, whose frames carry a - // single flat method byte here, lands in exactly this range. - // - // Do not answer it. Such a peer would read our `(255, 255)` - // reply as codec 1 discriminant 255 - its own protocol-error id - // - carrying a payload it cannot decode, and close its - // transport over a malformed-payload error that says nothing - // about the real fault. The log is the diagnostic instead. - tracing::error!( - request_id = %message.request_id, - trait_id, - method_id, - "trait id {trait_id} is below the codec {WIRE_CODEC_VERSION} minimum \ - {MIN_TRAIT_ID}; the peer appears to be speaking codec 1. Dropping frame" - ); - return; - } // A codec 2 peer that asked for something unimplemented can read // the answer, and dropping it would leave the peer waiting forever. transport.send(ProtocolMessage { diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index edd7fc716..1b9807eac 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -362,9 +362,9 @@ mod tests { fn handshake_request_encodes_with_the_system_trait_pair() { // SCALE-encoded HostHandshakeRequest::V1(2u8) = [0u8 variant][2u8 codec_version] let inner: Vec = vec![0x00, 0x02]; - // system trait = 193, handshake request = 0. - let msg = build(193, 0, inner.clone()); - assert_eq!(msg.encode(), expected_wire(193, 0, &inner)); + // system trait = 1, handshake request = 0. + let msg = build(1, 0, inner.clone()); + assert_eq!(msg.encode(), expected_wire(1, 0, &inner)); } /// Pins where the pair lands for a multi-byte payload. The payload here is @@ -377,9 +377,9 @@ mod tests { let mut inner = vec![0x00]; // V1 variant "foo".to_string().encode_to(&mut inner); 0u32.encode_to(&mut inner); - // account trait = 194, get_account request = 4. - let msg = build(194, 4, inner.clone()); - assert_eq!(msg.encode(), expected_wire(194, 4, &inner)); + // account trait = 2, get_account request = 4. + let msg = build(2, 4, inner.clone()); + assert_eq!(msg.encode(), expected_wire(2, 4, &inner)); } #[test] @@ -448,7 +448,7 @@ mod tests { /// codec. Catches a regression where `Decode` mishandles a frame whose /// payload is a bare `[version, Stop]` pair (no further inner data) but /// carries more for `Start`/`Interrupt`/`Receive`. The address is - /// `account_connection_status_subscribe`'s (trait 194, method 0). + /// `account_connection_status_subscribe`'s (trait 2, method 0). #[test] fn subscription_phases_round_trip_through_codec() { let cases: &[Vec] = &[ @@ -458,11 +458,11 @@ mod tests { vec![0x00, 0x03, 0x01, 0x02, 0x03], // receive: [version, Receive, item bytes] ]; for value in cases { - let msg = build(194, 0, value.clone()); + let msg = build(2, 0, value.clone()); let bytes = msg.encode(); assert_eq!( bytes, - expected_wire(194, 0, value), + expected_wire(2, 0, value), "encode mismatch for payload {value:?}" ); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); @@ -475,16 +475,16 @@ mod tests { #[test] fn id_helpers_resolve_known_methods() { let handshake = request_ids("system_handshake").expect("known request method"); - assert_eq!(handshake.trait_id, 193); + assert_eq!(handshake.trait_id, 1); assert_eq!(handshake.method_id, 0); let get_account = request_ids("account_get_account").expect("known request method"); - assert_eq!(get_account.trait_id, 194); + assert_eq!(get_account.trait_id, 2); assert_eq!(get_account.method_id, 1); let sub = subscription_ids("account_connection_status_subscribe").expect("known subscription"); - assert_eq!(sub.trait_id, 194); + assert_eq!(sub.trait_id, 2); assert_eq!(sub.method_id, 0); // A request method is not a subscription and vice versa. @@ -533,10 +533,10 @@ mod tests { /// handle `remaining_len == 0` without erroring or reading past EOF. #[test] fn empty_payload_round_trips() { - // local_storage_clear_response = (199, 5). - let msg = build(199, 5, Vec::new()); + // local_storage_clear_response = (7, 5). + let msg = build(7, 5, Vec::new()); let bytes = msg.encode(); - // [SCALE compact-len 0x0c][p][:][1][u8 199][u8 5] = 4 + 2 = 6 bytes total + // [SCALE compact-len 0x0c][p][:][1][u8 7][u8 5] = 4 + 2 = 6 bytes total assert_eq!(bytes.len(), 6); let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); assert_eq!(decoded, msg); diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs index c3d06f400..07cafa293 100644 --- a/rust/crates/truapi-server/src/generated/wire_table.rs +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -40,433 +40,433 @@ pub enum WireKind { /// Wire discriminants for `system_handshake`. pub const SYSTEM_HANDSHAKE: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 0, }; /// Wire discriminants for `system_feature_supported`. pub const SYSTEM_FEATURE_SUPPORTED: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 1, }; /// Wire discriminants for `system_navigate_to`. pub const SYSTEM_NAVIGATE_TO: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 2, }; /// Wire discriminants for `system_host_info`. pub const SYSTEM_HOST_INFO: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 3, }; /// Wire discriminants for `system_get_product_context`. pub const SYSTEM_GET_PRODUCT_CONTEXT: MethodIds = MethodIds { - trait_id: 193, + trait_id: 1, method_id: 4, }; /// Wire discriminants for `account_connection_status_subscribe`. pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 0, }; /// Wire discriminants for `account_get_account`. pub const ACCOUNT_GET_ACCOUNT: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 1, }; /// Wire discriminants for `account_get_account_alias`. pub const ACCOUNT_GET_ACCOUNT_ALIAS: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 2, }; /// Wire discriminants for `account_create_account_proof`. pub const ACCOUNT_CREATE_ACCOUNT_PROOF: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 3, }; /// Wire discriminants for `account_get_legacy_accounts`. pub const ACCOUNT_GET_LEGACY_ACCOUNTS: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 4, }; /// Wire discriminants for `account_get_user_id`. pub const ACCOUNT_GET_USER_ID: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 5, }; /// Wire discriminants for `account_request_login`. pub const ACCOUNT_REQUEST_LOGIN: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 6, }; /// Wire discriminants for `account_sign_vrf`. pub const ACCOUNT_SIGN_VRF: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 7, }; /// Wire discriminants for `account_register_ring_vrf_key`. pub const ACCOUNT_REGISTER_RING_VRF_KEY: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 8, }; /// Wire discriminants for `account_list_ring_vrf_keys`. pub const ACCOUNT_LIST_RING_VRF_KEYS: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 9, }; /// Wire discriminants for `account_ring_vrf_sign`. pub const ACCOUNT_RING_VRF_SIGN: MethodIds = MethodIds { - trait_id: 194, + trait_id: 2, method_id: 10, }; /// Wire discriminants for `chain_follow_head_subscribe`. pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 0, }; /// Wire discriminants for `chain_get_head_header`. pub const CHAIN_GET_HEAD_HEADER: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 1, }; /// Wire discriminants for `chain_get_head_body`. pub const CHAIN_GET_HEAD_BODY: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 2, }; /// Wire discriminants for `chain_get_head_storage`. pub const CHAIN_GET_HEAD_STORAGE: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 3, }; /// Wire discriminants for `chain_call_head`. pub const CHAIN_CALL_HEAD: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 4, }; /// Wire discriminants for `chain_unpin_head`. pub const CHAIN_UNPIN_HEAD: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 5, }; /// Wire discriminants for `chain_continue_head`. pub const CHAIN_CONTINUE_HEAD: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 6, }; /// Wire discriminants for `chain_stop_head_operation`. pub const CHAIN_STOP_HEAD_OPERATION: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 7, }; /// Wire discriminants for `chain_get_spec_genesis_hash`. pub const CHAIN_GET_SPEC_GENESIS_HASH: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 8, }; /// Wire discriminants for `chain_get_spec_chain_name`. pub const CHAIN_GET_SPEC_CHAIN_NAME: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 9, }; /// Wire discriminants for `chain_get_spec_properties`. pub const CHAIN_GET_SPEC_PROPERTIES: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 10, }; /// Wire discriminants for `chain_broadcast_transaction`. pub const CHAIN_BROADCAST_TRANSACTION: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 11, }; /// Wire discriminants for `chain_stop_transaction`. pub const CHAIN_STOP_TRANSACTION: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 12, }; /// Wire discriminants for `chain_get_chain_info`. pub const CHAIN_GET_CHAIN_INFO: MethodIds = MethodIds { - trait_id: 195, + trait_id: 3, method_id: 13, }; /// Wire discriminants for `chat_create_room`. pub const CHAT_CREATE_ROOM: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 0, }; /// Wire discriminants for `chat_register_bot`. pub const CHAT_REGISTER_BOT: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 1, }; /// Wire discriminants for `chat_list_subscribe`. pub const CHAT_LIST_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 2, }; /// Wire discriminants for `chat_post_message`. pub const CHAT_POST_MESSAGE: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 3, }; /// Wire discriminants for `chat_action_subscribe`. pub const CHAT_ACTION_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 4, }; /// Wire discriminants for `chat_custom_message_render`. pub const CHAT_CUSTOM_MESSAGE_RENDER: MethodIds = MethodIds { - trait_id: 196, + trait_id: 4, method_id: 5, }; /// Wire discriminants for `coin_payment_create_purse`. pub const COIN_PAYMENT_CREATE_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 0, }; /// Wire discriminants for `coin_payment_query_purse`. pub const COIN_PAYMENT_QUERY_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 1, }; /// Wire discriminants for `coin_payment_rebalance_purse`. pub const COIN_PAYMENT_REBALANCE_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 2, }; /// Wire discriminants for `coin_payment_delete_purse`. pub const COIN_PAYMENT_DELETE_PURSE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 3, }; /// Wire discriminants for `coin_payment_create_receivable`. pub const COIN_PAYMENT_CREATE_RECEIVABLE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 4, }; /// Wire discriminants for `coin_payment_create_cheque`. pub const COIN_PAYMENT_CREATE_CHEQUE: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 5, }; /// Wire discriminants for `coin_payment_deposit`. pub const COIN_PAYMENT_DEPOSIT: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 6, }; /// Wire discriminants for `coin_payment_refund`. pub const COIN_PAYMENT_REFUND: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 7, }; /// Wire discriminants for `coin_payment_listen_for_payment`. pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: MethodIds = MethodIds { - trait_id: 197, + trait_id: 5, method_id: 8, }; /// Wire discriminants for `entropy_derive`. pub const ENTROPY_DERIVE: MethodIds = MethodIds { - trait_id: 198, + trait_id: 6, method_id: 0, }; /// Wire discriminants for `local_storage_read`. pub const LOCAL_STORAGE_READ: MethodIds = MethodIds { - trait_id: 199, + trait_id: 7, method_id: 0, }; /// Wire discriminants for `local_storage_write`. pub const LOCAL_STORAGE_WRITE: MethodIds = MethodIds { - trait_id: 199, + trait_id: 7, method_id: 1, }; /// Wire discriminants for `local_storage_clear`. pub const LOCAL_STORAGE_CLEAR: MethodIds = MethodIds { - trait_id: 199, + trait_id: 7, method_id: 2, }; /// Wire discriminants for `notifications_send_push_notification`. pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: MethodIds = MethodIds { - trait_id: 200, + trait_id: 8, method_id: 0, }; /// Wire discriminants for `notifications_cancel_push_notification`. pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: MethodIds = MethodIds { - trait_id: 200, + trait_id: 8, method_id: 1, }; /// Wire discriminants for `payment_balance_subscribe`. pub const PAYMENT_BALANCE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 0, }; /// Wire discriminants for `payment_top_up`. pub const PAYMENT_TOP_UP: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 1, }; /// Wire discriminants for `payment_request`. pub const PAYMENT_REQUEST: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 2, }; /// Wire discriminants for `payment_status_subscribe`. pub const PAYMENT_STATUS_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 201, + trait_id: 9, method_id: 3, }; /// Wire discriminants for `permissions_request_device_permission`. pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: MethodIds = MethodIds { - trait_id: 202, + trait_id: 10, method_id: 0, }; /// Wire discriminants for `permissions_request_remote_permission`. pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: MethodIds = MethodIds { - trait_id: 202, + trait_id: 10, method_id: 1, }; /// Wire discriminants for `preimage_lookup_subscribe`. pub const PREIMAGE_LOOKUP_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 203, + trait_id: 11, method_id: 0, }; /// Wire discriminants for `preimage_submit`. pub const PREIMAGE_SUBMIT: MethodIds = MethodIds { - trait_id: 203, + trait_id: 11, method_id: 1, }; /// Wire discriminants for `resource_allocation_request`. pub const RESOURCE_ALLOCATION_REQUEST: MethodIds = MethodIds { - trait_id: 204, + trait_id: 12, method_id: 0, }; /// Wire discriminants for `signing_create_transaction`. pub const SIGNING_CREATE_TRANSACTION: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 0, }; /// Wire discriminants for `signing_create_transaction_with_legacy_account`. pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 1, }; /// Wire discriminants for `signing_sign_raw_with_legacy_account`. pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 2, }; /// Wire discriminants for `signing_sign_payload_with_legacy_account`. pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 3, }; /// Wire discriminants for `signing_sign_raw`. pub const SIGNING_SIGN_RAW: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 4, }; /// Wire discriminants for `signing_sign_payload`. pub const SIGNING_SIGN_PAYLOAD: MethodIds = MethodIds { - trait_id: 205, + trait_id: 13, method_id: 5, }; /// Wire discriminants for `statement_store_subscribe`. pub const STATEMENT_STORE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 0, }; /// Wire discriminants for `statement_store_create_proof`. pub const STATEMENT_STORE_CREATE_PROOF: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 1, }; /// Wire discriminants for `statement_store_submit`. pub const STATEMENT_STORE_SUBMIT: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 2, }; /// Wire discriminants for `statement_store_create_proof_authorized`. pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: MethodIds = MethodIds { - trait_id: 206, + trait_id: 14, method_id: 3, }; /// Wire discriminants for `theme_subscribe`. pub const THEME_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 207, + trait_id: 15, method_id: 0, }; /// Wire discriminants for `locale_subscribe`. pub const LOCALE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 208, + trait_id: 16, method_id: 0, }; diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs index 0b468685a..16a06019f 100644 --- a/rust/crates/truapi-server/tests/golden_frame.rs +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -21,7 +21,7 @@ //! //! On the wire (17 bytes): //! [0c 70 3a 31] requestId = compact-len(3) + "p:1" -//! [c2] trait discriminant 194 = account +//! [02] trait discriminant 2 = account //! [01] method discriminant 1 = get_account //! [00] envelope version V1 //! [00] direction tag: Request diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin index b6357a4bb906dba2db8ba3394bceb95569dbdd38..bf1bbe7fea9d3b75bd9c6f68cabf77739de96243 100644 GIT binary patch literal 17 Vcmd-nurg#~WMJS)%g<*30RR_P0y_Wz literal 17 Vcmd-nurfTv$iTppmY>f60stWv0{Z{} diff --git a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs index cb80f95bc..ff83a890d 100644 --- a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs +++ b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs @@ -184,11 +184,10 @@ fn rust_and_ts_wire_tables_agree() { ); } -/// `transport.ts` hand-mirrors two Rust constants that the generated table does -/// not carry: the codec's trait-id floor and the reserved protocol-error -/// address. They are hand-written on the TS side, so nothing but this test stops -/// them drifting - and a drift means one language answers frames the other -/// refuses. +/// `transport.ts` hand-mirrors the reserved protocol-error address, which the +/// generated table does not carry. It is hand-written on the TS side, so +/// nothing but this test stops it drifting - and a drift means one language +/// answers frames the other refuses. #[test] fn transport_ts_mirrors_the_rust_wire_constants() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -197,7 +196,6 @@ fn transport_ts_mirrors_the_rust_wire_constants() { .unwrap_or_else(|err| panic!("read {}: {err}", path.display())); for (name, expected) in [ - ("MIN_TRAIT_ID", truapi::MIN_TRAIT_ID), ("PROTOCOL_ERROR_TRAIT_ID", 255), ("PROTOCOL_ERROR_METHOD_ID", 255), ] { diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index dc3ca934b..df9274143 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -18,7 +18,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Account lookup, aliasing, and proof generation. -#[wire_trait(id = 194)] +#[wire_trait(id = 2)] #[crate::async_trait] pub trait Account: Send + Sync { /// Subscribe to account connection status changes. diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index 4209eb7cf..453a529db 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -23,7 +23,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Chain interaction methods. -#[wire_trait(id = 195)] +#[wire_trait(id = 3)] #[crate::async_trait] pub trait Chain: Send + Sync { /// Follow the chain head and receive block events. diff --git a/rust/crates/truapi/src/api/chat.rs b/rust/crates/truapi/src/api/chat.rs index 7bb591cca..580e55f71 100644 --- a/rust/crates/truapi/src/api/chat.rs +++ b/rust/crates/truapi/src/api/chat.rs @@ -11,7 +11,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Chat room, bot, and message APIs. -#[wire_trait(id = 196)] +#[wire_trait(id = 4)] #[crate::service(required_execution = Worker)] #[crate::async_trait] pub trait Chat: Send + Sync { diff --git a/rust/crates/truapi/src/api/coin_payment.rs b/rust/crates/truapi/src/api/coin_payment.rs index 47bf97217..e105d4e4c 100644 --- a/rust/crates/truapi/src/api/coin_payment.rs +++ b/rust/crates/truapi/src/api/coin_payment.rs @@ -22,7 +22,7 @@ use crate::{wire, wire_trait}; /// RFC 0017 describes `Resolvable` values for long-running operations. /// TrUAPI represents those as subscriptions whose items are the RFC status /// updates. -#[wire_trait(id = 197)] +#[wire_trait(id = 5)] #[crate::async_trait] pub trait CoinPayment: Send + Sync { /// Create a new firewalled CoinPayment purse. diff --git a/rust/crates/truapi/src/api/entropy.rs b/rust/crates/truapi/src/api/entropy.rs index 882acd7db..14c797e59 100644 --- a/rust/crates/truapi/src/api/entropy.rs +++ b/rust/crates/truapi/src/api/entropy.rs @@ -7,7 +7,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Deterministic entropy derivation. -#[wire_trait(id = 198)] +#[wire_trait(id = 6)] #[crate::async_trait] pub trait Entropy: Send + Sync { /// Derive deterministic entropy. diff --git a/rust/crates/truapi/src/api/local_storage.rs b/rust/crates/truapi/src/api/local_storage.rs index 299aa3f2e..df024aac7 100644 --- a/rust/crates/truapi/src/api/local_storage.rs +++ b/rust/crates/truapi/src/api/local_storage.rs @@ -9,7 +9,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Local key/value storage scoped to the calling product. -#[wire_trait(id = 199)] +#[wire_trait(id = 7)] #[crate::async_trait] pub trait LocalStorage: Send + Sync { /// Read a value by key. diff --git a/rust/crates/truapi/src/api/locale.rs b/rust/crates/truapi/src/api/locale.rs index 8009b2389..6d4ff0c38 100644 --- a/rust/crates/truapi/src/api/locale.rs +++ b/rust/crates/truapi/src/api/locale.rs @@ -5,7 +5,7 @@ use crate::{CallContext, Subscription}; use crate::{wire, wire_trait}; /// Host locale subscription. -#[wire_trait(id = 208)] +#[wire_trait(id = 16)] #[crate::async_trait] pub trait Locale: Send + Sync { /// Subscribe to the host's selected locale. diff --git a/rust/crates/truapi/src/api/notifications.rs b/rust/crates/truapi/src/api/notifications.rs index 707189682..aeb2e4726 100644 --- a/rust/crates/truapi/src/api/notifications.rs +++ b/rust/crates/truapi/src/api/notifications.rs @@ -9,7 +9,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Notification methods for locally-rendered push notifications. -#[wire_trait(id = 200)] +#[wire_trait(id = 8)] #[crate::async_trait] pub trait Notifications: Send + Sync { /// Send a push notification to the user. diff --git a/rust/crates/truapi/src/api/payment.rs b/rust/crates/truapi/src/api/payment.rs index 40744d1dc..2c3438e22 100644 --- a/rust/crates/truapi/src/api/payment.rs +++ b/rust/crates/truapi/src/api/payment.rs @@ -11,7 +11,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Payment request and balance/status subscription methods. -#[wire_trait(id = 201)] +#[wire_trait(id = 9)] #[crate::async_trait] pub trait Payment: Send + Sync { /// Subscribe to payment balance updates. diff --git a/rust/crates/truapi/src/api/permissions.rs b/rust/crates/truapi/src/api/permissions.rs index 996951466..df9e8a7ad 100644 --- a/rust/crates/truapi/src/api/permissions.rs +++ b/rust/crates/truapi/src/api/permissions.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Permission request methods. -#[wire_trait(id = 202)] +#[wire_trait(id = 10)] #[crate::async_trait] pub trait Permissions: Send + Sync { /// Request a device-capability permission from the user. diff --git a/rust/crates/truapi/src/api/preimage.rs b/rust/crates/truapi/src/api/preimage.rs index f178fcc5c..64cca8e3d 100644 --- a/rust/crates/truapi/src/api/preimage.rs +++ b/rust/crates/truapi/src/api/preimage.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Preimage lookup and submission methods. -#[wire_trait(id = 203)] +#[wire_trait(id = 11)] #[crate::async_trait] pub trait Preimage: Send + Sync { /// Subscribe to preimage lookups for a given key. diff --git a/rust/crates/truapi/src/api/resource_allocation.rs b/rust/crates/truapi/src/api/resource_allocation.rs index a0ae1cb4a..7872f604c 100644 --- a/rust/crates/truapi/src/api/resource_allocation.rs +++ b/rust/crates/truapi/src/api/resource_allocation.rs @@ -8,7 +8,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Resource pre-allocation (allowance management). -#[wire_trait(id = 204)] +#[wire_trait(id = 12)] #[crate::async_trait] pub trait ResourceAllocation: Send + Sync { /// Request the host to pre-allocate one or more resources. diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 8b8e5e3de..616a9244f 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -16,7 +16,7 @@ use crate::{CallContext, CallError}; use crate::{wire, wire_trait}; /// Signing operations. -#[wire_trait(id = 205)] +#[wire_trait(id = 13)] #[crate::async_trait] pub trait Signing: Send + Sync { /// Construct a transaction for a product account. diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 83a3fb0e0..cac57faad 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -13,7 +13,7 @@ use crate::{CallContext, CallError, Subscription}; use crate::{wire, wire_trait}; /// Statement store methods. -#[wire_trait(id = 206)] +#[wire_trait(id = 14)] #[crate::async_trait] pub trait StatementStore: Send + Sync { /// Subscribe to statements matching a topic filter. diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 632098f7d..88203d17e 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -12,7 +12,7 @@ use crate::{wire, wire_trait}; /// General-purpose TrUAPI methods for handshake, feature detection, /// navigation, and runtime information. -#[wire_trait(id = 193)] +#[wire_trait(id = 1)] #[crate::async_trait] pub trait System: Send + Sync { /// Negotiate the wire codec version with the product. diff --git a/rust/crates/truapi/src/api/theme.rs b/rust/crates/truapi/src/api/theme.rs index 48c15dc83..69a07adf2 100644 --- a/rust/crates/truapi/src/api/theme.rs +++ b/rust/crates/truapi/src/api/theme.rs @@ -5,7 +5,7 @@ use crate::{CallContext, Subscription}; use crate::{wire, wire_trait}; /// Host theme subscription. -#[wire_trait(id = 207)] +#[wire_trait(id = 15)] #[crate::async_trait] pub trait Theme: Send + Sync { /// Subscribe to host theme changes. diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index 4ead6d3d1..e57d92be4 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -207,22 +207,6 @@ pub use truapi_macros::{service, wire, wire_trait}; /// generated clients, so every peer derives it from here. pub const WIRE_CODEC_VERSION: u8 = 2; -/// Highest method discriminant any codec 1 implementation assigned, and so -/// the largest first byte a codec 1 frame can carry. `triangle-js-sdks` -/// `host-api` allocated 166..=171 to the RFC-0024 ring VRF methods; this -/// crate's own flat numbering later reached 192 via `System::host_info`, -/// added on `main` while codec 2 was still unmerged. 192 is the highest -/// known codec 1 discriminant across both, so it sets the ceiling. -pub const MAX_CODEC_1_METHOD_ID: u8 = 192; - -/// Lowest wire trait id [`WIRE_CODEC_VERSION`] may assign. The first byte -/// after the request id is the trait, so keeping every trait id above -/// [`MAX_CODEC_1_METHOD_ID`] means a codec 1 frame can never decode into a -/// registered trait: it is reported as unroutable instead of executing -/// whichever trait happens to share its old flat id. Codegen rejects any -/// `#[wire_trait(id = N)]` below this floor. -pub const MIN_TRAIT_ID: u8 = 193; - /// Per-message id carried from the transport frame. pub type RequestId = String;