Skip to main content

host_api/
protocol.rs

1//! Wire protocol for the Polkadot app host-api.
2//!
3//! Message = Struct { requestId: str, payload: Enum { ...76+ variants... } }
4//!
5//! Each payload variant is itself a versioned enum (currently only "v1" = tag 0).
6//! Inside the version wrapper, the actual data is method-specific.
7//!
8//! Tag indices are determined by the insertion order of methods in the protocol
9//! definition, expanded as: request methods → _request, _response;
10//! subscription methods → _start, _stop, _interrupt, _receive.
11
12use crate::codec::*;
13
14// ---------------------------------------------------------------------------
15// Payload tag indices (determined by protocol method insertion order)
16// ---------------------------------------------------------------------------
17
18// request/response pairs: tag N = request, tag N+1 = response
19pub const TAG_HANDSHAKE_REQ: u8 = 0;
20pub const TAG_HANDSHAKE_RESP: u8 = 1;
21pub const TAG_FEATURE_SUPPORTED_REQ: u8 = 2;
22pub const TAG_FEATURE_SUPPORTED_RESP: u8 = 3;
23pub const TAG_PUSH_NOTIFICATION_REQ: u8 = 4;
24pub const TAG_PUSH_NOTIFICATION_RESP: u8 = 5;
25pub const TAG_NAVIGATE_TO_REQ: u8 = 6;
26pub const TAG_NAVIGATE_TO_RESP: u8 = 7;
27pub const TAG_DEVICE_PERMISSION_REQ: u8 = 8;
28pub const TAG_DEVICE_PERMISSION_RESP: u8 = 9;
29pub const TAG_REMOTE_PERMISSION_REQ: u8 = 10;
30pub const TAG_REMOTE_PERMISSION_RESP: u8 = 11;
31pub const TAG_LOCAL_STORAGE_READ_REQ: u8 = 12;
32pub const TAG_LOCAL_STORAGE_READ_RESP: u8 = 13;
33pub const TAG_LOCAL_STORAGE_WRITE_REQ: u8 = 14;
34pub const TAG_LOCAL_STORAGE_WRITE_RESP: u8 = 15;
35pub const TAG_LOCAL_STORAGE_CLEAR_REQ: u8 = 16;
36pub const TAG_LOCAL_STORAGE_CLEAR_RESP: u8 = 17;
37// subscription: _start, _stop, _interrupt, _receive
38pub const TAG_ACCOUNT_STATUS_START: u8 = 18;
39pub const TAG_ACCOUNT_STATUS_STOP: u8 = 19;
40pub const TAG_ACCOUNT_STATUS_INTERRUPT: u8 = 20;
41pub const TAG_ACCOUNT_STATUS_RECEIVE: u8 = 21;
42pub const TAG_ACCOUNT_GET_REQ: u8 = 22;
43pub const TAG_ACCOUNT_GET_RESP: u8 = 23;
44pub const TAG_ACCOUNT_GET_ALIAS_REQ: u8 = 24;
45pub const TAG_ACCOUNT_GET_ALIAS_RESP: u8 = 25;
46pub const TAG_ACCOUNT_CREATE_PROOF_REQ: u8 = 26;
47pub const TAG_ACCOUNT_CREATE_PROOF_RESP: u8 = 27;
48pub const TAG_GET_NON_PRODUCT_ACCOUNTS_REQ: u8 = 28;
49pub const TAG_GET_NON_PRODUCT_ACCOUNTS_RESP: u8 = 29;
50pub const TAG_CREATE_TRANSACTION_REQ: u8 = 30;
51pub const TAG_CREATE_TRANSACTION_RESP: u8 = 31;
52pub const TAG_CREATE_TX_NON_PRODUCT_REQ: u8 = 32;
53pub const TAG_CREATE_TX_NON_PRODUCT_RESP: u8 = 33;
54pub const TAG_SIGN_RAW_REQ: u8 = 34;
55pub const TAG_SIGN_RAW_RESP: u8 = 35;
56pub const TAG_SIGN_PAYLOAD_REQ: u8 = 36;
57pub const TAG_SIGN_PAYLOAD_RESP: u8 = 37;
58pub const TAG_CHAT_CREATE_ROOM_REQ: u8 = 38;
59pub const TAG_CHAT_CREATE_ROOM_RESP: u8 = 39;
60pub const TAG_CHAT_REGISTER_BOT_REQ: u8 = 40;
61pub const TAG_CHAT_REGISTER_BOT_RESP: u8 = 41;
62pub const TAG_CHAT_LIST_START: u8 = 42;
63pub const TAG_CHAT_LIST_STOP: u8 = 43;
64pub const TAG_CHAT_LIST_INTERRUPT: u8 = 44;
65pub const TAG_CHAT_LIST_RECEIVE: u8 = 45;
66pub const TAG_CHAT_POST_MSG_REQ: u8 = 46;
67pub const TAG_CHAT_POST_MSG_RESP: u8 = 47;
68pub const TAG_CHAT_ACTION_START: u8 = 48;
69pub const TAG_CHAT_ACTION_STOP: u8 = 49;
70pub const TAG_CHAT_ACTION_INTERRUPT: u8 = 50;
71pub const TAG_CHAT_ACTION_RECEIVE: u8 = 51;
72pub const TAG_CHAT_CUSTOM_MSG_START: u8 = 52;
73pub const TAG_CHAT_CUSTOM_MSG_STOP: u8 = 53;
74pub const TAG_CHAT_CUSTOM_MSG_INTERRUPT: u8 = 54;
75pub const TAG_CHAT_CUSTOM_MSG_RECEIVE: u8 = 55;
76pub const TAG_STATEMENT_STORE_START: u8 = 56;
77pub const TAG_STATEMENT_STORE_STOP: u8 = 57;
78pub const TAG_STATEMENT_STORE_INTERRUPT: u8 = 58;
79pub const TAG_STATEMENT_STORE_RECEIVE: u8 = 59;
80pub const TAG_STATEMENT_PROOF_REQ: u8 = 60;
81pub const TAG_STATEMENT_PROOF_RESP: u8 = 61;
82pub const TAG_STATEMENT_SUBMIT_REQ: u8 = 62;
83pub const TAG_STATEMENT_SUBMIT_RESP: u8 = 63;
84pub const TAG_PREIMAGE_LOOKUP_START: u8 = 64;
85pub const TAG_PREIMAGE_LOOKUP_STOP: u8 = 65;
86pub const TAG_PREIMAGE_LOOKUP_INTERRUPT: u8 = 66;
87pub const TAG_PREIMAGE_LOOKUP_RECEIVE: u8 = 67;
88pub const TAG_PREIMAGE_SUBMIT_REQ: u8 = 68;
89pub const TAG_PREIMAGE_SUBMIT_RESP: u8 = 69;
90pub const TAG_JSONRPC_SEND_REQ: u8 = 70;
91pub const TAG_JSONRPC_SEND_RESP: u8 = 71;
92pub const TAG_JSONRPC_SUB_START: u8 = 72;
93pub const TAG_JSONRPC_SUB_STOP: u8 = 73;
94pub const TAG_JSONRPC_SUB_INTERRUPT: u8 = 74;
95pub const TAG_JSONRPC_SUB_RECEIVE: u8 = 75;
96// remote_chain_head_follow (subscription: start, stop, interrupt, receive)
97pub const TAG_CHAIN_HEAD_FOLLOW_START: u8 = 76;
98pub const TAG_CHAIN_HEAD_FOLLOW_STOP: u8 = 77;
99pub const TAG_CHAIN_HEAD_FOLLOW_INTERRUPT: u8 = 78;
100pub const TAG_CHAIN_HEAD_FOLLOW_RECEIVE: u8 = 79;
101// remote_chain_head_header (request/response)
102pub const TAG_CHAIN_HEAD_HEADER_REQ: u8 = 80;
103pub const TAG_CHAIN_HEAD_HEADER_RESP: u8 = 81;
104// remote_chain_head_body (request/response)
105pub const TAG_CHAIN_HEAD_BODY_REQ: u8 = 82;
106pub const TAG_CHAIN_HEAD_BODY_RESP: u8 = 83;
107// remote_chain_head_storage (request/response)
108pub const TAG_CHAIN_HEAD_STORAGE_REQ: u8 = 84;
109pub const TAG_CHAIN_HEAD_STORAGE_RESP: u8 = 85;
110// remote_chain_head_call (request/response)
111pub const TAG_CHAIN_HEAD_CALL_REQ: u8 = 86;
112pub const TAG_CHAIN_HEAD_CALL_RESP: u8 = 87;
113// remote_chain_head_unpin (request/response)
114pub const TAG_CHAIN_HEAD_UNPIN_REQ: u8 = 88;
115pub const TAG_CHAIN_HEAD_UNPIN_RESP: u8 = 89;
116// remote_chain_head_continue (request/response)
117pub const TAG_CHAIN_HEAD_CONTINUE_REQ: u8 = 90;
118pub const TAG_CHAIN_HEAD_CONTINUE_RESP: u8 = 91;
119// remote_chain_head_stop_operation (request/response)
120pub const TAG_CHAIN_HEAD_STOP_OP_REQ: u8 = 92;
121pub const TAG_CHAIN_HEAD_STOP_OP_RESP: u8 = 93;
122// remote_chain_spec_genesis_hash (request/response)
123pub const TAG_CHAIN_SPEC_GENESIS_REQ: u8 = 94;
124pub const TAG_CHAIN_SPEC_GENESIS_RESP: u8 = 95;
125// remote_chain_spec_chain_name (request/response)
126pub const TAG_CHAIN_SPEC_NAME_REQ: u8 = 96;
127pub const TAG_CHAIN_SPEC_NAME_RESP: u8 = 97;
128// remote_chain_spec_properties (request/response)
129pub const TAG_CHAIN_SPEC_PROPS_REQ: u8 = 98;
130pub const TAG_CHAIN_SPEC_PROPS_RESP: u8 = 99;
131// remote_chain_transaction_broadcast (request/response)
132pub const TAG_CHAIN_TX_BROADCAST_REQ: u8 = 100;
133pub const TAG_CHAIN_TX_BROADCAST_RESP: u8 = 101;
134// remote_chain_transaction_stop (request/response)
135pub const TAG_CHAIN_TX_STOP_REQ: u8 = 102;
136pub const TAG_CHAIN_TX_STOP_RESP: u8 = 103;
137
138/// Protocol version (JAM_CODEC_PROTOCOL_ID).
139pub const PROTOCOL_VERSION: u8 = 1;
140
141// ---------------------------------------------------------------------------
142// High-level types
143// ---------------------------------------------------------------------------
144
145/// An account returned by host_get_non_product_accounts.
146#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
147pub struct Account {
148    /// Raw public key bytes (typically 32 bytes for sr25519/ed25519).
149    pub public_key: Vec<u8>,
150    /// Optional display name.
151    pub name: Option<String>,
152}
153
154/// Decoded incoming request from the app.
155#[derive(Debug)]
156pub enum HostRequest {
157    Handshake {
158        version: u8,
159    },
160    GetNonProductAccounts,
161    FeatureSupported {
162        feature_data: Vec<u8>,
163    },
164    LocalStorageRead {
165        key: String,
166    },
167    LocalStorageWrite {
168        key: String,
169        value: Vec<u8>,
170    },
171    LocalStorageClear {
172        key: String,
173    },
174    SignPayload {
175        public_key: Vec<u8>,
176        payload: Vec<u8>,
177    },
178    SignRaw {
179        public_key: Vec<u8>,
180        data: Vec<u8>,
181    },
182    CreateTransaction {
183        payload: Vec<u8>,
184    },
185    NavigateTo {
186        url: String,
187    },
188    PushNotification {
189        text: String,
190        deeplink: Option<String>,
191    },
192    AccountConnectionStatusStart,
193    JsonRpcSend {
194        data: Vec<u8>,
195    },
196    JsonRpcSubscribeStart {
197        data: Vec<u8>,
198    },
199    /// chainHead_v1_follow start: genesis hash + withRuntime flag
200    ChainHeadFollowStart {
201        genesis_hash: Vec<u8>,
202        with_runtime: bool,
203    },
204    /// chainHead request with genesis hash, follow subscription ID, and block hash.
205    /// Used for header (tag 80), body (82), unpin (88 - hashes instead of hash).
206    ChainHeadRequest {
207        tag: u8,
208        genesis_hash: Vec<u8>,
209        follow_sub_id: String,
210        data: serde_json::Value,
211    },
212    /// chainSpec request with just genesis hash (tags 94, 96, 98).
213    ChainSpecRequest {
214        tag: u8,
215        genesis_hash: Vec<u8>,
216    },
217    /// Transaction broadcast (tag 100): genesis hash + transaction hex bytes.
218    ChainTxBroadcast {
219        genesis_hash: Vec<u8>,
220        transaction: Vec<u8>,
221    },
222    /// Transaction stop (tag 102): genesis hash + operation ID.
223    ChainTxStop {
224        genesis_hash: Vec<u8>,
225        operation_id: String,
226    },
227    /// A request we recognize by tag but don't handle yet.
228    Unimplemented {
229        tag: u8,
230    },
231    /// A tag we don't recognize at all.
232    Unknown {
233        tag: u8,
234    },
235}
236
237/// Outgoing response to the app.
238#[derive(Debug)]
239pub enum HostResponse {
240    HandshakeOk,
241    AccountList(Vec<Account>),
242    Error(String),
243}
244
245// ---------------------------------------------------------------------------
246// Wire message decode / encode
247// ---------------------------------------------------------------------------
248
249/// Encode raw bytes as a 0x-prefixed lowercase hex string.
250fn bytes_to_hex(bytes: &[u8]) -> String {
251    let mut s = String::with_capacity(2 + bytes.len() * 2);
252    s.push_str("0x");
253    for b in bytes {
254        s.push_str(&format!("{b:02x}"));
255    }
256    s
257}
258
259/// Decode a raw binary message into (request_id, HostRequest).
260pub fn decode_message(data: &[u8]) -> Result<(String, u8, HostRequest), DecodeErr> {
261    let mut r = Reader::new(data);
262
263    // requestId: SCALE string
264    let request_id = r.read_string()?;
265
266    // payload: enum tag (u8) + inner bytes
267    let tag = r.read_u8()?;
268
269    let req = match tag {
270        TAG_HANDSHAKE_REQ => {
271            // inner: Enum { v1: u8 }
272            let _version_tag = r.read_u8()?; // 0 = "v1"
273            let version = r.read_u8()?;
274            HostRequest::Handshake { version }
275        }
276        TAG_GET_NON_PRODUCT_ACCOUNTS_REQ => {
277            // inner: Enum { v1: void }
278            let _version_tag = r.read_u8()?;
279            // void = no more bytes
280            HostRequest::GetNonProductAccounts
281        }
282        TAG_FEATURE_SUPPORTED_REQ => {
283            let _version_tag = r.read_u8()?;
284            let rest = r.remaining().to_vec();
285            r.skip_rest();
286            HostRequest::FeatureSupported { feature_data: rest }
287        }
288        TAG_LOCAL_STORAGE_READ_REQ => {
289            let _version_tag = r.read_u8()?;
290            let key = r.read_string()?;
291            HostRequest::LocalStorageRead { key }
292        }
293        TAG_LOCAL_STORAGE_WRITE_REQ => {
294            let _version_tag = r.read_u8()?;
295            let key = r.read_string()?;
296            let value = r.read_var_bytes()?;
297            HostRequest::LocalStorageWrite { key, value }
298        }
299        TAG_LOCAL_STORAGE_CLEAR_REQ => {
300            let _version_tag = r.read_u8()?;
301            let key = r.read_string()?;
302            HostRequest::LocalStorageClear { key }
303        }
304        TAG_SIGN_PAYLOAD_REQ => {
305            let _version_tag = r.read_u8()?;
306            let public_key = r.read_var_bytes()?;
307            let payload = r.remaining().to_vec();
308            r.skip_rest();
309            HostRequest::SignPayload {
310                public_key,
311                payload,
312            }
313        }
314        TAG_SIGN_RAW_REQ => {
315            let _version_tag = r.read_u8()?;
316            let public_key = r.read_var_bytes()?;
317            let data = r.remaining().to_vec();
318            r.skip_rest();
319            HostRequest::SignRaw { public_key, data }
320        }
321        TAG_CREATE_TRANSACTION_REQ => {
322            let _version_tag = r.read_u8()?;
323            let rest = r.remaining().to_vec();
324            r.skip_rest();
325            HostRequest::CreateTransaction { payload: rest }
326        }
327        TAG_NAVIGATE_TO_REQ => {
328            let _version_tag = r.read_u8()?;
329            let url = r.read_string()?;
330            HostRequest::NavigateTo { url }
331        }
332        TAG_PUSH_NOTIFICATION_REQ => {
333            let _version_tag = r.read_u8()?;
334            let text = r.read_string()?;
335            let deeplink = r.read_option(|r| r.read_string())?;
336            HostRequest::PushNotification { text, deeplink }
337        }
338        TAG_ACCOUNT_STATUS_START => {
339            let _version_tag = r.read_u8()?;
340            HostRequest::AccountConnectionStatusStart
341        }
342        TAG_JSONRPC_SEND_REQ => {
343            let _version_tag = r.read_u8()?;
344            let rest = r.remaining().to_vec();
345            r.skip_rest();
346            HostRequest::JsonRpcSend { data: rest }
347        }
348        TAG_JSONRPC_SUB_START => {
349            let _version_tag = r.read_u8()?;
350            let rest = r.remaining().to_vec();
351            r.skip_rest();
352            HostRequest::JsonRpcSubscribeStart { data: rest }
353        }
354        TAG_CHAIN_HEAD_FOLLOW_START => {
355            let _version_tag = r.read_u8()?;
356            let genesis_hash = r.read_var_bytes()?;
357            let with_runtime = r.read_u8()? != 0;
358            HostRequest::ChainHeadFollowStart {
359                genesis_hash,
360                with_runtime,
361            }
362        }
363        TAG_CHAIN_HEAD_HEADER_REQ => {
364            let _version_tag = r.read_u8()?;
365            let genesis_hash = r.read_var_bytes()?;
366            let follow_sub_id = r.read_string()?;
367            let hash = r.read_var_bytes()?;
368            let hash_hex = bytes_to_hex(&hash);
369            HostRequest::ChainHeadRequest {
370                tag,
371                genesis_hash,
372                follow_sub_id,
373                data: serde_json::json!([hash_hex]),
374            }
375        }
376        TAG_CHAIN_HEAD_BODY_REQ => {
377            let _version_tag = r.read_u8()?;
378            let genesis_hash = r.read_var_bytes()?;
379            let follow_sub_id = r.read_string()?;
380            let hash = r.read_var_bytes()?;
381            let hash_hex = bytes_to_hex(&hash);
382            HostRequest::ChainHeadRequest {
383                tag,
384                genesis_hash,
385                follow_sub_id,
386                data: serde_json::json!([hash_hex]),
387            }
388        }
389        TAG_CHAIN_HEAD_STORAGE_REQ => {
390            let _version_tag = r.read_u8()?;
391            let genesis_hash = r.read_var_bytes()?;
392            let follow_sub_id = r.read_string()?;
393            let hash = r.read_var_bytes()?;
394            let hash_hex = bytes_to_hex(&hash);
395            // items: Vector(StorageQueryItem)
396            let item_count = r.read_compact_u32()?;
397            let mut items = Vec::new();
398            for _ in 0..item_count {
399                let key = r.read_var_bytes()?;
400                let storage_type = r.read_u8()?; // Status enum index
401                let type_str = match storage_type {
402                    0 => "value",
403                    1 => "hash",
404                    2 => "closestDescendantMerkleValue",
405                    3 => "descendantsValues",
406                    4 => "descendantsHashes",
407                    _ => "value",
408                };
409                items.push(serde_json::json!({
410                    "key": bytes_to_hex(&key),
411                    "type": type_str,
412                }));
413            }
414            // childTrie: Nullable(Hex()) = Option(var_bytes)
415            let child_trie = r.read_option(|r| r.read_var_bytes())?;
416            let child_trie_hex = child_trie.map(|b| bytes_to_hex(&b));
417            HostRequest::ChainHeadRequest {
418                tag,
419                genesis_hash,
420                follow_sub_id,
421                data: serde_json::json!([hash_hex, items, child_trie_hex]),
422            }
423        }
424        TAG_CHAIN_HEAD_CALL_REQ => {
425            let _version_tag = r.read_u8()?;
426            let genesis_hash = r.read_var_bytes()?;
427            let follow_sub_id = r.read_string()?;
428            let hash = r.read_var_bytes()?;
429            let function = r.read_string()?;
430            let call_params = r.read_var_bytes()?;
431            HostRequest::ChainHeadRequest {
432                tag,
433                genesis_hash,
434                follow_sub_id,
435                data: serde_json::json!([
436                    bytes_to_hex(&hash),
437                    function,
438                    bytes_to_hex(&call_params)
439                ]),
440            }
441        }
442        TAG_CHAIN_HEAD_UNPIN_REQ => {
443            let _version_tag = r.read_u8()?;
444            let genesis_hash = r.read_var_bytes()?;
445            let follow_sub_id = r.read_string()?;
446            // hashes: Vector(Hex())
447            let count = r.read_compact_u32()?;
448            let mut hashes = Vec::new();
449            for _ in 0..count {
450                let h = r.read_var_bytes()?;
451                hashes.push(serde_json::Value::String(bytes_to_hex(&h)));
452            }
453            HostRequest::ChainHeadRequest {
454                tag,
455                genesis_hash,
456                follow_sub_id,
457                data: serde_json::json!([hashes]),
458            }
459        }
460        TAG_CHAIN_HEAD_CONTINUE_REQ | TAG_CHAIN_HEAD_STOP_OP_REQ => {
461            let _version_tag = r.read_u8()?;
462            let genesis_hash = r.read_var_bytes()?;
463            let follow_sub_id = r.read_string()?;
464            let operation_id = r.read_string()?;
465            HostRequest::ChainHeadRequest {
466                tag,
467                genesis_hash,
468                follow_sub_id,
469                data: serde_json::json!([operation_id]),
470            }
471        }
472        TAG_CHAIN_SPEC_GENESIS_REQ | TAG_CHAIN_SPEC_NAME_REQ | TAG_CHAIN_SPEC_PROPS_REQ => {
473            let _version_tag = r.read_u8()?;
474            let genesis_hash = r.read_var_bytes()?;
475            HostRequest::ChainSpecRequest { tag, genesis_hash }
476        }
477        TAG_CHAIN_TX_BROADCAST_REQ => {
478            let _version_tag = r.read_u8()?;
479            let genesis_hash = r.read_var_bytes()?;
480            let transaction = r.read_var_bytes()?;
481            HostRequest::ChainTxBroadcast {
482                genesis_hash,
483                transaction,
484            }
485        }
486        TAG_CHAIN_TX_STOP_REQ => {
487            let _version_tag = r.read_u8()?;
488            let genesis_hash = r.read_var_bytes()?;
489            let operation_id = r.read_string()?;
490            HostRequest::ChainTxStop {
491                genesis_hash,
492                operation_id,
493            }
494        }
495        // Known tags we don't handle yet
496        TAG_DEVICE_PERMISSION_REQ
497        | TAG_REMOTE_PERMISSION_REQ
498        | TAG_ACCOUNT_GET_REQ
499        | TAG_ACCOUNT_GET_ALIAS_REQ
500        | TAG_ACCOUNT_CREATE_PROOF_REQ
501        | TAG_CREATE_TX_NON_PRODUCT_REQ
502        | TAG_CHAT_CREATE_ROOM_REQ
503        | TAG_CHAT_REGISTER_BOT_REQ
504        | TAG_CHAT_POST_MSG_REQ
505        | TAG_STATEMENT_PROOF_REQ
506        | TAG_STATEMENT_SUBMIT_REQ
507        | TAG_PREIMAGE_SUBMIT_REQ => HostRequest::Unimplemented { tag },
508        // Subscription stop/interrupt — fire-and-forget, no response needed
509        TAG_ACCOUNT_STATUS_STOP
510        | TAG_ACCOUNT_STATUS_INTERRUPT
511        | TAG_CHAT_LIST_STOP
512        | TAG_CHAT_LIST_INTERRUPT
513        | TAG_CHAT_ACTION_STOP
514        | TAG_CHAT_ACTION_INTERRUPT
515        | TAG_CHAT_CUSTOM_MSG_STOP
516        | TAG_CHAT_CUSTOM_MSG_INTERRUPT
517        | TAG_STATEMENT_STORE_STOP
518        | TAG_STATEMENT_STORE_INTERRUPT
519        | TAG_PREIMAGE_LOOKUP_STOP
520        | TAG_PREIMAGE_LOOKUP_INTERRUPT
521        | TAG_JSONRPC_SUB_STOP
522        | TAG_JSONRPC_SUB_INTERRUPT
523        | TAG_CHAIN_HEAD_FOLLOW_STOP
524        | TAG_CHAIN_HEAD_FOLLOW_INTERRUPT => HostRequest::Unimplemented { tag },
525        _ => HostRequest::Unknown { tag },
526    };
527
528    Ok((request_id, tag, req))
529}
530
531/// Encode a response into a wire message.
532pub fn encode_response(request_id: &str, request_tag: u8, response: &HostResponse) -> Vec<u8> {
533    let mut buf = Vec::with_capacity(128);
534
535    // requestId: SCALE string
536    encode_string(&mut buf, request_id);
537
538    match response {
539        HostResponse::HandshakeOk => {
540            // payload tag: host_handshake_response
541            encode_tag(&mut buf, TAG_HANDSHAKE_RESP);
542            // inner version tag: v1 = 0
543            encode_tag(&mut buf, 0);
544            // Result::Ok(void)
545            encode_result_ok_void(&mut buf);
546        }
547
548        HostResponse::AccountList(accounts) => {
549            // payload tag: host_get_non_product_accounts_response
550            encode_tag(&mut buf, TAG_GET_NON_PRODUCT_ACCOUNTS_RESP);
551            // inner version tag: v1 = 0
552            encode_tag(&mut buf, 0);
553            // Result::Ok(Vector(Account))
554            encode_result_ok(&mut buf);
555            // Vector: compact count + items
556            encode_vector_len(&mut buf, accounts.len() as u32);
557            for account in accounts {
558                // Account = Struct { publicKey: Bytes(), name: Option(str) }
559                // publicKey: dynamic bytes (compact len + raw)
560                encode_var_bytes(&mut buf, &account.public_key);
561                // name: Option(str)
562                match &account.name {
563                    None => encode_option_none(&mut buf),
564                    Some(name) => {
565                        encode_option_some(&mut buf);
566                        encode_string(&mut buf, name);
567                    }
568                }
569            }
570        }
571
572        HostResponse::Error(_reason) => {
573            let resp_tag = response_tag_for(request_tag);
574            encode_tag(&mut buf, resp_tag);
575            encode_tag(&mut buf, 0); // v1
576            encode_result_err(&mut buf);
577            // GenericError / Unknown error variant (last in the enum)
578            // Error = Struct { reason: str }
579            // The exact error enum index depends on the method.
580            // Use variant 0 (first error kind) as a generic rejection.
581            encode_tag(&mut buf, 0);
582        }
583    }
584
585    buf
586}
587
588/// Given a request tag, return the corresponding response tag.
589/// Only valid for request/response pairs (even tags where tag+1 is the response).
590/// Panics for subscription tags — those use dedicated encode functions.
591fn response_tag_for(request_tag: u8) -> u8 {
592    assert!(
593        !matches!(
594            request_tag,
595            TAG_ACCOUNT_STATUS_START
596                | TAG_CHAT_LIST_START
597                | TAG_CHAT_ACTION_START
598                | TAG_CHAT_CUSTOM_MSG_START
599                | TAG_STATEMENT_STORE_START
600                | TAG_PREIMAGE_LOOKUP_START
601                | TAG_JSONRPC_SUB_START
602                | TAG_CHAIN_HEAD_FOLLOW_START
603        ),
604        "response_tag_for called with subscription start tag {request_tag}"
605    );
606    request_tag + 1
607}
608
609/// Encode a feature_supported response (Result::Ok(bool)).
610pub fn encode_feature_response(request_id: &str, supported: bool) -> Vec<u8> {
611    let mut buf = Vec::with_capacity(32);
612    encode_string(&mut buf, request_id);
613    encode_tag(&mut buf, TAG_FEATURE_SUPPORTED_RESP);
614    encode_tag(&mut buf, 0); // v1
615    encode_result_ok(&mut buf);
616    buf.push(if supported { 1 } else { 0 }); // bool as u8
617    buf
618}
619
620/// Encode an account_connection_status_receive message.
621pub fn encode_account_status(request_id: &str, connected: bool) -> Vec<u8> {
622    let mut buf = Vec::with_capacity(32);
623    encode_string(&mut buf, request_id);
624    encode_tag(&mut buf, TAG_ACCOUNT_STATUS_RECEIVE);
625    encode_tag(&mut buf, 0); // v1
626                             // Status enum: 0 = disconnected, 1 = connected
627    encode_tag(&mut buf, if connected { 1 } else { 0 });
628    buf
629}
630
631/// Encode a local_storage_read response.
632pub fn encode_storage_read_response(request_id: &str, value: Option<&[u8]>) -> Vec<u8> {
633    let mut buf = Vec::with_capacity(64);
634    encode_string(&mut buf, request_id);
635    encode_tag(&mut buf, TAG_LOCAL_STORAGE_READ_RESP);
636    encode_tag(&mut buf, 0); // v1
637    encode_result_ok(&mut buf);
638    match value {
639        None => encode_option_none(&mut buf),
640        Some(v) => {
641            encode_option_some(&mut buf);
642            encode_var_bytes(&mut buf, v);
643        }
644    }
645    buf
646}
647
648/// Encode a local_storage_write/clear response (Result::Ok(void)).
649pub fn encode_storage_write_response(request_id: &str, is_clear: bool) -> Vec<u8> {
650    let mut buf = Vec::with_capacity(32);
651    encode_string(&mut buf, request_id);
652    let tag = if is_clear {
653        TAG_LOCAL_STORAGE_CLEAR_RESP
654    } else {
655        TAG_LOCAL_STORAGE_WRITE_RESP
656    };
657    encode_tag(&mut buf, tag);
658    encode_tag(&mut buf, 0); // v1
659    encode_result_ok_void(&mut buf);
660    buf
661}
662
663/// Encode a navigate_to response (Result::Ok(void)).
664pub fn encode_navigate_response(request_id: &str) -> Vec<u8> {
665    let mut buf = Vec::with_capacity(32);
666    encode_string(&mut buf, request_id);
667    encode_tag(&mut buf, TAG_NAVIGATE_TO_RESP);
668    encode_tag(&mut buf, 0); // v1
669    encode_result_ok_void(&mut buf);
670    buf
671}
672
673/// Encode a push_notification response (Result::Ok(void)).
674pub fn encode_push_notification_response(request_id: &str) -> Vec<u8> {
675    let mut buf = Vec::with_capacity(32);
676    encode_string(&mut buf, request_id);
677    encode_tag(&mut buf, TAG_PUSH_NOTIFICATION_RESP);
678    encode_tag(&mut buf, 0); // v1
679    encode_result_ok_void(&mut buf);
680    buf
681}
682
683/// Encode a sign_payload or sign_raw success response.
684/// Result::Ok { id: u32, signature: Bytes }
685pub fn encode_sign_response(request_id: &str, is_raw: bool, signature: &[u8]) -> Vec<u8> {
686    let mut buf = Vec::with_capacity(128);
687    encode_string(&mut buf, request_id);
688    encode_tag(
689        &mut buf,
690        if is_raw {
691            TAG_SIGN_RAW_RESP
692        } else {
693            TAG_SIGN_PAYLOAD_RESP
694        },
695    );
696    encode_tag(&mut buf, 0); // v1
697    encode_result_ok(&mut buf);
698    encode_compact_u32(&mut buf, 0); // id = 0
699    encode_var_bytes(&mut buf, signature);
700    buf
701}
702
703/// Encode a `host_jsonrpc_send` response.
704///
705/// The response wraps the JSON-RPC result (or error) as a SCALE string inside
706/// the standard `Result<String, Error>` envelope.
707pub fn encode_jsonrpc_send_response(request_id: &str, json_rpc_result: &str) -> Vec<u8> {
708    let mut buf = Vec::with_capacity(64 + json_rpc_result.len());
709    encode_string(&mut buf, request_id);
710    encode_tag(&mut buf, TAG_JSONRPC_SEND_RESP);
711    encode_tag(&mut buf, 0); // v1
712    encode_result_ok(&mut buf);
713    encode_string(&mut buf, json_rpc_result);
714    buf
715}
716
717/// Encode a `host_jsonrpc_send` error response.
718pub fn encode_jsonrpc_send_error(request_id: &str) -> Vec<u8> {
719    let mut buf = Vec::with_capacity(32);
720    encode_string(&mut buf, request_id);
721    encode_tag(&mut buf, TAG_JSONRPC_SEND_RESP);
722    encode_tag(&mut buf, 0); // v1
723    encode_result_err(&mut buf);
724    encode_tag(&mut buf, 0); // error variant 0
725    buf
726}
727
728/// Encode a `host_jsonrpc_subscribe` receive message.
729///
730/// Pushes a JSON-RPC message (initial response or notification) to the app
731/// for an active subscription. Uses the same request_id from the original
732/// `JsonRpcSubscribeStart`.
733pub fn encode_jsonrpc_sub_receive(request_id: &str, json_rpc_msg: &str) -> Vec<u8> {
734    let mut buf = Vec::with_capacity(64 + json_rpc_msg.len());
735    encode_string(&mut buf, request_id);
736    encode_tag(&mut buf, TAG_JSONRPC_SUB_RECEIVE);
737    encode_tag(&mut buf, 0); // v1
738    encode_string(&mut buf, json_rpc_msg);
739    buf
740}
741
742/// Encode a sign_payload or sign_raw error response (user rejected, wallet locked, etc).
743pub fn encode_sign_error(request_id: &str, is_raw: bool) -> Vec<u8> {
744    let mut buf = Vec::with_capacity(32);
745    encode_string(&mut buf, request_id);
746    encode_tag(
747        &mut buf,
748        if is_raw {
749            TAG_SIGN_RAW_RESP
750        } else {
751            TAG_SIGN_PAYLOAD_RESP
752        },
753    );
754    encode_tag(&mut buf, 0); // v1
755    encode_result_err(&mut buf);
756    encode_tag(&mut buf, 0); // error variant 0 = Rejected
757    buf
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763
764    #[test]
765    fn decode_handshake_request() {
766        // Manually encode: requestId="test", tag=0, v1=0, version=1
767        let mut msg = Vec::new();
768        encode_string(&mut msg, "test");
769        msg.push(TAG_HANDSHAKE_REQ); // payload tag
770        msg.push(0); // v1 tag
771        msg.push(PROTOCOL_VERSION); // version value
772
773        let (id, tag, req) = decode_message(&msg).unwrap();
774        assert_eq!(id, "test");
775        assert_eq!(tag, TAG_HANDSHAKE_REQ);
776        match req {
777            HostRequest::Handshake { version } => assert_eq!(version, 1),
778            _ => panic!("expected Handshake"),
779        }
780    }
781
782    #[test]
783    fn encode_handshake_response() {
784        let resp = encode_response("test", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
785
786        // Decode and verify structure
787        let mut r = Reader::new(&resp);
788        let id = r.read_string().unwrap();
789        assert_eq!(id, "test");
790        let tag = r.read_u8().unwrap();
791        assert_eq!(tag, TAG_HANDSHAKE_RESP);
792        let v1_tag = r.read_u8().unwrap();
793        assert_eq!(v1_tag, 0);
794        let result_ok = r.read_u8().unwrap();
795        assert_eq!(result_ok, 0x00); // Ok
796        assert_eq!(r.pos, resp.len()); // no trailing bytes
797    }
798
799    #[test]
800    fn decode_get_non_product_accounts() {
801        let mut msg = Vec::new();
802        encode_string(&mut msg, "req-42");
803        msg.push(TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
804        msg.push(0); // v1
805
806        let (id, tag, req) = decode_message(&msg).unwrap();
807        assert_eq!(id, "req-42");
808        assert_eq!(tag, TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
809        assert!(matches!(req, HostRequest::GetNonProductAccounts));
810    }
811
812    #[test]
813    fn encode_account_list_response() {
814        let accounts = vec![
815            Account {
816                public_key: vec![0xd4; 32],
817                name: Some("Alice".into()),
818            },
819            Account {
820                public_key: vec![0x8e; 32],
821                name: None,
822            },
823        ];
824        let resp = encode_response(
825            "req-42",
826            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
827            &HostResponse::AccountList(accounts),
828        );
829
830        // Decode and verify structure
831        let mut r = Reader::new(&resp);
832        let id = r.read_string().unwrap();
833        assert_eq!(id, "req-42");
834        let tag = r.read_u8().unwrap();
835        assert_eq!(tag, TAG_GET_NON_PRODUCT_ACCOUNTS_RESP);
836        let v1 = r.read_u8().unwrap();
837        assert_eq!(v1, 0); // v1
838        let result = r.read_u8().unwrap();
839        assert_eq!(result, 0x00); // Ok
840        let count = r.read_compact_u32().unwrap();
841        assert_eq!(count, 2);
842
843        // Account 1: pubkey + Some("Alice")
844        let pk1 = r.read_var_bytes().unwrap();
845        assert_eq!(pk1.len(), 32);
846        assert_eq!(pk1[0], 0xd4);
847        let name1 = r.read_option(|r| r.read_string()).unwrap();
848        assert_eq!(name1.as_deref(), Some("Alice"));
849
850        // Account 2: pubkey + None
851        let pk2 = r.read_var_bytes().unwrap();
852        assert_eq!(pk2.len(), 32);
853        assert_eq!(pk2[0], 0x8e);
854        let name2 = r.read_option(|r| r.read_string()).unwrap();
855        assert!(name2.is_none());
856
857        assert_eq!(r.pos, resp.len());
858    }
859
860    #[test]
861    fn handshake_round_trip() {
862        // Simulate: app sends handshake request, host responds
863        let mut req_msg = Vec::new();
864        encode_string(&mut req_msg, "hsk-1");
865        req_msg.push(TAG_HANDSHAKE_REQ);
866        req_msg.push(0); // v1
867        req_msg.push(PROTOCOL_VERSION);
868
869        let (id, tag, req) = decode_message(&req_msg).unwrap();
870        assert!(matches!(req, HostRequest::Handshake { version: 1 }));
871
872        let resp_bytes = encode_response(&id, tag, &HostResponse::HandshakeOk);
873
874        // Verify the response can be decoded
875        let mut r = Reader::new(&resp_bytes);
876        assert_eq!(r.read_string().unwrap(), "hsk-1");
877        assert_eq!(r.read_u8().unwrap(), TAG_HANDSHAKE_RESP);
878        assert_eq!(r.read_u8().unwrap(), 0); // v1
879        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
880    }
881
882    // -------------------------------------------------------------------
883    // Golden byte vectors — hand-verified against SCALE spec.
884    //
885    // Format: requestId (compact_len + UTF-8), tag (u8), version (u8), payload.
886    // These catch accidental encoding drift.
887    // -------------------------------------------------------------------
888
889    #[test]
890    fn golden_handshake_request() {
891        // requestId = "t1" (compact_len=8, bytes "t1"), tag=0, v1=0, version=1
892        let expected: &[u8] = &[
893            0x08, b't', b'1', // compact(2) + "t1"
894            0x00, // TAG_HANDSHAKE_REQ
895            0x00, // v1
896            0x01, // version = 1
897        ];
898        let mut built = Vec::new();
899        encode_string(&mut built, "t1");
900        built.push(TAG_HANDSHAKE_REQ);
901        built.push(0);
902        built.push(PROTOCOL_VERSION);
903        assert_eq!(built, expected);
904    }
905
906    #[test]
907    fn golden_handshake_response_ok() {
908        let resp = encode_response("t1", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
909        let expected: &[u8] = &[
910            0x08, b't', b'1', // compact(2) + "t1"
911            0x01, // TAG_HANDSHAKE_RESP
912            0x00, // v1
913            0x00, // Result::Ok
914        ];
915        assert_eq!(resp, expected);
916    }
917
918    #[test]
919    fn golden_get_accounts_request() {
920        let expected: &[u8] = &[
921            0x08, b'a', b'1', // compact(2) + "a1"
922            28,   // TAG_GET_NON_PRODUCT_ACCOUNTS_REQ
923            0x00, // v1
924        ];
925        let mut built = Vec::new();
926        encode_string(&mut built, "a1");
927        built.push(TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
928        built.push(0);
929        assert_eq!(built, expected);
930    }
931
932    #[test]
933    fn golden_get_accounts_response_empty() {
934        let resp = encode_response(
935            "a1",
936            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
937            &HostResponse::AccountList(vec![]),
938        );
939        let expected: &[u8] = &[
940            0x08, b'a', b'1', // compact(2) + "a1"
941            29,   // TAG_GET_NON_PRODUCT_ACCOUNTS_RESP
942            0x00, // v1
943            0x00, // Result::Ok
944            0x00, // Vector len = 0
945        ];
946        assert_eq!(resp, expected);
947    }
948
949    #[test]
950    fn golden_storage_write_response() {
951        let resp = encode_storage_write_response("s1", false);
952        let expected: &[u8] = &[
953            0x08, b's', b'1', // compact(2) + "s1"
954            15,   // TAG_LOCAL_STORAGE_WRITE_RESP
955            0x00, // v1
956            0x00, // Result::Ok(void)
957        ];
958        assert_eq!(resp, expected);
959    }
960
961    #[test]
962    fn golden_storage_clear_response() {
963        let resp = encode_storage_write_response("s1", true);
964        let expected: &[u8] = &[
965            0x08, b's', b'1', // compact(2) + "s1"
966            17,   // TAG_LOCAL_STORAGE_CLEAR_RESP
967            0x00, // v1
968            0x00, // Result::Ok(void)
969        ];
970        assert_eq!(resp, expected);
971    }
972
973    #[test]
974    fn golden_feature_supported_response() {
975        let resp = encode_feature_response("f1", false);
976        let expected: &[u8] = &[
977            0x08, b'f', b'1', // compact(2) + "f1"
978            3,    // TAG_FEATURE_SUPPORTED_RESP
979            0x00, // v1
980            0x00, // Result::Ok
981            0x00, // false
982        ];
983        assert_eq!(resp, expected);
984    }
985
986    #[test]
987    fn golden_account_status_receive() {
988        let resp = encode_account_status("c1", true);
989        let expected: &[u8] = &[
990            0x08, b'c', b'1', // compact(2) + "c1"
991            21,   // TAG_ACCOUNT_STATUS_RECEIVE
992            0x00, // v1
993            0x01, // connected = true
994        ];
995        assert_eq!(resp, expected);
996    }
997
998    #[test]
999    fn golden_sign_payload_response_ok() {
1000        let sig = [0xAB; 64];
1001        let resp = encode_sign_response("s1", false, &sig);
1002        let mut r = Reader::new(&resp);
1003        assert_eq!(r.read_string().unwrap(), "s1");
1004        assert_eq!(r.read_u8().unwrap(), TAG_SIGN_PAYLOAD_RESP);
1005        assert_eq!(r.read_u8().unwrap(), 0); // v1
1006        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
1007        assert_eq!(r.read_compact_u32().unwrap(), 0); // id
1008        let sig_bytes = r.read_var_bytes().unwrap();
1009        assert_eq!(sig_bytes, vec![0xAB; 64]);
1010        assert_eq!(r.pos, resp.len());
1011    }
1012
1013    #[test]
1014    fn golden_sign_raw_response_ok() {
1015        let sig = [0xCD; 64];
1016        let resp = encode_sign_response("s2", true, &sig);
1017        let mut r = Reader::new(&resp);
1018        assert_eq!(r.read_string().unwrap(), "s2");
1019        assert_eq!(r.read_u8().unwrap(), TAG_SIGN_RAW_RESP);
1020        assert_eq!(r.read_u8().unwrap(), 0); // v1
1021        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
1022        assert_eq!(r.read_compact_u32().unwrap(), 0); // id
1023        let sig_bytes = r.read_var_bytes().unwrap();
1024        assert_eq!(sig_bytes, vec![0xCD; 64]);
1025    }
1026
1027    #[test]
1028    fn golden_sign_error_response() {
1029        let resp = encode_sign_error("s3", false);
1030        let expected: &[u8] = &[
1031            0x08, b's', b'3', // compact(2) + "s3"
1032            37,   // TAG_SIGN_PAYLOAD_RESP
1033            0x00, // v1
1034            0x01, // Result::Err
1035            0x00, // Rejected variant
1036        ];
1037        assert_eq!(resp, expected);
1038    }
1039
1040    #[test]
1041    fn decode_sign_payload_request() {
1042        let mut msg = Vec::new();
1043        encode_string(&mut msg, "sign-1");
1044        msg.push(TAG_SIGN_PAYLOAD_REQ);
1045        msg.push(0); // v1
1046        encode_var_bytes(&mut msg, &[0xAA; 32]); // publicKey
1047        msg.extend_from_slice(b"extrinsic-payload"); // payload
1048        let (id, tag, req) = decode_message(&msg).unwrap();
1049        assert_eq!(id, "sign-1");
1050        assert_eq!(tag, TAG_SIGN_PAYLOAD_REQ);
1051        match req {
1052            HostRequest::SignPayload {
1053                public_key,
1054                payload,
1055            } => {
1056                assert_eq!(public_key, vec![0xAA; 32]);
1057                assert_eq!(payload, b"extrinsic-payload");
1058            }
1059            _ => panic!("expected SignPayload"),
1060        }
1061    }
1062
1063    #[test]
1064    fn decode_sign_raw_request() {
1065        let mut msg = Vec::new();
1066        encode_string(&mut msg, "sign-2");
1067        msg.push(TAG_SIGN_RAW_REQ);
1068        msg.push(0); // v1
1069        encode_var_bytes(&mut msg, &[0xBB; 32]); // publicKey
1070        msg.extend_from_slice(b"raw-data"); // data
1071        let (id, tag, req) = decode_message(&msg).unwrap();
1072        assert_eq!(id, "sign-2");
1073        assert_eq!(tag, TAG_SIGN_RAW_REQ);
1074        match req {
1075            HostRequest::SignRaw { public_key, data } => {
1076                assert_eq!(public_key, vec![0xBB; 32]);
1077                assert_eq!(data, b"raw-data");
1078            }
1079            _ => panic!("expected SignRaw"),
1080        }
1081    }
1082
1083    // -- Push notification protocol tests --
1084
1085    #[test]
1086    fn decode_push_notification_request_with_deeplink() {
1087        let mut msg = Vec::new();
1088        encode_string(&mut msg, "pn-1");
1089        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1090        msg.push(0); // v1
1091        encode_string(&mut msg, "Transfer complete");
1092        msg.push(0x01); // Some
1093        encode_string(&mut msg, "https://app.example/tx/123");
1094
1095        let (id, tag, req) = decode_message(&msg).unwrap();
1096        assert_eq!(id, "pn-1");
1097        assert_eq!(tag, TAG_PUSH_NOTIFICATION_REQ);
1098        match req {
1099            HostRequest::PushNotification { text, deeplink } => {
1100                assert_eq!(text, "Transfer complete");
1101                assert_eq!(deeplink.as_deref(), Some("https://app.example/tx/123"));
1102            }
1103            _ => panic!("expected PushNotification"),
1104        }
1105    }
1106
1107    #[test]
1108    fn decode_push_notification_request_without_deeplink() {
1109        let mut msg = Vec::new();
1110        encode_string(&mut msg, "pn-2");
1111        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1112        msg.push(0); // v1
1113        encode_string(&mut msg, "Hello world");
1114        msg.push(0x00); // None
1115
1116        let (id, tag, req) = decode_message(&msg).unwrap();
1117        assert_eq!(id, "pn-2");
1118        assert_eq!(tag, TAG_PUSH_NOTIFICATION_REQ);
1119        match req {
1120            HostRequest::PushNotification { text, deeplink } => {
1121                assert_eq!(text, "Hello world");
1122                assert!(deeplink.is_none());
1123            }
1124            _ => panic!("expected PushNotification"),
1125        }
1126    }
1127
1128    #[test]
1129    fn encode_push_notification_response_produces_ok() {
1130        let resp = encode_push_notification_response("pn-1");
1131        let mut r = Reader::new(&resp);
1132        assert_eq!(r.read_string().unwrap(), "pn-1");
1133        assert_eq!(r.read_u8().unwrap(), TAG_PUSH_NOTIFICATION_RESP);
1134        assert_eq!(r.read_u8().unwrap(), 0); // v1
1135        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok(void)
1136        assert_eq!(r.pos, resp.len());
1137    }
1138
1139    #[test]
1140    fn golden_push_notification_response_ok() {
1141        let resp = encode_push_notification_response("t1");
1142        let expected: &[u8] = &[
1143            0x08, b't', b'1', // compact(2) + "t1"
1144            0x05, // TAG_PUSH_NOTIFICATION_RESP
1145            0x00, // v1
1146            0x00, // Result::Ok(void)
1147        ];
1148        assert_eq!(resp, expected);
1149    }
1150
1151    #[test]
1152    fn push_notification_round_trip() {
1153        let mut req_msg = Vec::new();
1154        encode_string(&mut req_msg, "pn-rt");
1155        req_msg.push(TAG_PUSH_NOTIFICATION_REQ);
1156        req_msg.push(0); // v1
1157        encode_string(&mut req_msg, "Test notification");
1158        req_msg.push(0x00); // None deeplink
1159
1160        let (id, _tag, req) = decode_message(&req_msg).unwrap();
1161        assert!(matches!(req, HostRequest::PushNotification { .. }));
1162
1163        let resp_bytes = encode_push_notification_response(&id);
1164
1165        let mut r = Reader::new(&resp_bytes);
1166        assert_eq!(r.read_string().unwrap(), "pn-rt");
1167        assert_eq!(r.read_u8().unwrap(), TAG_PUSH_NOTIFICATION_RESP);
1168        assert_eq!(r.read_u8().unwrap(), 0); // v1
1169        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok(void)
1170    }
1171
1172    #[test]
1173    fn golden_storage_read_response_some() {
1174        let resp = encode_storage_read_response("s1", Some(b"hi"));
1175        let expected: &[u8] = &[
1176            0x08, b's', b'1', // compact(2) + "s1"
1177            13,   // TAG_LOCAL_STORAGE_READ_RESP
1178            0x00, // v1
1179            0x00, // Result::Ok
1180            0x01, // Option::Some
1181            0x08, b'h', b'i', // compact(2) + "hi"
1182        ];
1183        assert_eq!(resp, expected);
1184    }
1185
1186    #[test]
1187    fn golden_storage_read_response_none() {
1188        let resp = encode_storage_read_response("s1", None);
1189        let expected: &[u8] = &[
1190            0x08, b's', b'1', // compact(2) + "s1"
1191            13,   // TAG_LOCAL_STORAGE_READ_RESP
1192            0x00, // v1
1193            0x00, // Result::Ok
1194            0x00, // Option::None
1195        ];
1196        assert_eq!(resp, expected);
1197    }
1198    #[test]
1199    fn decode_push_notification_truncated_missing_deeplink() {
1200        let mut msg = Vec::new();
1201        encode_string(&mut msg, "pn-trunc");
1202        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1203        msg.push(0); // v1
1204        encode_string(&mut msg, "Hello");
1205        // Missing: Option byte for deeplink
1206
1207        assert!(decode_message(&msg).is_err());
1208    }
1209}