Skip to main content

host_api/
protocol.rs

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