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_ACCOUNT_STATUS_START => {
372            let _version_tag = r.read_u8()?;
373            HostRequest::AccountConnectionStatusStart
374        }
375        TAG_JSONRPC_SEND_REQ => {
376            let _version_tag = r.read_u8()?;
377            let rest = r.remaining().to_vec();
378            r.skip_rest();
379            HostRequest::JsonRpcSend { data: rest }
380        }
381        TAG_JSONRPC_SUB_START => {
382            let _version_tag = r.read_u8()?;
383            let rest = r.remaining().to_vec();
384            r.skip_rest();
385            HostRequest::JsonRpcSubscribeStart { data: rest }
386        }
387        TAG_CHAIN_HEAD_FOLLOW_START => {
388            let _version_tag = r.read_u8()?;
389            let genesis_hash = r.read_var_bytes()?;
390            let with_runtime = r.read_u8()? != 0;
391            HostRequest::ChainHeadFollowStart {
392                genesis_hash,
393                with_runtime,
394            }
395        }
396        TAG_CHAIN_HEAD_HEADER_REQ => {
397            let _version_tag = r.read_u8()?;
398            let genesis_hash = r.read_var_bytes()?;
399            let follow_sub_id = r.read_string()?;
400            let hash = r.read_var_bytes()?;
401            let hash_hex = bytes_to_hex(&hash);
402            HostRequest::ChainHeadRequest {
403                tag,
404                genesis_hash,
405                follow_sub_id,
406                data: serde_json::json!([hash_hex]),
407            }
408        }
409        TAG_CHAIN_HEAD_BODY_REQ => {
410            let _version_tag = r.read_u8()?;
411            let genesis_hash = r.read_var_bytes()?;
412            let follow_sub_id = r.read_string()?;
413            let hash = r.read_var_bytes()?;
414            let hash_hex = bytes_to_hex(&hash);
415            HostRequest::ChainHeadRequest {
416                tag,
417                genesis_hash,
418                follow_sub_id,
419                data: serde_json::json!([hash_hex]),
420            }
421        }
422        TAG_CHAIN_HEAD_STORAGE_REQ => {
423            let _version_tag = r.read_u8()?;
424            let genesis_hash = r.read_var_bytes()?;
425            let follow_sub_id = r.read_string()?;
426            let hash = r.read_var_bytes()?;
427            let hash_hex = bytes_to_hex(&hash);
428            // items: Vector(StorageQueryItem)
429            let item_count = r.read_compact_u32()?;
430            let mut items = Vec::new();
431            for _ in 0..item_count {
432                let key = r.read_var_bytes()?;
433                let storage_type = r.read_u8()?; // Status enum index
434                let type_str = match storage_type {
435                    0 => "value",
436                    1 => "hash",
437                    2 => "closestDescendantMerkleValue",
438                    3 => "descendantsValues",
439                    4 => "descendantsHashes",
440                    _ => "value",
441                };
442                items.push(serde_json::json!({
443                    "key": bytes_to_hex(&key),
444                    "type": type_str,
445                }));
446            }
447            // childTrie: Nullable(Hex()) = Option(var_bytes)
448            let child_trie = r.read_option(|r| r.read_var_bytes())?;
449            let child_trie_hex = child_trie.map(|b| bytes_to_hex(&b));
450            HostRequest::ChainHeadRequest {
451                tag,
452                genesis_hash,
453                follow_sub_id,
454                data: serde_json::json!([hash_hex, items, child_trie_hex]),
455            }
456        }
457        TAG_CHAIN_HEAD_CALL_REQ => {
458            let _version_tag = r.read_u8()?;
459            let genesis_hash = r.read_var_bytes()?;
460            let follow_sub_id = r.read_string()?;
461            let hash = r.read_var_bytes()?;
462            let function = r.read_string()?;
463            let call_params = r.read_var_bytes()?;
464            HostRequest::ChainHeadRequest {
465                tag,
466                genesis_hash,
467                follow_sub_id,
468                data: serde_json::json!([
469                    bytes_to_hex(&hash),
470                    function,
471                    bytes_to_hex(&call_params)
472                ]),
473            }
474        }
475        TAG_CHAIN_HEAD_UNPIN_REQ => {
476            let _version_tag = r.read_u8()?;
477            let genesis_hash = r.read_var_bytes()?;
478            let follow_sub_id = r.read_string()?;
479            // hashes: Vector(Hex())
480            let count = r.read_compact_u32()?;
481            let mut hashes = Vec::new();
482            for _ in 0..count {
483                let h = r.read_var_bytes()?;
484                hashes.push(serde_json::Value::String(bytes_to_hex(&h)));
485            }
486            HostRequest::ChainHeadRequest {
487                tag,
488                genesis_hash,
489                follow_sub_id,
490                data: serde_json::json!([hashes]),
491            }
492        }
493        TAG_CHAIN_HEAD_CONTINUE_REQ | TAG_CHAIN_HEAD_STOP_OP_REQ => {
494            let _version_tag = r.read_u8()?;
495            let genesis_hash = r.read_var_bytes()?;
496            let follow_sub_id = r.read_string()?;
497            let operation_id = r.read_string()?;
498            HostRequest::ChainHeadRequest {
499                tag,
500                genesis_hash,
501                follow_sub_id,
502                data: serde_json::json!([operation_id]),
503            }
504        }
505        TAG_CHAIN_SPEC_GENESIS_REQ | TAG_CHAIN_SPEC_NAME_REQ | TAG_CHAIN_SPEC_PROPS_REQ => {
506            let _version_tag = r.read_u8()?;
507            let genesis_hash = r.read_var_bytes()?;
508            HostRequest::ChainSpecRequest { tag, genesis_hash }
509        }
510        TAG_CHAIN_TX_BROADCAST_REQ => {
511            let _version_tag = r.read_u8()?;
512            let genesis_hash = r.read_var_bytes()?;
513            let transaction = r.read_var_bytes()?;
514            HostRequest::ChainTxBroadcast {
515                genesis_hash,
516                transaction,
517            }
518        }
519        TAG_CHAIN_TX_STOP_REQ => {
520            let _version_tag = r.read_u8()?;
521            let genesis_hash = r.read_var_bytes()?;
522            let operation_id = r.read_string()?;
523            HostRequest::ChainTxStop {
524                genesis_hash,
525                operation_id,
526            }
527        }
528        TAG_PUSH_NOTIFICATION_REQ => {
529            let _version_tag = r.read_u8()?;
530            let text = r.read_string()?;
531            let deeplink = r.read_option(|r| r.read_string())?;
532            HostRequest::PushNotification { text, deeplink }
533        }
534        // Known tags we don't handle yet
535        TAG_DEVICE_PERMISSION_REQ
536        | TAG_REMOTE_PERMISSION_REQ
537        | TAG_ACCOUNT_GET_REQ
538        | TAG_ACCOUNT_GET_ALIAS_REQ
539        | TAG_ACCOUNT_CREATE_PROOF_REQ
540        | TAG_CREATE_TX_NON_PRODUCT_REQ
541        | TAG_CHAT_CREATE_ROOM_REQ
542        | TAG_CHAT_REGISTER_BOT_REQ
543        | TAG_CHAT_POST_MSG_REQ
544        | TAG_STATEMENT_PROOF_REQ
545        | TAG_STATEMENT_SUBMIT_REQ
546        | TAG_PREIMAGE_SUBMIT_REQ => HostRequest::Unimplemented { tag },
547        // Subscription stop/interrupt — fire-and-forget, no response needed
548        TAG_ACCOUNT_STATUS_STOP
549        | TAG_ACCOUNT_STATUS_INTERRUPT
550        | TAG_CHAT_LIST_STOP
551        | TAG_CHAT_LIST_INTERRUPT
552        | TAG_CHAT_ACTION_STOP
553        | TAG_CHAT_ACTION_INTERRUPT
554        | TAG_CHAT_CUSTOM_MSG_STOP
555        | TAG_CHAT_CUSTOM_MSG_INTERRUPT
556        | TAG_STATEMENT_STORE_STOP
557        | TAG_STATEMENT_STORE_INTERRUPT
558        | TAG_PREIMAGE_LOOKUP_STOP
559        | TAG_PREIMAGE_LOOKUP_INTERRUPT
560        | TAG_JSONRPC_SUB_STOP
561        | TAG_JSONRPC_SUB_INTERRUPT
562        | TAG_CHAIN_HEAD_FOLLOW_STOP
563        | TAG_CHAIN_HEAD_FOLLOW_INTERRUPT => HostRequest::Unimplemented { tag },
564        _ => HostRequest::Unknown { tag },
565    };
566
567    Ok((request_id, tag, req))
568}
569
570/// Encode a response into a wire message.
571pub fn encode_response(request_id: &str, request_tag: u8, response: &HostResponse) -> Vec<u8> {
572    let mut buf = Vec::with_capacity(128);
573    begin_message(&mut buf, request_id);
574
575    match response {
576        HostResponse::HandshakeOk => {
577            // payload tag: host_handshake_response
578            encode_tag(&mut buf, TAG_HANDSHAKE_RESP);
579            // inner version tag: v1 = 0
580            encode_tag(&mut buf, 0);
581            // Result::Ok(void)
582            encode_result_ok_void(&mut buf);
583        }
584
585        HostResponse::AccountList(accounts) => {
586            // payload tag: host_get_non_product_accounts_response
587            encode_tag(&mut buf, TAG_GET_NON_PRODUCT_ACCOUNTS_RESP);
588            // inner version tag: v1 = 0
589            encode_tag(&mut buf, 0);
590            // Result::Ok(Vector(Account))
591            encode_result_ok(&mut buf);
592            // Vector: compact count + items
593            encode_vector_len(&mut buf, accounts.len() as u32);
594            for account in accounts {
595                // Account = Struct { publicKey: Bytes(), name: Option(str) }
596                // publicKey: dynamic bytes (compact len + raw)
597                encode_var_bytes(&mut buf, &account.public_key);
598                // name: Option(str)
599                match &account.name {
600                    None => encode_option_none(&mut buf),
601                    Some(name) => {
602                        encode_option_some(&mut buf);
603                        encode_string(&mut buf, name);
604                    }
605                }
606            }
607        }
608
609        HostResponse::Error(_reason) => {
610            let resp_tag = response_tag_for(request_tag);
611            encode_tag(&mut buf, resp_tag);
612            encode_tag(&mut buf, 0); // v1
613            encode_result_err(&mut buf);
614            // GenericError / Unknown error variant (last in the enum)
615            // Error = Struct { reason: str }
616            // The exact error enum index depends on the method.
617            // Use variant 0 (first error kind) as a generic rejection.
618            encode_tag(&mut buf, 0);
619        }
620    }
621
622    buf
623}
624
625/// Given a request tag, return the corresponding response tag.
626/// Only valid for request/response pairs (even tags where tag+1 is the response).
627/// Panics for subscription tags — those use dedicated encode functions.
628fn response_tag_for(request_tag: u8) -> u8 {
629    assert!(
630        !matches!(
631            request_tag,
632            TAG_ACCOUNT_STATUS_START
633                | TAG_CHAT_LIST_START
634                | TAG_CHAT_ACTION_START
635                | TAG_CHAT_CUSTOM_MSG_START
636                | TAG_STATEMENT_STORE_START
637                | TAG_PREIMAGE_LOOKUP_START
638                | TAG_JSONRPC_SUB_START
639                | TAG_CHAIN_HEAD_FOLLOW_START
640        ),
641        "response_tag_for called with subscription start tag {request_tag}"
642    );
643    request_tag + 1
644}
645
646/// Encode a feature_supported response (Result::Ok(bool)).
647pub fn encode_feature_response(request_id: &str, supported: bool) -> Vec<u8> {
648    let mut buf = Vec::with_capacity(32);
649    begin_message(&mut buf, request_id);
650    encode_tag(&mut buf, TAG_FEATURE_SUPPORTED_RESP);
651    encode_tag(&mut buf, 0); // v1
652    encode_result_ok(&mut buf);
653    buf.push(if supported { 1 } else { 0 }); // bool as u8
654    buf
655}
656
657/// Encode an account_connection_status_receive message.
658pub fn encode_account_status(request_id: &str, connected: bool) -> Vec<u8> {
659    let mut buf = Vec::with_capacity(32);
660    begin_message(&mut buf, request_id);
661    encode_tag(&mut buf, TAG_ACCOUNT_STATUS_RECEIVE);
662    encode_tag(&mut buf, 0); // v1
663                             // Status enum: 0 = disconnected, 1 = connected
664    encode_tag(&mut buf, if connected { 1 } else { 0 });
665    buf
666}
667
668/// Encode a local_storage_read response.
669pub fn encode_storage_read_response(request_id: &str, value: Option<&[u8]>) -> Vec<u8> {
670    let mut buf = Vec::with_capacity(64);
671    begin_message(&mut buf, request_id);
672    encode_tag(&mut buf, TAG_LOCAL_STORAGE_READ_RESP);
673    encode_tag(&mut buf, 0); // v1
674    encode_result_ok(&mut buf);
675    match value {
676        None => encode_option_none(&mut buf),
677        Some(v) => {
678            encode_option_some(&mut buf);
679            encode_var_bytes(&mut buf, v);
680        }
681    }
682    buf
683}
684
685/// Encode a local_storage_write/clear response (Result::Ok(void)).
686pub fn encode_storage_write_response(request_id: &str, is_clear: bool) -> Vec<u8> {
687    let mut buf = Vec::with_capacity(32);
688    begin_message(&mut buf, request_id);
689    let tag = if is_clear {
690        TAG_LOCAL_STORAGE_CLEAR_RESP
691    } else {
692        TAG_LOCAL_STORAGE_WRITE_RESP
693    };
694    encode_tag(&mut buf, tag);
695    encode_tag(&mut buf, 0); // v1
696    encode_result_ok_void(&mut buf);
697    buf
698}
699
700/// Encode a navigate_to response (Result::Ok(void)).
701pub fn encode_navigate_response(request_id: &str) -> Vec<u8> {
702    let mut buf = Vec::with_capacity(32);
703    begin_message(&mut buf, request_id);
704    encode_tag(&mut buf, TAG_NAVIGATE_TO_RESP);
705    encode_tag(&mut buf, 0); // v1
706    encode_result_ok_void(&mut buf);
707    buf
708}
709
710/// Encode a push_notification response (Result::Ok(void)).
711pub fn encode_push_notification_response(request_id: &str) -> Vec<u8> {
712    let mut buf = Vec::with_capacity(32);
713    begin_message(&mut buf, request_id);
714    encode_tag(&mut buf, TAG_PUSH_NOTIFICATION_RESP);
715    encode_tag(&mut buf, 0); // v1
716    encode_result_ok_void(&mut buf);
717    buf
718}
719
720/// Encode a sign_payload or sign_raw success response.
721/// Result::Ok { id: u32, signature: Bytes }
722pub fn encode_sign_response(request_id: &str, is_raw: bool, signature: &[u8]) -> Vec<u8> {
723    let mut buf = Vec::with_capacity(128);
724    begin_message(&mut buf, request_id);
725    encode_tag(
726        &mut buf,
727        if is_raw {
728            TAG_SIGN_RAW_RESP
729        } else {
730            TAG_SIGN_PAYLOAD_RESP
731        },
732    );
733    encode_tag(&mut buf, 0); // v1
734    encode_result_ok(&mut buf);
735    encode_compact_u32(&mut buf, 0); // id = 0
736    encode_var_bytes(&mut buf, signature);
737    buf
738}
739
740/// Encode a `host_jsonrpc_send` response.
741///
742/// The response wraps the JSON-RPC result (or error) as a SCALE string inside
743/// the standard `Result<String, Error>` envelope.
744pub fn encode_jsonrpc_send_response(request_id: &str, json_rpc_result: &str) -> Vec<u8> {
745    let mut buf = Vec::with_capacity(64 + json_rpc_result.len());
746    begin_message(&mut buf, request_id);
747    encode_tag(&mut buf, TAG_JSONRPC_SEND_RESP);
748    encode_tag(&mut buf, 0); // v1
749    encode_result_ok(&mut buf);
750    encode_string(&mut buf, json_rpc_result);
751    buf
752}
753
754/// Encode a `host_jsonrpc_send` error response.
755pub fn encode_jsonrpc_send_error(request_id: &str) -> Vec<u8> {
756    let mut buf = Vec::with_capacity(32);
757    begin_message(&mut buf, request_id);
758    encode_tag(&mut buf, TAG_JSONRPC_SEND_RESP);
759    encode_tag(&mut buf, 0); // v1
760    encode_result_err(&mut buf);
761    encode_tag(&mut buf, 0); // error variant 0
762    buf
763}
764
765/// Encode a `host_jsonrpc_subscribe` receive message.
766///
767/// Pushes a JSON-RPC message (initial response or notification) to the app
768/// for an active subscription. Uses the same request_id from the original
769/// `JsonRpcSubscribeStart`.
770pub fn encode_jsonrpc_sub_receive(request_id: &str, json_rpc_msg: &str) -> Vec<u8> {
771    let mut buf = Vec::with_capacity(64 + json_rpc_msg.len());
772    begin_message(&mut buf, request_id);
773    encode_tag(&mut buf, TAG_JSONRPC_SUB_RECEIVE);
774    encode_tag(&mut buf, 0); // v1
775    encode_string(&mut buf, json_rpc_msg);
776    buf
777}
778
779/// Encode a sign_payload or sign_raw error response (user rejected, wallet locked, etc).
780pub fn encode_sign_error(request_id: &str, is_raw: bool) -> Vec<u8> {
781    let mut buf = Vec::with_capacity(32);
782    begin_message(&mut buf, request_id);
783    encode_tag(
784        &mut buf,
785        if is_raw {
786            TAG_SIGN_RAW_RESP
787        } else {
788            TAG_SIGN_PAYLOAD_RESP
789        },
790    );
791    encode_tag(&mut buf, 0); // v1
792    encode_result_err(&mut buf);
793    encode_tag(&mut buf, 0); // error variant 0 = Rejected
794    buf
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800
801    #[test]
802    fn decode_handshake_request() {
803        // Manually encode: discriminator, requestId="test", tag=0, v1=0, version=1
804        let mut msg = Vec::new();
805        msg.push(PROTOCOL_DISCRIMINATOR);
806        encode_string(&mut msg, "test");
807        msg.push(TAG_HANDSHAKE_REQ); // payload tag
808        msg.push(0); // v1 tag
809        msg.push(PROTOCOL_VERSION); // version value
810
811        let (id, tag, req) = decode_message(&msg).unwrap();
812        assert_eq!(id, "test");
813        assert_eq!(tag, TAG_HANDSHAKE_REQ);
814        match req {
815            HostRequest::Handshake { version } => assert_eq!(version, 1),
816            _ => panic!("expected Handshake"),
817        }
818    }
819
820    #[test]
821    fn encode_handshake_response() {
822        let resp = encode_response("test", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
823
824        // Decode and verify structure
825        let mut r = Reader::new(&resp);
826        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
827        let id = r.read_string().unwrap();
828        assert_eq!(id, "test");
829        let tag = r.read_u8().unwrap();
830        assert_eq!(tag, TAG_HANDSHAKE_RESP);
831        let v1_tag = r.read_u8().unwrap();
832        assert_eq!(v1_tag, 0);
833        let result_ok = r.read_u8().unwrap();
834        assert_eq!(result_ok, 0x00); // Ok
835        assert_eq!(r.pos, resp.len()); // no trailing bytes
836    }
837
838    #[test]
839    fn decode_get_non_product_accounts() {
840        let mut msg = Vec::new();
841        msg.push(PROTOCOL_DISCRIMINATOR);
842        encode_string(&mut msg, "req-42");
843        msg.push(TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
844        msg.push(0); // v1
845
846        let (id, tag, req) = decode_message(&msg).unwrap();
847        assert_eq!(id, "req-42");
848        assert_eq!(tag, TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
849        assert!(matches!(req, HostRequest::GetNonProductAccounts));
850    }
851
852    #[test]
853    fn encode_account_list_response() {
854        let accounts = vec![
855            Account {
856                public_key: vec![0xd4; 32],
857                name: Some("Alice".into()),
858            },
859            Account {
860                public_key: vec![0x8e; 32],
861                name: None,
862            },
863        ];
864        let resp = encode_response(
865            "req-42",
866            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
867            &HostResponse::AccountList(accounts),
868        );
869
870        // Decode and verify structure
871        let mut r = Reader::new(&resp);
872        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
873        let id = r.read_string().unwrap();
874        assert_eq!(id, "req-42");
875        let tag = r.read_u8().unwrap();
876        assert_eq!(tag, TAG_GET_NON_PRODUCT_ACCOUNTS_RESP);
877        let v1 = r.read_u8().unwrap();
878        assert_eq!(v1, 0); // v1
879        let result = r.read_u8().unwrap();
880        assert_eq!(result, 0x00); // Ok
881        let count = r.read_compact_u32().unwrap();
882        assert_eq!(count, 2);
883
884        // Account 1: pubkey + Some("Alice")
885        let pk1 = r.read_var_bytes().unwrap();
886        assert_eq!(pk1.len(), 32);
887        assert_eq!(pk1[0], 0xd4);
888        let name1 = r.read_option(|r| r.read_string()).unwrap();
889        assert_eq!(name1.as_deref(), Some("Alice"));
890
891        // Account 2: pubkey + None
892        let pk2 = r.read_var_bytes().unwrap();
893        assert_eq!(pk2.len(), 32);
894        assert_eq!(pk2[0], 0x8e);
895        let name2 = r.read_option(|r| r.read_string()).unwrap();
896        assert!(name2.is_none());
897
898        assert_eq!(r.pos, resp.len());
899    }
900
901    #[test]
902    fn handshake_round_trip() {
903        // Simulate: app sends handshake request, host responds
904        let mut req_msg = Vec::new();
905        req_msg.push(PROTOCOL_DISCRIMINATOR);
906        encode_string(&mut req_msg, "hsk-1");
907        req_msg.push(TAG_HANDSHAKE_REQ);
908        req_msg.push(0); // v1
909        req_msg.push(PROTOCOL_VERSION);
910
911        let (id, tag, req) = decode_message(&req_msg).unwrap();
912        assert!(matches!(req, HostRequest::Handshake { version: 1 }));
913
914        let resp_bytes = encode_response(&id, tag, &HostResponse::HandshakeOk);
915
916        // Verify the response can be decoded
917        let mut r = Reader::new(&resp_bytes);
918        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
919        assert_eq!(r.read_string().unwrap(), "hsk-1");
920        assert_eq!(r.read_u8().unwrap(), TAG_HANDSHAKE_RESP);
921        assert_eq!(r.read_u8().unwrap(), 0); // v1
922        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
923    }
924
925    // -------------------------------------------------------------------
926    // Golden byte vectors — hand-verified against SCALE spec.
927    //
928    // Format: requestId (compact_len + UTF-8), tag (u8), version (u8), payload.
929    // These catch accidental encoding drift.
930    // -------------------------------------------------------------------
931
932    #[test]
933    fn golden_handshake_request() {
934        // discriminator=0x01, requestId = "t1" (compact_len=8, bytes "t1"), tag=0, v1=0, version=1
935        let expected: &[u8] = &[
936            0x01, // PROTOCOL_DISCRIMINATOR
937            0x08, b't', b'1', // compact(2) + "t1"
938            0x00, // TAG_HANDSHAKE_REQ
939            0x00, // v1
940            0x01, // version = 1
941        ];
942        let mut built = Vec::new();
943        built.push(PROTOCOL_DISCRIMINATOR);
944        encode_string(&mut built, "t1");
945        built.push(TAG_HANDSHAKE_REQ);
946        built.push(0);
947        built.push(PROTOCOL_VERSION);
948        assert_eq!(built, expected);
949    }
950
951    #[test]
952    fn golden_handshake_response_ok() {
953        let resp = encode_response("t1", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
954        let expected: &[u8] = &[
955            0x01, // PROTOCOL_DISCRIMINATOR
956            0x08, b't', b'1', // compact(2) + "t1"
957            0x01, // TAG_HANDSHAKE_RESP
958            0x00, // v1
959            0x00, // Result::Ok
960        ];
961        assert_eq!(resp, expected);
962    }
963
964    #[test]
965    fn golden_get_accounts_request() {
966        let expected: &[u8] = &[
967            0x01, // PROTOCOL_DISCRIMINATOR
968            0x08, b'a', b'1', // compact(2) + "a1"
969            28,   // TAG_GET_NON_PRODUCT_ACCOUNTS_REQ
970            0x00, // v1
971        ];
972        let mut built = Vec::new();
973        built.push(PROTOCOL_DISCRIMINATOR);
974        encode_string(&mut built, "a1");
975        built.push(TAG_GET_NON_PRODUCT_ACCOUNTS_REQ);
976        built.push(0);
977        assert_eq!(built, expected);
978    }
979
980    #[test]
981    fn golden_get_accounts_response_empty() {
982        let resp = encode_response(
983            "a1",
984            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
985            &HostResponse::AccountList(vec![]),
986        );
987        let expected: &[u8] = &[
988            0x01, // PROTOCOL_DISCRIMINATOR
989            0x08, b'a', b'1', // compact(2) + "a1"
990            29,   // TAG_GET_NON_PRODUCT_ACCOUNTS_RESP
991            0x00, // v1
992            0x00, // Result::Ok
993            0x00, // Vector len = 0
994        ];
995        assert_eq!(resp, expected);
996    }
997
998    #[test]
999    fn golden_storage_write_response() {
1000        let resp = encode_storage_write_response("s1", false);
1001        let expected: &[u8] = &[
1002            0x01, // PROTOCOL_DISCRIMINATOR
1003            0x08, b's', b'1', // compact(2) + "s1"
1004            15,   // TAG_LOCAL_STORAGE_WRITE_RESP
1005            0x00, // v1
1006            0x00, // Result::Ok(void)
1007        ];
1008        assert_eq!(resp, expected);
1009    }
1010
1011    #[test]
1012    fn golden_storage_clear_response() {
1013        let resp = encode_storage_write_response("s1", true);
1014        let expected: &[u8] = &[
1015            0x01, // PROTOCOL_DISCRIMINATOR
1016            0x08, b's', b'1', // compact(2) + "s1"
1017            17,   // TAG_LOCAL_STORAGE_CLEAR_RESP
1018            0x00, // v1
1019            0x00, // Result::Ok(void)
1020        ];
1021        assert_eq!(resp, expected);
1022    }
1023
1024    #[test]
1025    fn golden_feature_supported_response() {
1026        let resp = encode_feature_response("f1", false);
1027        let expected: &[u8] = &[
1028            0x01, // PROTOCOL_DISCRIMINATOR
1029            0x08, b'f', b'1', // compact(2) + "f1"
1030            3,    // TAG_FEATURE_SUPPORTED_RESP
1031            0x00, // v1
1032            0x00, // Result::Ok
1033            0x00, // false
1034        ];
1035        assert_eq!(resp, expected);
1036    }
1037
1038    #[test]
1039    fn golden_account_status_receive() {
1040        let resp = encode_account_status("c1", true);
1041        let expected: &[u8] = &[
1042            0x01, // PROTOCOL_DISCRIMINATOR
1043            0x08, b'c', b'1', // compact(2) + "c1"
1044            21,   // TAG_ACCOUNT_STATUS_RECEIVE
1045            0x00, // v1
1046            0x01, // connected = true
1047        ];
1048        assert_eq!(resp, expected);
1049    }
1050
1051    #[test]
1052    fn golden_sign_payload_response_ok() {
1053        let sig = [0xAB; 64];
1054        let resp = encode_sign_response("s1", false, &sig);
1055        let mut r = Reader::new(&resp);
1056        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1057        assert_eq!(r.read_string().unwrap(), "s1");
1058        assert_eq!(r.read_u8().unwrap(), TAG_SIGN_PAYLOAD_RESP);
1059        assert_eq!(r.read_u8().unwrap(), 0); // v1
1060        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
1061        assert_eq!(r.read_compact_u32().unwrap(), 0); // id
1062        let sig_bytes = r.read_var_bytes().unwrap();
1063        assert_eq!(sig_bytes, vec![0xAB; 64]);
1064        assert_eq!(r.pos, resp.len());
1065    }
1066
1067    #[test]
1068    fn golden_sign_raw_response_ok() {
1069        let sig = [0xCD; 64];
1070        let resp = encode_sign_response("s2", true, &sig);
1071        let mut r = Reader::new(&resp);
1072        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1073        assert_eq!(r.read_string().unwrap(), "s2");
1074        assert_eq!(r.read_u8().unwrap(), TAG_SIGN_RAW_RESP);
1075        assert_eq!(r.read_u8().unwrap(), 0); // v1
1076        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok
1077        assert_eq!(r.read_compact_u32().unwrap(), 0); // id
1078        let sig_bytes = r.read_var_bytes().unwrap();
1079        assert_eq!(sig_bytes, vec![0xCD; 64]);
1080    }
1081
1082    #[test]
1083    fn golden_sign_error_response() {
1084        let resp = encode_sign_error("s3", false);
1085        let expected: &[u8] = &[
1086            0x01, // PROTOCOL_DISCRIMINATOR
1087            0x08, b's', b'3', // compact(2) + "s3"
1088            37,   // TAG_SIGN_PAYLOAD_RESP
1089            0x00, // v1
1090            0x01, // Result::Err
1091            0x00, // Rejected variant
1092        ];
1093        assert_eq!(resp, expected);
1094    }
1095
1096    #[test]
1097    fn decode_sign_payload_request() {
1098        let mut msg = Vec::new();
1099        msg.push(PROTOCOL_DISCRIMINATOR);
1100        encode_string(&mut msg, "sign-1");
1101        msg.push(TAG_SIGN_PAYLOAD_REQ);
1102        msg.push(0); // v1
1103        encode_var_bytes(&mut msg, &[0xAA; 32]); // publicKey
1104        msg.extend_from_slice(b"extrinsic-payload"); // payload
1105        let (id, tag, req) = decode_message(&msg).unwrap();
1106        assert_eq!(id, "sign-1");
1107        assert_eq!(tag, TAG_SIGN_PAYLOAD_REQ);
1108        match req {
1109            HostRequest::SignPayload {
1110                public_key,
1111                payload,
1112            } => {
1113                assert_eq!(public_key, vec![0xAA; 32]);
1114                assert_eq!(payload, b"extrinsic-payload");
1115            }
1116            _ => panic!("expected SignPayload"),
1117        }
1118    }
1119
1120    #[test]
1121    fn decode_sign_raw_request() {
1122        let mut msg = Vec::new();
1123        msg.push(PROTOCOL_DISCRIMINATOR);
1124        encode_string(&mut msg, "sign-2");
1125        msg.push(TAG_SIGN_RAW_REQ);
1126        msg.push(0); // v1
1127        encode_var_bytes(&mut msg, &[0xBB; 32]); // publicKey
1128        msg.extend_from_slice(b"raw-data"); // data
1129        let (id, tag, req) = decode_message(&msg).unwrap();
1130        assert_eq!(id, "sign-2");
1131        assert_eq!(tag, TAG_SIGN_RAW_REQ);
1132        match req {
1133            HostRequest::SignRaw { public_key, data } => {
1134                assert_eq!(public_key, vec![0xBB; 32]);
1135                assert_eq!(data, b"raw-data");
1136            }
1137            _ => panic!("expected SignRaw"),
1138        }
1139    }
1140
1141    // -- Push notification protocol tests --
1142
1143    #[test]
1144    fn decode_push_notification_request_with_deeplink() {
1145        let mut msg = Vec::new();
1146        msg.push(PROTOCOL_DISCRIMINATOR);
1147        encode_string(&mut msg, "pn-1");
1148        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1149        msg.push(0); // v1
1150        encode_string(&mut msg, "Transfer complete");
1151        msg.push(0x01); // Some
1152        encode_string(&mut msg, "https://app.example/tx/123");
1153
1154        let (id, tag, req) = decode_message(&msg).unwrap();
1155        assert_eq!(id, "pn-1");
1156        assert_eq!(tag, TAG_PUSH_NOTIFICATION_REQ);
1157        match req {
1158            HostRequest::PushNotification { text, deeplink } => {
1159                assert_eq!(text, "Transfer complete");
1160                assert_eq!(deeplink.as_deref(), Some("https://app.example/tx/123"));
1161            }
1162            _ => panic!("expected PushNotification"),
1163        }
1164    }
1165
1166    #[test]
1167    fn decode_push_notification_request_without_deeplink() {
1168        let mut msg = Vec::new();
1169        msg.push(PROTOCOL_DISCRIMINATOR);
1170        encode_string(&mut msg, "pn-2");
1171        msg.push(TAG_PUSH_NOTIFICATION_REQ);
1172        msg.push(0); // v1
1173        encode_string(&mut msg, "Hello world");
1174        msg.push(0x00); // None
1175
1176        let (id, tag, req) = decode_message(&msg).unwrap();
1177        assert_eq!(id, "pn-2");
1178        assert_eq!(tag, TAG_PUSH_NOTIFICATION_REQ);
1179        match req {
1180            HostRequest::PushNotification { text, deeplink } => {
1181                assert_eq!(text, "Hello world");
1182                assert!(deeplink.is_none());
1183            }
1184            _ => panic!("expected PushNotification"),
1185        }
1186    }
1187
1188    #[test]
1189    fn encode_push_notification_response_produces_ok() {
1190        let resp = encode_push_notification_response("pn-1");
1191        let mut r = Reader::new(&resp);
1192        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1193        assert_eq!(r.read_string().unwrap(), "pn-1");
1194        assert_eq!(r.read_u8().unwrap(), TAG_PUSH_NOTIFICATION_RESP);
1195        assert_eq!(r.read_u8().unwrap(), 0); // v1
1196        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok(void)
1197        assert_eq!(r.pos, resp.len());
1198    }
1199
1200    #[test]
1201    fn golden_push_notification_response_ok() {
1202        let resp = encode_push_notification_response("t1");
1203        let expected: &[u8] = &[
1204            0x01, // PROTOCOL_DISCRIMINATOR
1205            0x08, b't', b'1', // compact(2) + "t1"
1206            0x05, // TAG_PUSH_NOTIFICATION_RESP
1207            0x00, // v1
1208            0x00, // Result::Ok(void)
1209        ];
1210        assert_eq!(resp, expected);
1211    }
1212
1213    #[test]
1214    fn push_notification_round_trip() {
1215        let mut req_msg = Vec::new();
1216        req_msg.push(PROTOCOL_DISCRIMINATOR);
1217        encode_string(&mut req_msg, "pn-rt");
1218        req_msg.push(TAG_PUSH_NOTIFICATION_REQ);
1219        req_msg.push(0); // v1
1220        encode_string(&mut req_msg, "Test notification");
1221        req_msg.push(0x00); // None deeplink
1222
1223        let (id, _tag, req) = decode_message(&req_msg).unwrap();
1224        assert!(matches!(req, HostRequest::PushNotification { .. }));
1225
1226        let resp_bytes = encode_push_notification_response(&id);
1227
1228        let mut r = Reader::new(&resp_bytes);
1229        assert_eq!(r.read_u8().unwrap(), PROTOCOL_DISCRIMINATOR);
1230        assert_eq!(r.read_string().unwrap(), "pn-rt");
1231        assert_eq!(r.read_u8().unwrap(), TAG_PUSH_NOTIFICATION_RESP);
1232        assert_eq!(r.read_u8().unwrap(), 0); // v1
1233        assert_eq!(r.read_u8().unwrap(), 0); // Result::Ok(void)
1234    }
1235
1236    #[test]
1237    fn golden_storage_read_response_some() {
1238        let resp = encode_storage_read_response("s1", Some(b"hi"));
1239        let expected: &[u8] = &[
1240            PROTOCOL_DISCRIMINATOR,
1241            0x08,
1242            b's',
1243            b'1', // compact(2) + "s1"
1244            13,   // TAG_LOCAL_STORAGE_READ_RESP
1245            0x00, // v1
1246            0x00, // Result::Ok
1247            0x01, // Option::Some
1248            0x08,
1249            b'h',
1250            b'i', // compact(2) + "hi"
1251        ];
1252        assert_eq!(resp, expected);
1253    }
1254
1255    #[test]
1256    fn golden_storage_read_response_none() {
1257        let resp = encode_storage_read_response("s1", None);
1258        let expected: &[u8] = &[
1259            PROTOCOL_DISCRIMINATOR,
1260            0x08,
1261            b's',
1262            b'1', // compact(2) + "s1"
1263            13,   // TAG_LOCAL_STORAGE_READ_RESP
1264            0x00, // v1
1265            0x00, // Result::Ok
1266            0x00, // Option::None
1267        ];
1268        assert_eq!(resp, expected);
1269    }
1270
1271    #[test]
1272    fn test_decode_rejects_missing_discriminator() {
1273        // A valid handshake body with NO discriminator prefix
1274        let mut msg = Vec::new();
1275        encode_string(&mut msg, "t1");
1276        msg.push(TAG_HANDSHAKE_REQ);
1277        msg.push(0);
1278        msg.push(PROTOCOL_VERSION);
1279        assert!(matches!(
1280            decode_message(&msg),
1281            Err(DecodeErr::UnknownProtocol)
1282        ));
1283    }
1284
1285    #[test]
1286    fn test_decode_rejects_wrong_discriminator() {
1287        let mut msg = vec![0x00]; // wrong discriminator
1288        encode_string(&mut msg, "t1");
1289        msg.push(TAG_HANDSHAKE_REQ);
1290        msg.push(0);
1291        msg.push(PROTOCOL_VERSION);
1292        assert!(matches!(
1293            decode_message(&msg),
1294            Err(DecodeErr::UnknownProtocol)
1295        ));
1296    }
1297
1298    #[test]
1299    fn test_decode_accepts_correct_discriminator() {
1300        let mut msg = vec![PROTOCOL_DISCRIMINATOR];
1301        encode_string(&mut msg, "t1");
1302        msg.push(TAG_HANDSHAKE_REQ);
1303        msg.push(0);
1304        msg.push(PROTOCOL_VERSION);
1305        let (req_id, tag, _) = decode_message(&msg).unwrap();
1306        assert_eq!(req_id, "t1");
1307        assert_eq!(tag, TAG_HANDSHAKE_REQ);
1308    }
1309
1310    #[test]
1311    fn test_is_host_sdk_message_true() {
1312        assert!(is_host_sdk_message(&[0x01, 0x08, b't', b'1', 0]));
1313        assert!(is_host_sdk_message(&[0x01])); // single byte == discriminator
1314    }
1315
1316    #[test]
1317    fn test_is_host_sdk_message_false_empty() {
1318        assert!(!is_host_sdk_message(&[]));
1319    }
1320
1321    #[test]
1322    fn test_is_host_sdk_message_false_wrong_byte() {
1323        assert!(!is_host_sdk_message(&[0x00, 0x08]));
1324        assert!(!is_host_sdk_message(&[0x02, 0x08]));
1325        assert!(!is_host_sdk_message(&[0xFF]));
1326    }
1327
1328    #[test]
1329    fn test_all_encode_functions_start_with_discriminator() {
1330        // encode_response - HandshakeOk
1331        let buf = encode_response("r", TAG_HANDSHAKE_REQ, &HostResponse::HandshakeOk);
1332        assert_eq!(buf[0], PROTOCOL_DISCRIMINATOR);
1333
1334        // encode_response - AccountList
1335        let buf = encode_response(
1336            "r",
1337            TAG_GET_NON_PRODUCT_ACCOUNTS_REQ,
1338            &HostResponse::AccountList(vec![]),
1339        );
1340        assert_eq!(buf[0], PROTOCOL_DISCRIMINATOR);
1341
1342        // encode_response - Error
1343        let buf = encode_response("r", TAG_HANDSHAKE_REQ, &HostResponse::Error("e".into()));
1344        assert_eq!(buf[0], PROTOCOL_DISCRIMINATOR);
1345
1346        // encode_feature_response
1347        assert_eq!(
1348            encode_feature_response("r", true)[0],
1349            PROTOCOL_DISCRIMINATOR
1350        );
1351        assert_eq!(
1352            encode_feature_response("r", false)[0],
1353            PROTOCOL_DISCRIMINATOR
1354        );
1355
1356        // encode_account_status
1357        assert_eq!(encode_account_status("r", true)[0], PROTOCOL_DISCRIMINATOR);
1358
1359        // encode_storage_read_response
1360        assert_eq!(
1361            encode_storage_read_response("r", None)[0],
1362            PROTOCOL_DISCRIMINATOR
1363        );
1364        assert_eq!(
1365            encode_storage_read_response("r", Some(b"v"))[0],
1366            PROTOCOL_DISCRIMINATOR
1367        );
1368
1369        // encode_storage_write_response
1370        assert_eq!(
1371            encode_storage_write_response("r", false)[0],
1372            PROTOCOL_DISCRIMINATOR
1373        );
1374        assert_eq!(
1375            encode_storage_write_response("r", true)[0],
1376            PROTOCOL_DISCRIMINATOR
1377        );
1378
1379        // encode_navigate_response
1380        assert_eq!(encode_navigate_response("r")[0], PROTOCOL_DISCRIMINATOR);
1381
1382        // encode_push_notification_response
1383        assert_eq!(
1384            encode_push_notification_response("r")[0],
1385            PROTOCOL_DISCRIMINATOR
1386        );
1387
1388        // encode_sign_response
1389        assert_eq!(
1390            encode_sign_response("r", false, &[0; 64])[0],
1391            PROTOCOL_DISCRIMINATOR
1392        );
1393        assert_eq!(
1394            encode_sign_response("r", true, &[0; 64])[0],
1395            PROTOCOL_DISCRIMINATOR
1396        );
1397
1398        // encode_jsonrpc_send_response
1399        assert_eq!(
1400            encode_jsonrpc_send_response("r", "{}")[0],
1401            PROTOCOL_DISCRIMINATOR
1402        );
1403
1404        // encode_jsonrpc_send_error
1405        assert_eq!(encode_jsonrpc_send_error("r")[0], PROTOCOL_DISCRIMINATOR);
1406
1407        // encode_jsonrpc_sub_receive
1408        assert_eq!(
1409            encode_jsonrpc_sub_receive("r", "{}")[0],
1410            PROTOCOL_DISCRIMINATOR
1411        );
1412
1413        // encode_sign_error
1414        assert_eq!(encode_sign_error("r", false)[0], PROTOCOL_DISCRIMINATOR);
1415        assert_eq!(encode_sign_error("r", true)[0], PROTOCOL_DISCRIMINATOR);
1416    }
1417}