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/// Magic byte prepended to every host-sdk wire message to distinguish it from
142/// other protocols (e.g. Nova) on the same `postMessage` channel.
143///
144/// This is a **routing hint**, not an authentication mechanism — any sender can
145/// prepend this byte. The host uses it solely to decide which decoder to invoke.
146///
147/// The value `0x01` was chosen because it is never the first byte of a legacy
148/// (non-discriminated) message in practice: under SCALE compact encoding `0x01`
149/// indicates mode 1 (two-byte compact), meaning a requestId string of 64+
150/// characters — far longer than any real-world requestId.
151pub const PROTOCOL_DISCRIMINATOR: u8 = 0x01;
152
153/// Returns `true` if `data` starts with the host-sdk protocol discriminator.
154///
155/// Use this to route incoming raw messages before calling [`decode_message`]:
156/// if `true`, pass the full buffer to `decode_message`; otherwise, forward to
157/// an alternative protocol handler (e.g. Nova).
158pub fn is_host_sdk_message(data: &[u8]) -> bool {
159    data.first() == Some(&PROTOCOL_DISCRIMINATOR)
160}
161
162// ---------------------------------------------------------------------------
163// High-level types
164// ---------------------------------------------------------------------------
165
166/// An account returned by host_get_non_product_accounts.
167#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
168pub struct Account {
169    /// Raw public key bytes (typically 32 bytes for sr25519/ed25519).
170    pub public_key: Vec<u8>,
171    /// Optional display name.
172    pub name: Option<String>,
173}
174
175/// Decoded incoming request from the app.
176#[derive(Debug)]
177pub enum HostRequest {
178    Handshake {
179        version: u8,
180    },
181    GetNonProductAccounts,
182    FeatureSupported {
183        feature_data: Vec<u8>,
184    },
185    LocalStorageRead {
186        key: String,
187    },
188    LocalStorageWrite {
189        key: String,
190        value: Vec<u8>,
191    },
192    LocalStorageClear {
193        key: String,
194    },
195    SignPayload {
196        public_key: Vec<u8>,
197        payload: Vec<u8>,
198    },
199    SignRaw {
200        public_key: Vec<u8>,
201        data: Vec<u8>,
202    },
203    CreateTransaction {
204        payload: Vec<u8>,
205    },
206    NavigateTo {
207        url: String,
208    },
209    PushNotification {
210        text: String,
211        deeplink: Option<String>,
212    },
213    AccountConnectionStatusStart,
214    JsonRpcSend {
215        data: Vec<u8>,
216    },
217    JsonRpcSubscribeStart {
218        data: Vec<u8>,
219    },
220    /// chainHead_v1_follow start: genesis hash + withRuntime flag
221    ChainHeadFollowStart {
222        genesis_hash: Vec<u8>,
223        with_runtime: bool,
224    },
225    /// chainHead request with genesis hash, follow subscription ID, and block hash.
226    /// Used for header (tag 80), body (82), unpin (88 - hashes instead of hash).
227    ChainHeadRequest {
228        tag: u8,
229        genesis_hash: Vec<u8>,
230        follow_sub_id: String,
231        data: serde_json::Value,
232    },
233    /// chainSpec request with just genesis hash (tags 94, 96, 98).
234    ChainSpecRequest {
235        tag: u8,
236        genesis_hash: Vec<u8>,
237    },
238    /// Transaction broadcast (tag 100): genesis hash + transaction hex bytes.
239    ChainTxBroadcast {
240        genesis_hash: Vec<u8>,
241        transaction: Vec<u8>,
242    },
243    /// Transaction stop (tag 102): genesis hash + operation ID.
244    ChainTxStop {
245        genesis_hash: Vec<u8>,
246        operation_id: String,
247    },
248    /// A request we recognize by tag but don't handle yet.
249    Unimplemented {
250        tag: u8,
251    },
252    /// A tag we don't recognize at all.
253    Unknown {
254        tag: u8,
255    },
256}
257
258/// Outgoing response to the app.
259#[derive(Debug)]
260pub enum HostResponse {
261    HandshakeOk,
262    AccountList(Vec<Account>),
263    Error(String),
264}
265
266// ---------------------------------------------------------------------------
267// Wire message decode / encode
268// ---------------------------------------------------------------------------
269
270/// Encode raw bytes as a 0x-prefixed lowercase hex string.
271fn bytes_to_hex(bytes: &[u8]) -> String {
272    let mut s = String::with_capacity(2 + bytes.len() * 2);
273    s.push_str("0x");
274    for b in bytes {
275        s.push_str(&format!("{b:02x}"));
276    }
277    s
278}
279
280/// Write the protocol discriminator + requestId into `buf`.
281///
282/// Parallel to `chain::encode_envelope` — use this in every `encode_*`
283/// function in this module so the discriminator is never accidentally omitted.
284fn begin_message(buf: &mut Vec<u8>, request_id: &str) {
285    buf.push(PROTOCOL_DISCRIMINATOR);
286    encode_string(buf, request_id);
287}
288
289/// Decode a raw binary message into (request_id, HostRequest).
290///
291/// The message must start with [`PROTOCOL_DISCRIMINATOR`] (`0x01`).
292/// Returns [`DecodeErr::UnknownProtocol`] if the first byte does not match.
293pub fn decode_message(data: &[u8]) -> Result<(String, u8, HostRequest), DecodeErr> {
294    let mut r = Reader::new(data);
295
296    // protocol discriminator
297    let disc = r.read_u8()?;
298    if disc != PROTOCOL_DISCRIMINATOR {
299        return Err(DecodeErr::UnknownProtocol);
300    }
301
302    // requestId: SCALE string
303    let request_id = r.read_string()?;
304
305    // payload: enum tag (u8) + inner bytes
306    let tag = r.read_u8()?;
307
308    let req = match tag {
309        TAG_HANDSHAKE_REQ => {
310            // inner: Enum { v1: u8 }
311            let _version_tag = r.read_u8()?; // 0 = "v1"
312            let version = r.read_u8()?;
313            HostRequest::Handshake { version }
314        }
315        TAG_GET_NON_PRODUCT_ACCOUNTS_REQ => {
316            // inner: Enum { v1: void }
317            let _version_tag = r.read_u8()?;
318            // void = no more bytes
319            HostRequest::GetNonProductAccounts
320        }
321        TAG_FEATURE_SUPPORTED_REQ => {
322            let _version_tag = r.read_u8()?;
323            let rest = r.remaining().to_vec();
324            r.skip_rest();
325            HostRequest::FeatureSupported { feature_data: rest }
326        }
327        TAG_LOCAL_STORAGE_READ_REQ => {
328            let _version_tag = r.read_u8()?;
329            let key = r.read_string()?;
330            HostRequest::LocalStorageRead { key }
331        }
332        TAG_LOCAL_STORAGE_WRITE_REQ => {
333            let _version_tag = r.read_u8()?;
334            let key = r.read_string()?;
335            let value = r.read_var_bytes()?;
336            HostRequest::LocalStorageWrite { key, value }
337        }
338        TAG_LOCAL_STORAGE_CLEAR_REQ => {
339            let _version_tag = r.read_u8()?;
340            let key = r.read_string()?;
341            HostRequest::LocalStorageClear { key }
342        }
343        TAG_SIGN_PAYLOAD_REQ => {
344            let _version_tag = r.read_u8()?;
345            let public_key = r.read_var_bytes()?;
346            let payload = r.remaining().to_vec();
347            r.skip_rest();
348            HostRequest::SignPayload {
349                public_key,
350                payload,
351            }
352        }
353        TAG_SIGN_RAW_REQ => {
354            let _version_tag = r.read_u8()?;
355            let public_key = r.read_var_bytes()?;
356            let data = r.remaining().to_vec();
357            r.skip_rest();
358            HostRequest::SignRaw { public_key, data }
359        }
360        TAG_CREATE_TRANSACTION_REQ => {
361            let _version_tag = r.read_u8()?;
362            let rest = r.remaining().to_vec();
363            r.skip_rest();
364            HostRequest::CreateTransaction { payload: rest }
365        }
366        TAG_NAVIGATE_TO_REQ => {
367            let _version_tag = r.read_u8()?;
368            let url = r.read_string()?;
369            HostRequest::NavigateTo { url }
370        }
371        TAG_PUSH_NOTIFICATION_REQ => {
372            let _version_tag = r.read_u8()?;
373            let text = r.read_string()?;
374            let deeplink = r.read_option(|r| r.read_string())?;
375            HostRequest::PushNotification { text, deeplink }
376        }
377        TAG_ACCOUNT_STATUS_START => {
378            let _version_tag = r.read_u8()?;
379            HostRequest::AccountConnectionStatusStart
380        }
381        TAG_JSONRPC_SEND_REQ => {
382            let _version_tag = r.read_u8()?;
383            let rest = r.remaining().to_vec();
384            r.skip_rest();
385            HostRequest::JsonRpcSend { data: rest }
386        }
387        TAG_JSONRPC_SUB_START => {
388            let _version_tag = r.read_u8()?;
389            let rest = r.remaining().to_vec();
390            r.skip_rest();
391            HostRequest::JsonRpcSubscribeStart { data: rest }
392        }
393        TAG_CHAIN_HEAD_FOLLOW_START => {
394            let _version_tag = r.read_u8()?;
395            let genesis_hash = r.read_var_bytes()?;
396            let with_runtime = r.read_u8()? != 0;
397            HostRequest::ChainHeadFollowStart {
398                genesis_hash,
399                with_runtime,
400            }
401        }
402        TAG_CHAIN_HEAD_HEADER_REQ => {
403            let _version_tag = r.read_u8()?;
404            let genesis_hash = r.read_var_bytes()?;
405            let follow_sub_id = r.read_string()?;
406            let hash = r.read_var_bytes()?;
407            let hash_hex = bytes_to_hex(&hash);
408            HostRequest::ChainHeadRequest {
409                tag,
410                genesis_hash,
411                follow_sub_id,
412                data: serde_json::json!([hash_hex]),
413            }
414        }
415        TAG_CHAIN_HEAD_BODY_REQ => {
416            let _version_tag = r.read_u8()?;
417            let genesis_hash = r.read_var_bytes()?;
418            let follow_sub_id = r.read_string()?;
419            let hash = r.read_var_bytes()?;
420            let hash_hex = bytes_to_hex(&hash);
421            HostRequest::ChainHeadRequest {
422                tag,
423                genesis_hash,
424                follow_sub_id,
425                data: serde_json::json!([hash_hex]),
426            }
427        }
428        TAG_CHAIN_HEAD_STORAGE_REQ => {
429            let _version_tag = r.read_u8()?;
430            let genesis_hash = r.read_var_bytes()?;
431            let follow_sub_id = r.read_string()?;
432            let hash = r.read_var_bytes()?;
433            let hash_hex = bytes_to_hex(&hash);
434            // items: Vector(StorageQueryItem)
435            let item_count = r.read_compact_u32()?;
436            let mut items = Vec::new();
437            for _ in 0..item_count {
438                let key = r.read_var_bytes()?;
439                let storage_type = r.read_u8()?; // Status enum index
440                let type_str = match storage_type {
441                    0 => "value",
442                    1 => "hash",
443                    2 => "closestDescendantMerkleValue",
444                    3 => "descendantsValues",
445                    4 => "descendantsHashes",
446                    _ => "value",
447                };
448                items.push(serde_json::json!({
449                    "key": bytes_to_hex(&key),
450                    "type": type_str,
451                }));
452            }
453            // childTrie: Nullable(Hex()) = Option(var_bytes)
454            let child_trie = r.read_option(|r| r.read_var_bytes())?;
455            let child_trie_hex = child_trie.map(|b| bytes_to_hex(&b));
456            HostRequest::ChainHeadRequest {
457                tag,
458                genesis_hash,
459                follow_sub_id,
460                data: serde_json::json!([hash_hex, items, child_trie_hex]),
461            }
462        }
463        TAG_CHAIN_HEAD_CALL_REQ => {
464            let _version_tag = r.read_u8()?;
465            let genesis_hash = r.read_var_bytes()?;
466            let follow_sub_id = r.read_string()?;
467            let hash = r.read_var_bytes()?;
468            let function = r.read_string()?;
469            let call_params = r.read_var_bytes()?;
470            HostRequest::ChainHeadRequest {
471                tag,
472                genesis_hash,
473                follow_sub_id,
474                data: serde_json::json!([
475                    bytes_to_hex(&hash),
476                    function,
477                    bytes_to_hex(&call_params)
478                ]),
479            }
480        }
481        TAG_CHAIN_HEAD_UNPIN_REQ => {
482            let _version_tag = r.read_u8()?;
483            let genesis_hash = r.read_var_bytes()?;
484            let follow_sub_id = r.read_string()?;
485            // hashes: Vector(Hex())
486            let count = r.read_compact_u32()?;
487            let mut hashes = Vec::new();
488            for _ in 0..count {
489                let h = r.read_var_bytes()?;
490                hashes.push(serde_json::Value::String(bytes_to_hex(&h)));
491            }
492            HostRequest::ChainHeadRequest {
493                tag,
494                genesis_hash,
495                follow_sub_id,
496                data: serde_json::json!([hashes]),
497            }
498        }
499        TAG_CHAIN_HEAD_CONTINUE_REQ | TAG_CHAIN_HEAD_STOP_OP_REQ => {
500            let _version_tag = r.read_u8()?;
501            let genesis_hash = r.read_var_bytes()?;
502            let follow_sub_id = r.read_string()?;
503            let operation_id = r.read_string()?;
504            HostRequest::ChainHeadRequest {
505                tag,
506                genesis_hash,
507                follow_sub_id,
508                data: serde_json::json!([operation_id]),
509            }
510        }
511        TAG_CHAIN_SPEC_GENESIS_REQ | TAG_CHAIN_SPEC_NAME_REQ | TAG_CHAIN_SPEC_PROPS_REQ => {
512            let _version_tag = r.read_u8()?;
513            let genesis_hash = r.read_var_bytes()?;
514            HostRequest::ChainSpecRequest { tag, genesis_hash }
515        }
516        TAG_CHAIN_TX_BROADCAST_REQ => {
517            let _version_tag = r.read_u8()?;
518            let genesis_hash = r.read_var_bytes()?;
519            let transaction = r.read_var_bytes()?;
520            HostRequest::ChainTxBroadcast {
521                genesis_hash,
522                transaction,
523            }
524        }
525        TAG_CHAIN_TX_STOP_REQ => {
526            let _version_tag = r.read_u8()?;
527            let genesis_hash = r.read_var_bytes()?;
528            let operation_id = r.read_string()?;
529            HostRequest::ChainTxStop {
530                genesis_hash,
531                operation_id,
532            }
533        }
534        TAG_PUSH_NOTIFICATION_REQ => {
535            let _version_tag = r.read_u8()?;
536            let text = r.read_string()?;
537            let deeplink = r.read_option(|r| r.read_string())?;
538            HostRequest::PushNotification { text, deeplink }
539        }
540        // Known tags we don't handle yet
541        TAG_DEVICE_PERMISSION_REQ
542        | TAG_REMOTE_PERMISSION_REQ
543        | TAG_ACCOUNT_GET_REQ
544        | TAG_ACCOUNT_GET_ALIAS_REQ
545        | TAG_ACCOUNT_CREATE_PROOF_REQ
546        | TAG_CREATE_TX_NON_PRODUCT_REQ
547        | TAG_CHAT_CREATE_ROOM_REQ
548        | TAG_CHAT_REGISTER_BOT_REQ
549        | TAG_CHAT_POST_MSG_REQ
550        | TAG_STATEMENT_PROOF_REQ
551        | TAG_STATEMENT_SUBMIT_REQ
552        | TAG_PREIMAGE_SUBMIT_REQ => HostRequest::Unimplemented { tag },
553        // Subscription stop/interrupt — fire-and-forget, no response needed
554        TAG_ACCOUNT_STATUS_STOP
555        | TAG_ACCOUNT_STATUS_INTERRUPT
556        | TAG_CHAT_LIST_STOP
557        | TAG_CHAT_LIST_INTERRUPT
558        | TAG_CHAT_ACTION_STOP
559        | TAG_CHAT_ACTION_INTERRUPT
560        | TAG_CHAT_CUSTOM_MSG_STOP
561        | TAG_CHAT_CUSTOM_MSG_INTERRUPT
562        | TAG_STATEMENT_STORE_STOP
563        | TAG_STATEMENT_STORE_INTERRUPT
564        | TAG_PREIMAGE_LOOKUP_STOP
565        | TAG_PREIMAGE_LOOKUP_INTERRUPT
566        | TAG_JSONRPC_SUB_STOP
567        | TAG_JSONRPC_SUB_INTERRUPT
568        | TAG_CHAIN_HEAD_FOLLOW_STOP
569        | TAG_CHAIN_HEAD_FOLLOW_INTERRUPT => HostRequest::Unimplemented { tag },
570        _ => HostRequest::Unknown { tag },
571    };
572
573    Ok((request_id, tag, req))
574}
575
576/// Encode a response into a wire message.
577pub fn encode_response(request_id: &str, request_tag: u8, response: &HostResponse) -> Vec<u8> {
578    let mut buf = Vec::with_capacity(128);
579    begin_message(&mut buf, request_id);
580
581    match response {
582        HostResponse::HandshakeOk => {
583            // payload tag: host_handshake_response
584            encode_tag(&mut buf, TAG_HANDSHAKE_RESP);
585            // inner version tag: v1 = 0
586            encode_tag(&mut buf, 0);
587            // Result::Ok(void)
588            encode_result_ok_void(&mut buf);
589        }
590
591        HostResponse::AccountList(accounts) => {
592            // payload tag: host_get_non_product_accounts_response
593            encode_tag(&mut buf, TAG_GET_NON_PRODUCT_ACCOUNTS_RESP);
594            // inner version tag: v1 = 0
595            encode_tag(&mut buf, 0);
596            // Result::Ok(Vector(Account))
597            encode_result_ok(&mut buf);
598            // Vector: compact count + items
599            encode_vector_len(&mut buf, accounts.len() as u32);
600            for account in accounts {
601                // Account = Struct { publicKey: Bytes(), name: Option(str) }
602                // publicKey: dynamic bytes (compact len + raw)
603                encode_var_bytes(&mut buf, &account.public_key);
604                // name: Option(str)
605                match &account.name {
606                    None => encode_option_none(&mut buf),
607                    Some(name) => {
608                        encode_option_some(&mut buf);
609                        encode_string(&mut buf, name);
610                    }
611                }
612            }
613        }
614
615        HostResponse::Error(_reason) => {
616            let resp_tag = response_tag_for(request_tag);
617            encode_tag(&mut buf, resp_tag);
618            encode_tag(&mut buf, 0); // v1
619            encode_result_err(&mut buf);
620            // GenericError / Unknown error variant (last in the enum)
621            // Error = Struct { reason: str }
622            // The exact error enum index depends on the method.
623            // Use variant 0 (first error kind) as a generic rejection.
624            encode_tag(&mut buf, 0);
625        }
626    }
627
628    buf
629}
630
631/// Given a request tag, return the corresponding response tag.
632/// Only valid for request/response pairs (even tags where tag+1 is the response).
633/// Panics for subscription tags — those use dedicated encode functions.
634fn response_tag_for(request_tag: u8) -> u8 {
635    assert!(
636        !matches!(
637            request_tag,
638            TAG_ACCOUNT_STATUS_START
639                | TAG_CHAT_LIST_START
640                | TAG_CHAT_ACTION_START
641                | TAG_CHAT_CUSTOM_MSG_START
642                | TAG_STATEMENT_STORE_START
643                | TAG_PREIMAGE_LOOKUP_START
644                | TAG_JSONRPC_SUB_START
645                | TAG_CHAIN_HEAD_FOLLOW_START
646        ),
647        "response_tag_for called with subscription start tag {request_tag}"
648    );
649    request_tag + 1
650}
651
652/// Encode a feature_supported response (Result::Ok(bool)).
653pub fn encode_feature_response(request_id: &str, supported: bool) -> Vec<u8> {
654    let mut buf = Vec::with_capacity(32);
655    begin_message(&mut buf, request_id);
656    encode_tag(&mut buf, TAG_FEATURE_SUPPORTED_RESP);
657    encode_tag(&mut buf, 0); // v1
658    encode_result_ok(&mut buf);
659    buf.push(if supported { 1 } else { 0 }); // bool as u8
660    buf
661}
662
663/// Encode an account_connection_status_receive message.
664pub fn encode_account_status(request_id: &str, connected: bool) -> Vec<u8> {
665    let mut buf = Vec::with_capacity(32);
666    begin_message(&mut buf, request_id);
667    encode_tag(&mut buf, TAG_ACCOUNT_STATUS_RECEIVE);
668    encode_tag(&mut buf, 0); // v1
669                             // Status enum: 0 = disconnected, 1 = connected
670    encode_tag(&mut buf, if connected { 1 } else { 0 });
671    buf
672}
673
674/// Encode a local_storage_read response.
675pub fn encode_storage_read_response(request_id: &str, value: Option<&[u8]>) -> Vec<u8> {
676    let mut buf = Vec::with_capacity(64);
677    begin_message(&mut buf, request_id);
678    encode_tag(&mut buf, TAG_LOCAL_STORAGE_READ_RESP);
679    encode_tag(&mut buf, 0); // v1
680    encode_result_ok(&mut buf);
681    match value {
682        None => encode_option_none(&mut buf),
683        Some(v) => {
684            encode_option_some(&mut buf);
685            encode_var_bytes(&mut buf, v);
686        }
687    }
688    buf
689}
690
691/// Encode a local_storage_write/clear response (Result::Ok(void)).
692pub fn encode_storage_write_response(request_id: &str, is_clear: bool) -> Vec<u8> {
693    let mut buf = Vec::with_capacity(32);
694    begin_message(&mut buf, request_id);
695    let tag = if is_clear {
696        TAG_LOCAL_STORAGE_CLEAR_RESP
697    } else {
698        TAG_LOCAL_STORAGE_WRITE_RESP
699    };
700    encode_tag(&mut buf, tag);
701    encode_tag(&mut buf, 0); // v1
702    encode_result_ok_void(&mut buf);
703    buf
704}
705
706/// Encode a navigate_to response (Result::Ok(void)).
707pub fn encode_navigate_response(request_id: &str) -> Vec<u8> {
708    let mut buf = Vec::with_capacity(32);
709    begin_message(&mut buf, request_id);
710    encode_tag(&mut buf, TAG_NAVIGATE_TO_RESP);
711    encode_tag(&mut buf, 0); // v1
712    encode_result_ok_void(&mut buf);
713    buf
714}
715
716/// Encode a push_notification response (Result::Ok(void)).
717pub fn encode_push_notification_response(request_id: &str) -> Vec<u8> {
718    let mut buf = Vec::with_capacity(32);
719    begin_message(&mut buf, request_id);
720    encode_tag(&mut buf, TAG_PUSH_NOTIFICATION_RESP);
721    encode_tag(&mut buf, 0); // v1
722    encode_result_ok_void(&mut buf);
723    buf
724}
725
726/// Encode a sign_payload or sign_raw success response.
727/// Result::Ok { id: u32, signature: Bytes }
728pub fn encode_sign_response(request_id: &str, is_raw: bool, signature: &[u8]) -> Vec<u8> {
729    let mut buf = Vec::with_capacity(128);
730    begin_message(&mut buf, request_id);
731    encode_tag(
732        &mut buf,
733        if is_raw {
734            TAG_SIGN_RAW_RESP
735        } else {
736            TAG_SIGN_PAYLOAD_RESP
737        },
738    );
739    encode_tag(&mut buf, 0); // v1
740    encode_result_ok(&mut buf);
741    encode_compact_u32(&mut buf, 0); // id = 0
742    encode_var_bytes(&mut buf, signature);
743    buf
744}
745
746/// Encode a `host_jsonrpc_send` response.
747///
748/// The response wraps the JSON-RPC result (or error) as a SCALE string inside
749/// the standard `Result<String, Error>` envelope.
750pub fn encode_jsonrpc_send_response(request_id: &str, json_rpc_result: &str) -> Vec<u8> {
751    let mut buf = Vec::with_capacity(64 + json_rpc_result.len());
752    begin_message(&mut buf, request_id);
753    encode_tag(&mut buf, TAG_JSONRPC_SEND_RESP);
754    encode_tag(&mut buf, 0); // v1
755    encode_result_ok(&mut buf);
756    encode_string(&mut buf, json_rpc_result);
757    buf
758}
759
760/// Encode a `host_jsonrpc_send` error response.
761pub fn encode_jsonrpc_send_error(request_id: &str) -> Vec<u8> {
762    let mut buf = Vec::with_capacity(32);
763    begin_message(&mut buf, request_id);
764    encode_tag(&mut buf, TAG_JSONRPC_SEND_RESP);
765    encode_tag(&mut buf, 0); // v1
766    encode_result_err(&mut buf);
767    encode_tag(&mut buf, 0); // error variant 0
768    buf
769}
770
771/// Encode a `host_jsonrpc_subscribe` receive message.
772///
773/// Pushes a JSON-RPC message (initial response or notification) to the app
774/// for an active subscription. Uses the same request_id from the original
775/// `JsonRpcSubscribeStart`.
776pub fn encode_jsonrpc_sub_receive(request_id: &str, json_rpc_msg: &str) -> Vec<u8> {
777    let mut buf = Vec::with_capacity(64 + json_rpc_msg.len());
778    begin_message(&mut buf, request_id);
779    encode_tag(&mut buf, TAG_JSONRPC_SUB_RECEIVE);
780    encode_tag(&mut buf, 0); // v1
781    encode_string(&mut buf, json_rpc_msg);
782    buf
783}
784
785/// Encode a sign_payload or sign_raw error response (user rejected, wallet locked, etc).
786pub fn encode_sign_error(request_id: &str, is_raw: bool) -> Vec<u8> {
787    let mut buf = Vec::with_capacity(32);
788    begin_message(&mut buf, request_id);
789    encode_tag(
790        &mut buf,
791        if is_raw {
792            TAG_SIGN_RAW_RESP
793        } else {
794            TAG_SIGN_PAYLOAD_RESP
795        },
796    );
797    encode_tag(&mut buf, 0); // v1
798    encode_result_err(&mut buf);
799    encode_tag(&mut buf, 0); // error variant 0 = Rejected
800    buf
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn decode_handshake_request() {
809        // Manually encode: discriminator, requestId="test", tag=0, v1=0, version=1
810        let mut msg = Vec::new();
811        msg.push(PROTOCOL_DISCRIMINATOR);
812        encode_string(&mut msg, "test");
813        msg.push(TAG_HANDSHAKE_REQ); // payload tag
814        msg.push(0); // v1 tag
815        msg.push(PROTOCOL_VERSION); // version value
816
817        let (id, tag, req) = decode_message(&msg).unwrap();
818        assert_eq!(id, "test");
819        assert_eq!(tag, TAG_HANDSHAKE_REQ);
820        match req {
821            HostRequest::Handshake { version } => assert_eq!(version, 1),
822            _ => panic!("expected Handshake"),
823        }
824    }
825
826    #[test]
827    fn encode_handshake_response() {
828        let resp = encode_response("test", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
829
830        // Decode and verify structure
831        let mut r = Reader::new(&resp);
832        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
833        let id = r.read_string().unwrap();
834        assert_eq!(id, "test");
835        let tag = r.read_u8().unwrap();
836        assert_eq!(tag, TAG_HANDSHAKE_RESP);
837        let v1_tag = r.read_u8().unwrap();
838        assert_eq!(v1_tag, 0);
839        let result_ok = r.read_u8().unwrap();
840        assert_eq!(result_ok, 0x00); // Ok
841        assert_eq!(r.pos, resp.len()); // no trailing bytes
842    }
843
844    #[test]
845    fn decode_get_non_product_accounts() {
846        let mut msg = Vec::new();
847        msg.push(PROTOCOL_DISCRIMINATOR);
848        encode_string(&mut msg, "req-42");
849        msg.push(TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
850        msg.push(0); // v1
851
852        let (id, tag, req) = decode_message(&msg).unwrap();
853        assert_eq!(id, "req-42");
854        assert_eq!(tag, TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
855        assert!(matches!(req, HostRequest::GetNonProductAccounts));
856    }
857
858    #[test]
859    fn encode_account_list_response() {
860        let accounts = vec![
861            Account {
862                public_key: vec![0xd4; 32],
863                name: Some("Alice".into()),
864            },
865            Account {
866                public_key: vec![0x8e; 32],
867                name: None,
868            },
869        ];
870        let resp = encode_response(
871            "req-42",
872            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
873            &HostResponse::AccountList(accounts),
874        );
875
876        // Decode and verify structure
877        let mut r = Reader::new(&resp);
878        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
879        let id = r.read_string().unwrap();
880        assert_eq!(id, "req-42");
881        let tag = r.read_u8().unwrap();
882        assert_eq!(tag, TAG_GET_NON_PRODUCT_ACCOUNTS_RESP);
883        let v1 = r.read_u8().unwrap();
884        assert_eq!(v1, 0); // v1
885        let result = r.read_u8().unwrap();
886        assert_eq!(result, 0x00); // Ok
887        let count = r.read_compact_u32().unwrap();
888        assert_eq!(count, 2);
889
890        // Account 1: pubkey + Some("Alice")
891        let pk1 = r.read_var_bytes().unwrap();
892        assert_eq!(pk1.len(), 32);
893        assert_eq!(pk1[0], 0xd4);
894        let name1 = r.read_option(|r| r.read_string()).unwrap();
895        assert_eq!(name1.as_deref(), Some("Alice"));
896
897        // Account 2: pubkey + None
898        let pk2 = r.read_var_bytes().unwrap();
899        assert_eq!(pk2.len(), 32);
900        assert_eq!(pk2[0], 0x8e);
901        let name2 = r.read_option(|r| r.read_string()).unwrap();
902        assert!(name2.is_none());
903
904        assert_eq!(r.pos, resp.len());
905    }
906
907    #[test]
908    fn handshake_round_trip() {
909        // Simulate: app sends handshake request, host responds
910        let mut req_msg = Vec::new();
911        req_msg.push(PROTOCOL_DISCRIMINATOR);
912        encode_string(&mut req_msg, "hsk-1");
913        req_msg.push(TAG_HANDSHAKE_REQ);
914        req_msg.push(0); // v1
915        req_msg.push(PROTOCOL_VERSION);
916
917        let (id, tag, req) = decode_message(&req_msg).unwrap();
918        assert!(matches!(req, HostRequest::Handshake { version: 1 }));
919
920        let resp_bytes = encode_response(&id, tag, &HostResponse::HandshakeOk);
921
922        // Verify the response can be decoded
923        let mut r = Reader::new(&resp_bytes);
924        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
925        assert_eq!(r.read_string().unwrap(), "hsk-1");
926        assert_eq!(r.read_u8().unwrap(), TAG_HANDSHAKE_RESP);
927        assert_eq!(r.read_u8().unwrap(), 0); // v1
928        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
929    }
930
931    // -------------------------------------------------------------------
932    // Golden byte vectors — hand-verified against SCALE spec.
933    //
934    // Format: requestId (compact_len + UTF-8), tag (u8), version (u8), payload.
935    // These catch accidental encoding drift.
936    // -------------------------------------------------------------------
937
938    #[test]
939    fn golden_handshake_request() {
940        // discriminator=0x01, requestId = "t1" (compact_len=8, bytes "t1"), tag=0, v1=0, version=1
941        let expected: &[u8] = &[
942            0x01, // PROTOCOL_DISCRIMINATOR
943            0x08, b't', b'1', // compact(2) + "t1"
944            0x00, // TAG_HANDSHAKE_REQ
945            0x00, // v1
946            0x01, // version = 1
947        ];
948        let mut built = Vec::new();
949        built.push(PROTOCOL_DISCRIMINATOR);
950        encode_string(&mut built, "t1");
951        built.push(TAG_HANDSHAKE_REQ);
952        built.push(0);
953        built.push(PROTOCOL_VERSION);
954        assert_eq!(built, expected);
955    }
956
957    #[test]
958    fn golden_handshake_response_ok() {
959        let resp = encode_response("t1", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
960        let expected: &[u8] = &[
961            0x01, // PROTOCOL_DISCRIMINATOR
962            0x08, b't', b'1', // compact(2) + "t1"
963            0x01, // TAG_HANDSHAKE_RESP
964            0x00, // v1
965            0x00, // Result::Ok
966        ];
967        assert_eq!(resp, expected);
968    }
969
970    #[test]
971    fn golden_get_accounts_request() {
972        let expected: &[u8] = &[
973            0x01, // PROTOCOL_DISCRIMINATOR
974            0x08, b'a', b'1', // compact(2) + "a1"
975            28,   // TAG_GET_NON_PRODUCT_ACCOUNTS_REQ
976            0x00, // v1
977        ];
978        let mut built = Vec::new();
979        built.push(PROTOCOL_DISCRIMINATOR);
980        encode_string(&mut built, "a1");
981        built.push(TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
982        built.push(0);
983        assert_eq!(built, expected);
984    }
985
986    #[test]
987    fn golden_get_accounts_response_empty() {
988        let resp = encode_response(
989            "a1",
990            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
991            &HostResponse::AccountList(vec![]),
992        );
993        let expected: &[u8] = &[
994            0x01, // PROTOCOL_DISCRIMINATOR
995            0x08, b'a', b'1', // compact(2) + "a1"
996            29,   // TAG_GET_NON_PRODUCT_ACCOUNTS_RESP
997            0x00, // v1
998            0x00, // Result::Ok
999            0x00, // Vector len = 0
1000        ];
1001        assert_eq!(resp, expected);
1002    }
1003
1004    #[test]
1005    fn golden_storage_write_response() {
1006        let resp = encode_storage_write_response("s1", false);
1007        let expected: &[u8] = &[
1008            0x01, // PROTOCOL_DISCRIMINATOR
1009            0x08, b's', b'1', // compact(2) + "s1"
1010            15,   // TAG_LOCAL_STORAGE_WRITE_RESP
1011            0x00, // v1
1012            0x00, // Result::Ok(void)
1013        ];
1014        assert_eq!(resp, expected);
1015    }
1016
1017    #[test]
1018    fn golden_storage_clear_response() {
1019        let resp = encode_storage_write_response("s1", true);
1020        let expected: &[u8] = &[
1021            0x01, // PROTOCOL_DISCRIMINATOR
1022            0x08, b's', b'1', // compact(2) + "s1"
1023            17,   // TAG_LOCAL_STORAGE_CLEAR_RESP
1024            0x00, // v1
1025            0x00, // Result::Ok(void)
1026        ];
1027        assert_eq!(resp, expected);
1028    }
1029
1030    #[test]
1031    fn golden_feature_supported_response() {
1032        let resp = encode_feature_response("f1", false);
1033        let expected: &[u8] = &[
1034            0x01, // PROTOCOL_DISCRIMINATOR
1035            0x08, b'f', b'1', // compact(2) + "f1"
1036            3,    // TAG_FEATURE_SUPPORTED_RESP
1037            0x00, // v1
1038            0x00, // Result::Ok
1039            0x00, // false
1040        ];
1041        assert_eq!(resp, expected);
1042    }
1043
1044    #[test]
1045    fn golden_account_status_receive() {
1046        let resp = encode_account_status("c1", true);
1047        let expected: &[u8] = &[
1048            0x01, // PROTOCOL_DISCRIMINATOR
1049            0x08, b'c', b'1', // compact(2) + "c1"
1050            21,   // TAG_ACCOUNT_STATUS_RECEIVE
1051            0x00, // v1
1052            0x01, // connected = true
1053        ];
1054        assert_eq!(resp, expected);
1055    }
1056
1057    #[test]
1058    fn golden_sign_payload_response_ok() {
1059        let sig = [0xAB; 64];
1060        let resp = encode_sign_response("s1", false, &sig);
1061        let mut r = Reader::new(&resp);
1062        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1063        assert_eq!(r.read_string().unwrap(), "s1");
1064        assert_eq!(r.read_u8().unwrap(), TAG_SIGN_PAYLOAD_RESP);
1065        assert_eq!(r.read_u8().unwrap(), 0); // v1
1066        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
1067        assert_eq!(r.read_compact_u32().unwrap(), 0); // id
1068        let sig_bytes = r.read_var_bytes().unwrap();
1069        assert_eq!(sig_bytes, vec![0xAB; 64]);
1070        assert_eq!(r.pos, resp.len());
1071    }
1072
1073    #[test]
1074    fn golden_sign_raw_response_ok() {
1075        let sig = [0xCD; 64];
1076        let resp = encode_sign_response("s2", true, &sig);
1077        let mut r = Reader::new(&resp);
1078        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1079        assert_eq!(r.read_string().unwrap(), "s2");
1080        assert_eq!(r.read_u8().unwrap(), TAG_SIGN_RAW_RESP);
1081        assert_eq!(r.read_u8().unwrap(), 0); // v1
1082        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
1083        assert_eq!(r.read_compact_u32().unwrap(), 0); // id
1084        let sig_bytes = r.read_var_bytes().unwrap();
1085        assert_eq!(sig_bytes, vec![0xCD; 64]);
1086    }
1087
1088    #[test]
1089    fn golden_sign_error_response() {
1090        let resp = encode_sign_error("s3", false);
1091        let expected: &[u8] = &[
1092            0x01, // PROTOCOL_DISCRIMINATOR
1093            0x08, b's', b'3', // compact(2) + "s3"
1094            37,   // TAG_SIGN_PAYLOAD_RESP
1095            0x00, // v1
1096            0x01, // Result::Err
1097            0x00, // Rejected variant
1098        ];
1099        assert_eq!(resp, expected);
1100    }
1101
1102    #[test]
1103    fn decode_sign_payload_request() {
1104        let mut msg = Vec::new();
1105        msg.push(PROTOCOL_DISCRIMINATOR);
1106        encode_string(&mut msg, "sign-1");
1107        msg.push(TAG_SIGN_PAYLOAD_REQ);
1108        msg.push(0); // v1
1109        encode_var_bytes(&mut msg, &[0xAA; 32]); // publicKey
1110        msg.extend_from_slice(b"extrinsic-payload"); // payload
1111        let (id, tag, req) = decode_message(&msg).unwrap();
1112        assert_eq!(id, "sign-1");
1113        assert_eq!(tag, TAG_SIGN_PAYLOAD_REQ);
1114        match req {
1115            HostRequest::SignPayload {
1116                public_key,
1117                payload,
1118            } => {
1119                assert_eq!(public_key, vec![0xAA; 32]);
1120                assert_eq!(payload, b"extrinsic-payload");
1121            }
1122            _ => panic!("expected SignPayload"),
1123        }
1124    }
1125
1126    #[test]
1127    fn decode_sign_raw_request() {
1128        let mut msg = Vec::new();
1129        msg.push(PROTOCOL_DISCRIMINATOR);
1130        encode_string(&mut msg, "sign-2");
1131        msg.push(TAG_SIGN_RAW_REQ);
1132        msg.push(0); // v1
1133        encode_var_bytes(&mut msg, &[0xBB; 32]); // publicKey
1134        msg.extend_from_slice(b"raw-data"); // data
1135        let (id, tag, req) = decode_message(&msg).unwrap();
1136        assert_eq!(id, "sign-2");
1137        assert_eq!(tag, TAG_SIGN_RAW_REQ);
1138        match req {
1139            HostRequest::SignRaw { public_key, data } => {
1140                assert_eq!(public_key, vec![0xBB; 32]);
1141                assert_eq!(data, b"raw-data");
1142            }
1143            _ => panic!("expected SignRaw"),
1144        }
1145    }
1146
1147    // -- Push notification protocol tests --
1148
1149    #[test]
1150    fn decode_push_notification_request_with_deeplink() {
1151        let mut msg = Vec::new();
1152        msg.push(PROTOCOL_DISCRIMINATOR);
1153        encode_string(&mut msg, "pn-1");
1154        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1155        msg.push(0); // v1
1156        encode_string(&mut msg, "Transfer complete");
1157        msg.push(0x01); // Some
1158        encode_string(&mut msg, "https://app.example/tx/123");
1159
1160        let (id, tag, req) = decode_message(&msg).unwrap();
1161        assert_eq!(id, "pn-1");
1162        assert_eq!(tag, TAG_PUSH_NOTIFICATION_REQ);
1163        match req {
1164            HostRequest::PushNotification { text, deeplink } => {
1165                assert_eq!(text, "Transfer complete");
1166                assert_eq!(deeplink.as_deref(), Some("https://app.example/tx/123"));
1167            }
1168            _ => panic!("expected PushNotification"),
1169        }
1170    }
1171
1172    #[test]
1173    fn decode_push_notification_request_without_deeplink() {
1174        let mut msg = Vec::new();
1175        msg.push(PROTOCOL_DISCRIMINATOR);
1176        encode_string(&mut msg, "pn-2");
1177        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1178        msg.push(0); // v1
1179        encode_string(&mut msg, "Hello world");
1180        msg.push(0x00); // None
1181
1182        let (id, tag, req) = decode_message(&msg).unwrap();
1183        assert_eq!(id, "pn-2");
1184        assert_eq!(tag, TAG_PUSH_NOTIFICATION_REQ);
1185        match req {
1186            HostRequest::PushNotification { text, deeplink } => {
1187                assert_eq!(text, "Hello world");
1188                assert!(deeplink.is_none());
1189            }
1190            _ => panic!("expected PushNotification"),
1191        }
1192    }
1193
1194    #[test]
1195    fn encode_push_notification_response_produces_ok() {
1196        let resp = encode_push_notification_response("pn-1");
1197        let mut r = Reader::new(&resp);
1198        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1199        assert_eq!(r.read_string().unwrap(), "pn-1");
1200        assert_eq!(r.read_u8().unwrap(), TAG_PUSH_NOTIFICATION_RESP);
1201        assert_eq!(r.read_u8().unwrap(), 0); // v1
1202        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok(void)
1203        assert_eq!(r.pos, resp.len());
1204    }
1205
1206    #[test]
1207    fn golden_push_notification_response_ok() {
1208        let resp = encode_push_notification_response("t1");
1209        let expected: &[u8] = &[
1210            0x01, // PROTOCOL_DISCRIMINATOR
1211            0x08, b't', b'1', // compact(2) + "t1"
1212            0x05, // TAG_PUSH_NOTIFICATION_RESP
1213            0x00, // v1
1214            0x00, // Result::Ok(void)
1215        ];
1216        assert_eq!(resp, expected);
1217    }
1218
1219    #[test]
1220    fn push_notification_round_trip() {
1221        let mut req_msg = Vec::new();
1222        req_msg.push(PROTOCOL_DISCRIMINATOR);
1223        encode_string(&mut req_msg, "pn-rt");
1224        req_msg.push(TAG_PUSH_NOTIFICATION_REQ);
1225        req_msg.push(0); // v1
1226        encode_string(&mut req_msg, "Test notification");
1227        req_msg.push(0x00); // None deeplink
1228
1229        let (id, _tag, req) = decode_message(&req_msg).unwrap();
1230        assert!(matches!(req, HostRequest::PushNotification { .. }));
1231
1232        let resp_bytes = encode_push_notification_response(&id);
1233
1234        let mut r = Reader::new(&resp_bytes);
1235        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1236        assert_eq!(r.read_string().unwrap(), "pn-rt");
1237        assert_eq!(r.read_u8().unwrap(), TAG_PUSH_NOTIFICATION_RESP);
1238        assert_eq!(r.read_u8().unwrap(), 0); // v1
1239        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok(void)
1240    }
1241
1242    #[test]
1243    fn golden_storage_read_response_some() {
1244        let resp = encode_storage_read_response("s1", Some(b"hi"));
1245        let expected: &[u8] = &[
1246            PROTOCOL_DISCRIMINATOR,
1247            0x08,
1248            b's',
1249            b'1', // compact(2) + "s1"
1250            13,   // TAG_LOCAL_STORAGE_READ_RESP
1251            0x00, // v1
1252            0x00, // Result::Ok
1253            0x01, // Option::Some
1254            0x08,
1255            b'h',
1256            b'i', // compact(2) + "hi"
1257        ];
1258        assert_eq!(resp, expected);
1259    }
1260
1261    #[test]
1262    fn golden_storage_read_response_none() {
1263        let resp = encode_storage_read_response("s1", None);
1264        let expected: &[u8] = &[
1265            PROTOCOL_DISCRIMINATOR,
1266            0x08,
1267            b's',
1268            b'1', // compact(2) + "s1"
1269            13,   // TAG_LOCAL_STORAGE_READ_RESP
1270            0x00, // v1
1271            0x00, // Result::Ok
1272            0x00, // Option::None
1273        ];
1274        assert_eq!(resp, expected);
1275    }
1276
1277    #[test]
1278    fn test_decode_rejects_missing_discriminator() {
1279        // A valid handshake body with NO discriminator prefix
1280        let mut msg = Vec::new();
1281        encode_string(&mut msg, "t1");
1282        msg.push(TAG_HANDSHAKE_REQ);
1283        msg.push(0);
1284        msg.push(PROTOCOL_VERSION);
1285        assert!(matches!(
1286            decode_message(&msg),
1287            Err(DecodeErr::UnknownProtocol)
1288        ));
1289    }
1290
1291    #[test]
1292    fn test_decode_rejects_wrong_discriminator() {
1293        let mut msg = vec![0x00]; // wrong discriminator
1294        encode_string(&mut msg, "t1");
1295        msg.push(TAG_HANDSHAKE_REQ);
1296        msg.push(0);
1297        msg.push(PROTOCOL_VERSION);
1298        assert!(matches!(
1299            decode_message(&msg),
1300            Err(DecodeErr::UnknownProtocol)
1301        ));
1302    }
1303
1304    #[test]
1305    fn test_decode_accepts_correct_discriminator() {
1306        let mut msg = vec![PROTOCOL_DISCRIMINATOR];
1307        encode_string(&mut msg, "t1");
1308        msg.push(TAG_HANDSHAKE_REQ);
1309        msg.push(0);
1310        msg.push(PROTOCOL_VERSION);
1311        let (req_id, tag, _) = decode_message(&msg).unwrap();
1312        assert_eq!(req_id, "t1");
1313        assert_eq!(tag, TAG_HANDSHAKE_REQ);
1314    }
1315
1316    #[test]
1317    fn test_is_host_sdk_message_true() {
1318        assert!(is_host_sdk_message(&[0x01, 0x08, b't', b'1', 0]));
1319        assert!(is_host_sdk_message(&[0x01])); // single byte == discriminator
1320    }
1321
1322    #[test]
1323    fn test_is_host_sdk_message_false_empty() {
1324        assert!(!is_host_sdk_message(&[]));
1325    }
1326
1327    #[test]
1328    fn test_is_host_sdk_message_false_wrong_byte() {
1329        assert!(!is_host_sdk_message(&[0x00, 0x08]));
1330        assert!(!is_host_sdk_message(&[0x02, 0x08]));
1331        assert!(!is_host_sdk_message(&[0xFF]));
1332    }
1333
1334    #[test]
1335    fn test_all_encode_functions_start_with_discriminator() {
1336        // encode_response - HandshakeOk
1337        let buf = encode_response("r", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
1338        assert_eq!(buf[0], PROTOCOL_DISCRIMINATOR);
1339
1340        // encode_response - AccountList
1341        let buf = encode_response(
1342            "r",
1343            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
1344            &HostResponse::AccountList(vec![]),
1345        );
1346        assert_eq!(buf[0], PROTOCOL_DISCRIMINATOR);
1347
1348        // encode_response - Error
1349        let buf = encode_response("r", TAG_HANDSHAKE_REQ, &HostResponse::Error("e".into()));
1350        assert_eq!(buf[0], PROTOCOL_DISCRIMINATOR);
1351
1352        // encode_feature_response
1353        assert_eq!(
1354            encode_feature_response("r", true)[0],
1355            PROTOCOL_DISCRIMINATOR
1356        );
1357        assert_eq!(
1358            encode_feature_response("r", false)[0],
1359            PROTOCOL_DISCRIMINATOR
1360        );
1361
1362        // encode_account_status
1363        assert_eq!(encode_account_status("r", true)[0], PROTOCOL_DISCRIMINATOR);
1364
1365        // encode_storage_read_response
1366        assert_eq!(
1367            encode_storage_read_response("r", None)[0],
1368            PROTOCOL_DISCRIMINATOR
1369        );
1370        assert_eq!(
1371            encode_storage_read_response("r", Some(b"v"))[0],
1372            PROTOCOL_DISCRIMINATOR
1373        );
1374
1375        // encode_storage_write_response
1376        assert_eq!(
1377            encode_storage_write_response("r", false)[0],
1378            PROTOCOL_DISCRIMINATOR
1379        );
1380        assert_eq!(
1381            encode_storage_write_response("r", true)[0],
1382            PROTOCOL_DISCRIMINATOR
1383        );
1384
1385        // encode_navigate_response
1386        assert_eq!(encode_navigate_response("r")[0], PROTOCOL_DISCRIMINATOR);
1387
1388        // encode_push_notification_response
1389        assert_eq!(
1390            encode_push_notification_response("r")[0],
1391            PROTOCOL_DISCRIMINATOR
1392        );
1393
1394        // encode_sign_response
1395        assert_eq!(
1396            encode_sign_response("r", false, &[0; 64])[0],
1397            PROTOCOL_DISCRIMINATOR
1398        );
1399        assert_eq!(
1400            encode_sign_response("r", true, &[0; 64])[0],
1401            PROTOCOL_DISCRIMINATOR
1402        );
1403
1404        // encode_jsonrpc_send_response
1405        assert_eq!(
1406            encode_jsonrpc_send_response("r", "{}")[0],
1407            PROTOCOL_DISCRIMINATOR
1408        );
1409
1410        // encode_jsonrpc_send_error
1411        assert_eq!(encode_jsonrpc_send_error("r")[0], PROTOCOL_DISCRIMINATOR);
1412
1413        // encode_jsonrpc_sub_receive
1414        assert_eq!(
1415            encode_jsonrpc_sub_receive("r", "{}")[0],
1416            PROTOCOL_DISCRIMINATOR
1417        );
1418
1419        // encode_sign_error
1420        assert_eq!(encode_sign_error("r", false)[0], PROTOCOL_DISCRIMINATOR);
1421        assert_eq!(encode_sign_error("r", true)[0], PROTOCOL_DISCRIMINATOR);
1422    }
1423
1424    #[test]
1425    fn decode_push_notification_truncated_missing_deeplink() {
1426        let mut msg = Vec::new();
1427        msg.push(PROTOCOL_DISCRIMINATOR);
1428        encode_string(&mut msg, "pn-trunc");
1429        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1430        msg.push(0); // v1
1431        encode_string(&mut msg, "Hello");
1432        // Missing: Option byte for deeplink
1433
1434        assert!(decode_message(&msg).is_err());
1435    }
1436}