Skip to main content

bsv/wallet/serializer/
mod.rs

1//! Wire protocol serialization for all 28 wallet methods.
2//!
3//! Provides binary serialization/deserialization matching the Go SDK
4//! wallet/serializer package, using Bitcoin-style varints and specific
5//! sentinel values for optional fields.
6
7use std::collections::HashMap;
8use std::io::{Read, Write};
9
10use crate::primitives::public_key::PublicKey;
11use crate::wallet::error::WalletError;
12use crate::wallet::types::{Counterparty, CounterpartyType, Protocol};
13
14pub mod frame;
15
16pub mod abort_action;
17pub mod acquire_certificate;
18pub mod authenticated;
19pub mod certificate_ser;
20pub mod create_action;
21pub mod create_hmac;
22pub mod create_signature;
23pub mod decrypt;
24pub mod discover_by_attributes;
25pub mod discover_by_identity_key;
26pub mod discover_certificates_result;
27pub mod encrypt;
28pub mod get_header;
29pub mod get_height;
30pub mod get_network;
31pub mod get_public_key;
32pub mod get_version;
33pub mod internalize_action;
34pub mod list_actions;
35pub mod list_certificates;
36pub mod list_outputs;
37pub mod prove_certificate;
38pub mod relinquish_certificate;
39pub mod relinquish_output;
40pub mod reveal_counterparty_key_linkage;
41pub mod reveal_specific_key_linkage;
42pub mod sign_action;
43pub mod verify_hmac;
44pub mod verify_signature;
45
46// ---------------------------------------------------------------------------
47// Constants
48// ---------------------------------------------------------------------------
49
50/// Sentinel value for "negative one" / "none" in varint encoding.
51const NEGATIVE_ONE: u64 = u64::MAX;
52
53/// Sentinel byte for "none" in single-byte optional fields.
54const NEGATIVE_ONE_BYTE: u8 = 0xFF;
55
56/// Counterparty type codes matching Go SDK.
57const COUNTERPARTY_UNINITIALIZED: u8 = 0x00;
58const COUNTERPARTY_SELF: u8 = 0x0B;
59const COUNTERPARTY_ANYONE: u8 = 0x0C;
60
61/// Size of a compressed public key.
62const SIZE_PUB_KEY: usize = 33;
63
64/// Size of certificate type and serial number fields.
65const SIZE_TYPE: usize = 32;
66const SIZE_SERIAL: usize = 32;
67
68/// Call byte constants for each wallet method (request frame).
69pub const CALL_CREATE_ACTION: u8 = 1;
70pub const CALL_SIGN_ACTION: u8 = 2;
71pub const CALL_ABORT_ACTION: u8 = 3;
72pub const CALL_LIST_ACTIONS: u8 = 4;
73pub const CALL_INTERNALIZE_ACTION: u8 = 5;
74pub const CALL_LIST_OUTPUTS: u8 = 6;
75pub const CALL_RELINQUISH_OUTPUT: u8 = 7;
76pub const CALL_GET_PUBLIC_KEY: u8 = 8;
77pub const CALL_REVEAL_COUNTERPARTY_KEY_LINKAGE: u8 = 9;
78pub const CALL_REVEAL_SPECIFIC_KEY_LINKAGE: u8 = 10;
79pub const CALL_ENCRYPT: u8 = 11;
80pub const CALL_DECRYPT: u8 = 12;
81pub const CALL_CREATE_HMAC: u8 = 13;
82pub const CALL_VERIFY_HMAC: u8 = 14;
83pub const CALL_CREATE_SIGNATURE: u8 = 15;
84pub const CALL_VERIFY_SIGNATURE: u8 = 16;
85pub const CALL_ACQUIRE_CERTIFICATE: u8 = 17;
86pub const CALL_LIST_CERTIFICATES: u8 = 18;
87pub const CALL_PROVE_CERTIFICATE: u8 = 19;
88pub const CALL_RELINQUISH_CERTIFICATE: u8 = 20;
89pub const CALL_DISCOVER_BY_IDENTITY_KEY: u8 = 21;
90pub const CALL_DISCOVER_BY_ATTRIBUTES: u8 = 22;
91pub const CALL_IS_AUTHENTICATED: u8 = 23;
92pub const CALL_WAIT_FOR_AUTHENTICATION: u8 = 24;
93pub const CALL_GET_HEIGHT: u8 = 25;
94pub const CALL_GET_HEADER_FOR_HEIGHT: u8 = 26;
95pub const CALL_GET_NETWORK: u8 = 27;
96pub const CALL_GET_VERSION: u8 = 28;
97
98// ---------------------------------------------------------------------------
99// VarInt helpers (Bitcoin-style)
100// ---------------------------------------------------------------------------
101
102/// Write a Bitcoin-style variable-length integer.
103pub fn write_varint(writer: &mut impl Write, value: u64) -> Result<(), WalletError> {
104    let bytes = varint_bytes(value);
105    writer
106        .write_all(&bytes)
107        .map_err(|e| WalletError::Internal(e.to_string()))
108}
109
110/// Read a Bitcoin-style variable-length integer.
111pub fn read_varint(reader: &mut impl Read) -> Result<u64, WalletError> {
112    let mut first = [0u8; 1];
113    reader
114        .read_exact(&mut first)
115        .map_err(|e| WalletError::Internal(e.to_string()))?;
116    match first[0] {
117        0xff => {
118            let mut buf = [0u8; 8];
119            reader
120                .read_exact(&mut buf)
121                .map_err(|e| WalletError::Internal(e.to_string()))?;
122            Ok(u64::from_le_bytes(buf))
123        }
124        0xfe => {
125            let mut buf = [0u8; 4];
126            reader
127                .read_exact(&mut buf)
128                .map_err(|e| WalletError::Internal(e.to_string()))?;
129            Ok(u32::from_le_bytes(buf) as u64)
130        }
131        0xfd => {
132            let mut buf = [0u8; 2];
133            reader
134                .read_exact(&mut buf)
135                .map_err(|e| WalletError::Internal(e.to_string()))?;
136            Ok(u16::from_le_bytes(buf) as u64)
137        }
138        b => Ok(b as u64),
139    }
140}
141
142/// Encode a u64 as Bitcoin-style varint bytes.
143fn varint_bytes(value: u64) -> Vec<u8> {
144    if value < 0xfd {
145        vec![value as u8]
146    } else if value < 0x10000 {
147        let mut buf = vec![0xfd, 0, 0];
148        buf[1..3].copy_from_slice(&(value as u16).to_le_bytes());
149        buf
150    } else if value < 0x100000000 {
151        let mut buf = vec![0xfe, 0, 0, 0, 0];
152        buf[1..5].copy_from_slice(&(value as u32).to_le_bytes());
153        buf
154    } else {
155        let mut buf = vec![0xff, 0, 0, 0, 0, 0, 0, 0, 0];
156        buf[1..9].copy_from_slice(&value.to_le_bytes());
157        buf
158    }
159}
160
161// ---------------------------------------------------------------------------
162// Byte helpers
163// ---------------------------------------------------------------------------
164
165/// Write a single byte.
166pub fn write_byte(writer: &mut impl Write, b: u8) -> Result<(), WalletError> {
167    writer
168        .write_all(&[b])
169        .map_err(|e| WalletError::Internal(e.to_string()))
170}
171
172/// Read a single byte.
173pub fn read_byte(reader: &mut impl Read) -> Result<u8, WalletError> {
174    let mut buf = [0u8; 1];
175    reader
176        .read_exact(&mut buf)
177        .map_err(|e| WalletError::Internal(e.to_string()))?;
178    Ok(buf[0])
179}
180
181/// Write varint-length-prefixed bytes.
182pub fn write_bytes(writer: &mut impl Write, data: &[u8]) -> Result<(), WalletError> {
183    write_varint(writer, data.len() as u64)?;
184    writer
185        .write_all(data)
186        .map_err(|e| WalletError::Internal(e.to_string()))
187}
188
189/// Read varint-length-prefixed bytes.
190pub fn read_bytes(reader: &mut impl Read) -> Result<Vec<u8>, WalletError> {
191    let len = read_varint(reader)?;
192    if len == 0 {
193        return Ok(Vec::new());
194    }
195    let mut buf = vec![0u8; len as usize];
196    reader
197        .read_exact(&mut buf)
198        .map_err(|e| WalletError::Internal(e.to_string()))?;
199    Ok(buf)
200}
201
202/// Write raw bytes without length prefix.
203pub fn write_raw_bytes(writer: &mut impl Write, data: &[u8]) -> Result<(), WalletError> {
204    writer
205        .write_all(data)
206        .map_err(|e| WalletError::Internal(e.to_string()))
207}
208
209/// Read exactly n raw bytes.
210pub fn read_raw_bytes(reader: &mut impl Read, n: usize) -> Result<Vec<u8>, WalletError> {
211    let mut buf = vec![0u8; n];
212    reader
213        .read_exact(&mut buf)
214        .map_err(|e| WalletError::Internal(e.to_string()))?;
215    Ok(buf)
216}
217
218/// Read n raw bytes and reverse them (for txid display order).
219pub fn read_raw_bytes_reverse(reader: &mut impl Read, n: usize) -> Result<Vec<u8>, WalletError> {
220    let mut buf = read_raw_bytes(reader, n)?;
221    buf.reverse();
222    Ok(buf)
223}
224
225/// Write bytes in reversed order (for txid display order).
226pub fn write_raw_bytes_reverse(writer: &mut impl Write, data: &[u8]) -> Result<(), WalletError> {
227    let mut reversed = data.to_vec();
228    reversed.reverse();
229    write_raw_bytes(writer, &reversed)
230}
231
232// ---------------------------------------------------------------------------
233// String helpers
234// ---------------------------------------------------------------------------
235
236/// Write a length-prefixed UTF-8 string.
237pub fn write_string(writer: &mut impl Write, s: &str) -> Result<(), WalletError> {
238    let bytes = s.as_bytes();
239    write_varint(writer, bytes.len() as u64)?;
240    writer
241        .write_all(bytes)
242        .map_err(|e| WalletError::Internal(e.to_string()))
243}
244
245/// Read a length-prefixed UTF-8 string.
246pub fn read_string(reader: &mut impl Read) -> Result<String, WalletError> {
247    let len = read_varint(reader)?;
248    if len == NEGATIVE_ONE || len == 0 {
249        return Ok(String::new());
250    }
251    let mut buf = vec![0u8; len as usize];
252    reader
253        .read_exact(&mut buf)
254        .map_err(|e| WalletError::Internal(e.to_string()))?;
255    String::from_utf8(buf).map_err(|e| WalletError::Internal(e.to_string()))
256}
257
258/// Write an optional string. None writes the full varint NegativeOne sentinel.
259pub fn write_optional_string(
260    writer: &mut impl Write,
261    s: &Option<String>,
262) -> Result<(), WalletError> {
263    match s {
264        Some(ref val) if !val.is_empty() => write_string(writer, val),
265        _ => write_varint(writer, NEGATIVE_ONE),
266    }
267}
268
269/// Read an optional string. Varint NegativeOne = None.
270pub fn read_optional_string(reader: &mut impl Read) -> Result<Option<String>, WalletError> {
271    let len = read_varint(reader)?;
272    if len == NEGATIVE_ONE {
273        return Ok(None);
274    }
275    if len == 0 {
276        return Ok(Some(String::new()));
277    }
278    let mut buf = vec![0u8; len as usize];
279    reader
280        .read_exact(&mut buf)
281        .map_err(|e| WalletError::Internal(e.to_string()))?;
282    let s = String::from_utf8(buf).map_err(|e| WalletError::Internal(e.to_string()))?;
283    Ok(Some(s))
284}
285
286/// Write a string that uses the full varint negative-one sentinel for empty.
287/// This matches Go SDK's WriteOptionalString behavior (empty string -> NegativeOne).
288pub fn write_string_optional(writer: &mut impl Write, s: &str) -> Result<(), WalletError> {
289    if s.is_empty() {
290        write_varint(writer, NEGATIVE_ONE)
291    } else {
292        write_string(writer, s)
293    }
294}
295
296/// Read a string that uses NegativeOne sentinel for None (returns empty string for None).
297pub fn read_string_optional(reader: &mut impl Read) -> Result<String, WalletError> {
298    let len = read_varint(reader)?;
299    if len == NEGATIVE_ONE || len == 0 {
300        return Ok(String::new());
301    }
302    let mut buf = vec![0u8; len as usize];
303    reader
304        .read_exact(&mut buf)
305        .map_err(|e| WalletError::Internal(e.to_string()))?;
306    String::from_utf8(buf).map_err(|e| WalletError::Internal(e.to_string()))
307}
308
309// ---------------------------------------------------------------------------
310// Bool helpers
311// ---------------------------------------------------------------------------
312
313/// Write an optional bool: 0x00=false, 0x01=true, 0xFF=none.
314pub fn write_optional_bool(writer: &mut impl Write, v: Option<bool>) -> Result<(), WalletError> {
315    match v {
316        None => write_byte(writer, NEGATIVE_ONE_BYTE),
317        Some(true) => write_byte(writer, 1),
318        Some(false) => write_byte(writer, 0),
319    }
320}
321
322/// Read an optional bool: 0x00=false, 0x01=true, 0xFF=none.
323pub fn read_optional_bool(reader: &mut impl Read) -> Result<Option<bool>, WalletError> {
324    let b = read_byte(reader)?;
325    match b {
326        0xFF => Ok(None),
327        0 => Ok(Some(false)),
328        1 => Ok(Some(true)),
329        _ => Err(WalletError::Internal(format!(
330            "invalid optional bool byte: {b}"
331        ))),
332    }
333}
334
335/// Write a bool: 0x00=false, 0x01=true.
336pub fn write_bool(writer: &mut impl Write, v: bool) -> Result<(), WalletError> {
337    write_byte(writer, if v { 1 } else { 0 })
338}
339
340/// Read a bool: 0x00=false, 0x01=true.
341pub fn read_bool(reader: &mut impl Read) -> Result<bool, WalletError> {
342    Ok(read_byte(reader)? == 1)
343}
344
345// ---------------------------------------------------------------------------
346// Integer helpers
347// ---------------------------------------------------------------------------
348
349/// Write a u32 as little-endian 4 bytes.
350pub fn write_uint32(writer: &mut impl Write, v: u32) -> Result<(), WalletError> {
351    writer
352        .write_all(&v.to_le_bytes())
353        .map_err(|e| WalletError::Internal(e.to_string()))
354}
355
356/// Read a u32 from little-endian 4 bytes.
357pub fn read_uint32(reader: &mut impl Read) -> Result<u32, WalletError> {
358    let mut buf = [0u8; 4];
359    reader
360        .read_exact(&mut buf)
361        .map_err(|e| WalletError::Internal(e.to_string()))?;
362    Ok(u32::from_le_bytes(buf))
363}
364
365/// Write an optional u32 using varint. None writes NegativeOne sentinel.
366pub fn write_optional_uint32(writer: &mut impl Write, v: Option<u32>) -> Result<(), WalletError> {
367    match v {
368        Some(val) => write_varint(writer, val as u64),
369        None => write_varint(writer, NEGATIVE_ONE),
370    }
371}
372
373/// Read an optional u32 from varint. NegativeOne = None.
374pub fn read_optional_uint32(reader: &mut impl Read) -> Result<Option<u32>, WalletError> {
375    let val = read_varint(reader)?;
376    if val == NEGATIVE_ONE {
377        Ok(None)
378    } else {
379        Ok(Some(val as u32))
380    }
381}
382
383// ---------------------------------------------------------------------------
384// Counterparty encoding
385// ---------------------------------------------------------------------------
386
387/// Write a counterparty: Uninitialized=0x00, Self_=0x0B, Anyone=0x0C,
388/// Other=0x02/0x03 prefix + 32 bytes x-coordinate.
389pub fn write_counterparty(writer: &mut impl Write, c: &Counterparty) -> Result<(), WalletError> {
390    match c.counterparty_type {
391        CounterpartyType::Uninitialized => write_byte(writer, COUNTERPARTY_UNINITIALIZED),
392        CounterpartyType::Self_ => write_byte(writer, COUNTERPARTY_SELF),
393        CounterpartyType::Anyone => write_byte(writer, COUNTERPARTY_ANYONE),
394        CounterpartyType::Other => {
395            let pk = c.public_key.as_ref().ok_or_else(|| {
396                WalletError::Internal("counterparty is Other but no public key".to_string())
397            })?;
398            let compressed = pk.to_der();
399            write_raw_bytes(writer, &compressed)
400        }
401    }
402}
403
404/// Read a counterparty from wire format.
405pub fn read_counterparty(reader: &mut impl Read) -> Result<Counterparty, WalletError> {
406    let flag = read_byte(reader)?;
407    match flag {
408        COUNTERPARTY_UNINITIALIZED => Ok(Counterparty {
409            counterparty_type: CounterpartyType::Uninitialized,
410            public_key: None,
411        }),
412        COUNTERPARTY_SELF => Ok(Counterparty {
413            counterparty_type: CounterpartyType::Self_,
414            public_key: None,
415        }),
416        COUNTERPARTY_ANYONE => Ok(Counterparty {
417            counterparty_type: CounterpartyType::Anyone,
418            public_key: None,
419        }),
420        0x02 | 0x03 => {
421            let mut buf = vec![flag];
422            let rest = read_raw_bytes(reader, 32)?;
423            buf.extend_from_slice(&rest);
424            let pk = PublicKey::from_der_bytes(&buf)?;
425            Ok(Counterparty {
426                counterparty_type: CounterpartyType::Other,
427                public_key: Some(pk),
428            })
429        }
430        _ => Err(WalletError::Internal(format!(
431            "invalid counterparty flag byte: 0x{flag:02x}"
432        ))),
433    }
434}
435
436// ---------------------------------------------------------------------------
437// Protocol encoding
438// ---------------------------------------------------------------------------
439
440/// Write a protocol: security_level byte + protocol name string.
441pub fn write_protocol(writer: &mut impl Write, p: &Protocol) -> Result<(), WalletError> {
442    write_byte(writer, p.security_level)?;
443    write_string(writer, &p.protocol)
444}
445
446/// Read a protocol from wire.
447pub fn read_protocol(reader: &mut impl Read) -> Result<Protocol, WalletError> {
448    let security_level = read_byte(reader)?;
449    let protocol = read_string(reader)?;
450    Ok(Protocol {
451        security_level,
452        protocol,
453    })
454}
455
456// ---------------------------------------------------------------------------
457// Key-related params (common prefix for crypto methods)
458// ---------------------------------------------------------------------------
459
460/// Common parameters for key-related wallet operations.
461pub struct KeyRelatedParams {
462    pub protocol: Protocol,
463    pub key_id: String,
464    pub counterparty: Counterparty,
465    pub privileged: Option<bool>,
466    pub privileged_reason: String,
467}
468
469/// Write the common key-related params prefix.
470pub fn write_key_related_params(
471    writer: &mut impl Write,
472    params: &KeyRelatedParams,
473) -> Result<(), WalletError> {
474    write_protocol(writer, &params.protocol)?;
475    write_string(writer, &params.key_id)?;
476    write_counterparty(writer, &params.counterparty)?;
477    write_privileged_params(writer, params.privileged, &params.privileged_reason)
478}
479
480/// Read the common key-related params prefix.
481pub fn read_key_related_params(reader: &mut impl Read) -> Result<KeyRelatedParams, WalletError> {
482    let protocol = read_protocol(reader)?;
483    let key_id = read_string(reader)?;
484    let counterparty = read_counterparty(reader)?;
485    let (privileged, privileged_reason) = read_privileged_params(reader)?;
486    Ok(KeyRelatedParams {
487        protocol,
488        key_id,
489        counterparty,
490        privileged,
491        privileged_reason,
492    })
493}
494
495// ---------------------------------------------------------------------------
496// Privileged params encoding
497// ---------------------------------------------------------------------------
498
499/// Write privileged flag + optional reason.
500pub fn write_privileged_params(
501    writer: &mut impl Write,
502    privileged: Option<bool>,
503    privileged_reason: &str,
504) -> Result<(), WalletError> {
505    write_optional_bool(writer, privileged)?;
506    if !privileged_reason.is_empty() {
507        write_string(writer, privileged_reason)
508    } else {
509        write_byte(writer, NEGATIVE_ONE_BYTE)
510    }
511}
512
513/// Read privileged flag + optional reason.
514pub fn read_privileged_params(
515    reader: &mut impl Read,
516) -> Result<(Option<bool>, String), WalletError> {
517    let privileged = read_optional_bool(reader)?;
518    let b = read_byte(reader)?;
519    if b == NEGATIVE_ONE_BYTE {
520        return Ok((privileged, String::new()));
521    }
522    // The byte we just read is the first byte of the varint length.
523    // We need to reconstruct the string by "unreading" this byte.
524    // Since we already consumed it, we need to decode it as part of the varint.
525    let len = match b {
526        0xff => {
527            // This case shouldn't happen since we already handled 0xFF above
528            return Ok((privileged, String::new()));
529        }
530        0xfe => {
531            let mut buf = [0u8; 4];
532            reader
533                .read_exact(&mut buf)
534                .map_err(|e| WalletError::Internal(e.to_string()))?;
535            u32::from_le_bytes(buf) as u64
536        }
537        0xfd => {
538            let mut buf = [0u8; 2];
539            reader
540                .read_exact(&mut buf)
541                .map_err(|e| WalletError::Internal(e.to_string()))?;
542            u16::from_le_bytes(buf) as u64
543        }
544        _ => b as u64,
545    };
546    if len == 0 {
547        return Ok((privileged, String::new()));
548    }
549    let mut buf = vec![0u8; len as usize];
550    reader
551        .read_exact(&mut buf)
552        .map_err(|e| WalletError::Internal(e.to_string()))?;
553    let reason = String::from_utf8(buf).map_err(|e| WalletError::Internal(e.to_string()))?;
554    Ok((privileged, reason))
555}
556
557// ---------------------------------------------------------------------------
558// String slice helpers
559// ---------------------------------------------------------------------------
560
561/// Write a string slice (nil-able). Go uses NegativeOne for nil slices.
562pub fn write_string_slice(
563    writer: &mut impl Write,
564    slice: &Option<Vec<String>>,
565) -> Result<(), WalletError> {
566    match slice {
567        None => write_varint(writer, NEGATIVE_ONE),
568        Some(s) => {
569            write_varint(writer, s.len() as u64)?;
570            for item in s {
571                write_string_optional(writer, item)?;
572            }
573            Ok(())
574        }
575    }
576}
577
578/// Read a string slice (nil-able).
579pub fn read_string_slice(reader: &mut impl Read) -> Result<Option<Vec<String>>, WalletError> {
580    let count = read_varint(reader)?;
581    if count == NEGATIVE_ONE {
582        return Ok(None);
583    }
584    let mut result = Vec::with_capacity(count as usize);
585    for _ in 0..count {
586        result.push(read_string_optional(reader)?);
587    }
588    Ok(Some(result))
589}
590
591// ---------------------------------------------------------------------------
592// String map helpers
593// ---------------------------------------------------------------------------
594
595/// Write a sorted string map (key-value pairs).
596pub fn write_string_map(
597    writer: &mut impl Write,
598    map: &HashMap<String, String>,
599) -> Result<(), WalletError> {
600    let mut keys: Vec<&String> = map.keys().collect();
601    keys.sort();
602    write_varint(writer, keys.len() as u64)?;
603    for key in keys {
604        write_string(writer, key)?;
605        write_string(writer, &map[key])?;
606    }
607    Ok(())
608}
609
610/// Read a string map.
611pub fn read_string_map(reader: &mut impl Read) -> Result<HashMap<String, String>, WalletError> {
612    let count = read_varint(reader)?;
613    let mut map = HashMap::with_capacity(count as usize);
614    for _ in 0..count {
615        let key = read_string(reader)?;
616        let value = read_string(reader)?;
617        map.insert(key, value);
618    }
619    Ok(map)
620}
621
622// ---------------------------------------------------------------------------
623// Optional bytes helpers (with flag/options matching Go SDK)
624// ---------------------------------------------------------------------------
625
626/// Write optional bytes with a presence flag byte.
627/// If data is Some and non-empty: write 1 then len-prefixed bytes.
628/// If data is None or empty: write 0.
629pub fn write_optional_bytes_with_flag(
630    writer: &mut impl Write,
631    data: Option<&[u8]>,
632) -> Result<(), WalletError> {
633    match data {
634        Some(b) if !b.is_empty() => {
635            write_byte(writer, 1)?;
636            write_bytes(writer, b)
637        }
638        _ => write_byte(writer, 0),
639    }
640}
641
642/// Read optional bytes with a presence flag byte.
643pub fn read_optional_bytes_with_flag(
644    reader: &mut impl Read,
645) -> Result<Option<Vec<u8>>, WalletError> {
646    let flag = read_byte(reader)?;
647    if flag != 1 {
648        return Ok(None);
649    }
650    let data = read_bytes(reader)?;
651    Ok(Some(data))
652}
653
654/// Write optional bytes with flag but without length prefix (fixed-size like txid).
655pub fn write_optional_bytes_with_flag_fixed(
656    writer: &mut impl Write,
657    data: Option<&[u8]>,
658) -> Result<(), WalletError> {
659    match data {
660        Some(b) if !b.is_empty() => {
661            write_byte(writer, 1)?;
662            write_raw_bytes(writer, b)
663        }
664        _ => write_byte(writer, 0),
665    }
666}
667
668/// Read optional bytes with flag but fixed size (txid = 32 bytes).
669pub fn read_optional_bytes_with_flag_fixed(
670    reader: &mut impl Read,
671    size: usize,
672) -> Result<Option<Vec<u8>>, WalletError> {
673    let flag = read_byte(reader)?;
674    if flag != 1 {
675        return Ok(None);
676    }
677    read_raw_bytes(reader, size).map(Some)
678}
679
680/// Write optional bytes using varint sentinel (NegativeOne = None).
681pub fn write_optional_bytes_varint(
682    writer: &mut impl Write,
683    data: Option<&[u8]>,
684) -> Result<(), WalletError> {
685    match data {
686        Some(b) if !b.is_empty() => write_bytes(writer, b),
687        _ => write_varint(writer, NEGATIVE_ONE),
688    }
689}
690
691/// Read optional bytes using varint sentinel (NegativeOne = None).
692pub fn read_optional_bytes_varint(reader: &mut impl Read) -> Result<Option<Vec<u8>>, WalletError> {
693    let len = read_varint(reader)?;
694    if len == NEGATIVE_ONE || len == 0 {
695        return Ok(None);
696    }
697    let mut buf = vec![0u8; len as usize];
698    reader
699        .read_exact(&mut buf)
700        .map_err(|e| WalletError::Internal(e.to_string()))?;
701    Ok(Some(buf))
702}
703
704// ---------------------------------------------------------------------------
705// Outpoint encoding
706// ---------------------------------------------------------------------------
707
708/// Write an outpoint (txid bytes in display hex order + index as varint).
709/// Expects outpoint in "txid.index" format where txid is display-order hex.
710/// The Go SDK writes outpoint txids in display order on wire
711/// (chainhash stores internal order, WriteBytesReverse converts to display).
712pub fn write_outpoint(writer: &mut impl Write, outpoint: &str) -> Result<(), WalletError> {
713    let parts: Vec<&str> = outpoint.split('.').collect();
714    if parts.len() != 2 {
715        return Err(WalletError::Internal(format!(
716            "invalid outpoint format: {outpoint}"
717        )));
718    }
719    let txid_bytes = hex_decode(parts[0])?;
720    if txid_bytes.len() != 32 {
721        return Err(WalletError::Internal(format!(
722            "invalid txid length: {}",
723            txid_bytes.len()
724        )));
725    }
726    // Write txid in display hex order (same as Go SDK's WriteBytesReverse of internal-order hash)
727    write_raw_bytes(writer, &txid_bytes)?;
728    let index: u32 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
729        WalletError::Internal(format!("invalid outpoint index: {e}"))
730    })?;
731    write_varint(writer, index as u64)
732}
733
734/// Read an outpoint (txid in display hex order + varint index) and return as "txid.index".
735pub fn read_outpoint(reader: &mut impl Read) -> Result<String, WalletError> {
736    let txid_bytes = read_raw_bytes(reader, 32)?;
737    let index = read_varint(reader)? as u32;
738    Ok(format!("{}.{}", hex_encode(&txid_bytes), index))
739}
740
741// ---------------------------------------------------------------------------
742// PublicKey helpers
743// ---------------------------------------------------------------------------
744
745/// Write a compressed public key (33 bytes).
746pub fn write_public_key(writer: &mut impl Write, pk: &PublicKey) -> Result<(), WalletError> {
747    write_raw_bytes(writer, &pk.to_der())
748}
749
750/// Read a compressed public key (33 bytes).
751pub fn read_public_key(reader: &mut impl Read) -> Result<PublicKey, WalletError> {
752    let buf = read_raw_bytes(reader, SIZE_PUB_KEY)?;
753    PublicKey::from_der_bytes(&buf).map_err(|e| WalletError::Internal(e.to_string()))
754}
755
756// ---------------------------------------------------------------------------
757// Hex helpers
758// ---------------------------------------------------------------------------
759
760/// Decode a hex string to bytes.
761pub fn hex_decode(s: &str) -> Result<Vec<u8>, WalletError> {
762    let mut result = Vec::with_capacity(s.len() / 2);
763    let chars: Vec<char> = s.chars().collect();
764    if !chars.len().is_multiple_of(2) {
765        return Err(WalletError::Internal(
766            "hex string has odd length".to_string(),
767        ));
768    }
769    for i in (0..chars.len()).step_by(2) {
770        let hi = hex_nibble(chars[i])?;
771        let lo = hex_nibble(chars[i + 1])?;
772        result.push((hi << 4) | lo);
773    }
774    Ok(result)
775}
776
777fn hex_nibble(c: char) -> Result<u8, WalletError> {
778    match c {
779        '0'..='9' => Ok(c as u8 - b'0'),
780        'a'..='f' => Ok(c as u8 - b'a' + 10),
781        'A'..='F' => Ok(c as u8 - b'A' + 10),
782        _ => Err(WalletError::Internal(format!("invalid hex char: {c}"))),
783    }
784}
785
786/// Encode bytes to lowercase hex string.
787pub fn hex_encode(data: &[u8]) -> String {
788    let mut s = String::with_capacity(data.len() * 2);
789    for b in data {
790        s.push_str(&format!("{b:02x}"));
791    }
792    s
793}
794
795// ---------------------------------------------------------------------------
796// Convenience: serialize to Vec<u8>
797// ---------------------------------------------------------------------------
798
799/// Helper to serialize using a closure that writes to a `Vec<u8>`.
800pub fn serialize_to_vec<F>(f: F) -> Result<Vec<u8>, WalletError>
801where
802    F: FnOnce(&mut Vec<u8>) -> Result<(), WalletError>,
803{
804    let mut buf = Vec::new();
805    f(&mut buf)?;
806    Ok(buf)
807}