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: {}",
331            b
332        ))),
333    }
334}
335
336/// Write a bool: 0x00=false, 0x01=true.
337pub fn write_bool(writer: &mut impl Write, v: bool) -> Result<(), WalletError> {
338    write_byte(writer, if v { 1 } else { 0 })
339}
340
341/// Read a bool: 0x00=false, 0x01=true.
342pub fn read_bool(reader: &mut impl Read) -> Result<bool, WalletError> {
343    Ok(read_byte(reader)? == 1)
344}
345
346// ---------------------------------------------------------------------------
347// Integer helpers
348// ---------------------------------------------------------------------------
349
350/// Write a u32 as little-endian 4 bytes.
351pub fn write_uint32(writer: &mut impl Write, v: u32) -> Result<(), WalletError> {
352    writer
353        .write_all(&v.to_le_bytes())
354        .map_err(|e| WalletError::Internal(e.to_string()))
355}
356
357/// Read a u32 from little-endian 4 bytes.
358pub fn read_uint32(reader: &mut impl Read) -> Result<u32, WalletError> {
359    let mut buf = [0u8; 4];
360    reader
361        .read_exact(&mut buf)
362        .map_err(|e| WalletError::Internal(e.to_string()))?;
363    Ok(u32::from_le_bytes(buf))
364}
365
366/// Write an optional u32 using varint. None writes NegativeOne sentinel.
367pub fn write_optional_uint32(writer: &mut impl Write, v: Option<u32>) -> Result<(), WalletError> {
368    match v {
369        Some(val) => write_varint(writer, val as u64),
370        None => write_varint(writer, NEGATIVE_ONE),
371    }
372}
373
374/// Read an optional u32 from varint. NegativeOne = None.
375pub fn read_optional_uint32(reader: &mut impl Read) -> Result<Option<u32>, WalletError> {
376    let val = read_varint(reader)?;
377    if val == NEGATIVE_ONE {
378        Ok(None)
379    } else {
380        Ok(Some(val as u32))
381    }
382}
383
384// ---------------------------------------------------------------------------
385// Counterparty encoding
386// ---------------------------------------------------------------------------
387
388/// Write a counterparty: Uninitialized=0x00, Self_=0x0B, Anyone=0x0C,
389/// Other=0x02/0x03 prefix + 32 bytes x-coordinate.
390pub fn write_counterparty(writer: &mut impl Write, c: &Counterparty) -> Result<(), WalletError> {
391    match c.counterparty_type {
392        CounterpartyType::Uninitialized => write_byte(writer, COUNTERPARTY_UNINITIALIZED),
393        CounterpartyType::Self_ => write_byte(writer, COUNTERPARTY_SELF),
394        CounterpartyType::Anyone => write_byte(writer, COUNTERPARTY_ANYONE),
395        CounterpartyType::Other => {
396            let pk = c.public_key.as_ref().ok_or_else(|| {
397                WalletError::Internal("counterparty is Other but no public key".to_string())
398            })?;
399            let compressed = pk.to_der();
400            write_raw_bytes(writer, &compressed)
401        }
402    }
403}
404
405/// Read a counterparty from wire format.
406pub fn read_counterparty(reader: &mut impl Read) -> Result<Counterparty, WalletError> {
407    let flag = read_byte(reader)?;
408    match flag {
409        COUNTERPARTY_UNINITIALIZED => Ok(Counterparty {
410            counterparty_type: CounterpartyType::Uninitialized,
411            public_key: None,
412        }),
413        COUNTERPARTY_SELF => Ok(Counterparty {
414            counterparty_type: CounterpartyType::Self_,
415            public_key: None,
416        }),
417        COUNTERPARTY_ANYONE => Ok(Counterparty {
418            counterparty_type: CounterpartyType::Anyone,
419            public_key: None,
420        }),
421        0x02 | 0x03 => {
422            let mut buf = vec![flag];
423            let rest = read_raw_bytes(reader, 32)?;
424            buf.extend_from_slice(&rest);
425            let pk = PublicKey::from_der_bytes(&buf)?;
426            Ok(Counterparty {
427                counterparty_type: CounterpartyType::Other,
428                public_key: Some(pk),
429            })
430        }
431        _ => Err(WalletError::Internal(format!(
432            "invalid counterparty flag byte: 0x{:02x}",
433            flag
434        ))),
435    }
436}
437
438// ---------------------------------------------------------------------------
439// Protocol encoding
440// ---------------------------------------------------------------------------
441
442/// Write a protocol: security_level byte + protocol name string.
443pub fn write_protocol(writer: &mut impl Write, p: &Protocol) -> Result<(), WalletError> {
444    write_byte(writer, p.security_level)?;
445    write_string(writer, &p.protocol)
446}
447
448/// Read a protocol from wire.
449pub fn read_protocol(reader: &mut impl Read) -> Result<Protocol, WalletError> {
450    let security_level = read_byte(reader)?;
451    let protocol = read_string(reader)?;
452    Ok(Protocol {
453        security_level,
454        protocol,
455    })
456}
457
458// ---------------------------------------------------------------------------
459// Key-related params (common prefix for crypto methods)
460// ---------------------------------------------------------------------------
461
462/// Common parameters for key-related wallet operations.
463pub struct KeyRelatedParams {
464    pub protocol: Protocol,
465    pub key_id: String,
466    pub counterparty: Counterparty,
467    pub privileged: Option<bool>,
468    pub privileged_reason: String,
469}
470
471/// Write the common key-related params prefix.
472pub fn write_key_related_params(
473    writer: &mut impl Write,
474    params: &KeyRelatedParams,
475) -> Result<(), WalletError> {
476    write_protocol(writer, &params.protocol)?;
477    write_string(writer, &params.key_id)?;
478    write_counterparty(writer, &params.counterparty)?;
479    write_privileged_params(writer, params.privileged, &params.privileged_reason)
480}
481
482/// Read the common key-related params prefix.
483pub fn read_key_related_params(reader: &mut impl Read) -> Result<KeyRelatedParams, WalletError> {
484    let protocol = read_protocol(reader)?;
485    let key_id = read_string(reader)?;
486    let counterparty = read_counterparty(reader)?;
487    let (privileged, privileged_reason) = read_privileged_params(reader)?;
488    Ok(KeyRelatedParams {
489        protocol,
490        key_id,
491        counterparty,
492        privileged,
493        privileged_reason,
494    })
495}
496
497// ---------------------------------------------------------------------------
498// Privileged params encoding
499// ---------------------------------------------------------------------------
500
501/// Write privileged flag + optional reason.
502pub fn write_privileged_params(
503    writer: &mut impl Write,
504    privileged: Option<bool>,
505    privileged_reason: &str,
506) -> Result<(), WalletError> {
507    write_optional_bool(writer, privileged)?;
508    if !privileged_reason.is_empty() {
509        write_string(writer, privileged_reason)
510    } else {
511        write_byte(writer, NEGATIVE_ONE_BYTE)
512    }
513}
514
515/// Read privileged flag + optional reason.
516pub fn read_privileged_params(
517    reader: &mut impl Read,
518) -> Result<(Option<bool>, String), WalletError> {
519    let privileged = read_optional_bool(reader)?;
520    let b = read_byte(reader)?;
521    if b == NEGATIVE_ONE_BYTE {
522        return Ok((privileged, String::new()));
523    }
524    // The byte we just read is the first byte of the varint length.
525    // We need to reconstruct the string by "unreading" this byte.
526    // Since we already consumed it, we need to decode it as part of the varint.
527    let len = match b {
528        0xff => {
529            // This case shouldn't happen since we already handled 0xFF above
530            return Ok((privileged, String::new()));
531        }
532        0xfe => {
533            let mut buf = [0u8; 4];
534            reader
535                .read_exact(&mut buf)
536                .map_err(|e| WalletError::Internal(e.to_string()))?;
537            u32::from_le_bytes(buf) as u64
538        }
539        0xfd => {
540            let mut buf = [0u8; 2];
541            reader
542                .read_exact(&mut buf)
543                .map_err(|e| WalletError::Internal(e.to_string()))?;
544            u16::from_le_bytes(buf) as u64
545        }
546        _ => b as u64,
547    };
548    if len == 0 {
549        return Ok((privileged, String::new()));
550    }
551    let mut buf = vec![0u8; len as usize];
552    reader
553        .read_exact(&mut buf)
554        .map_err(|e| WalletError::Internal(e.to_string()))?;
555    let reason = String::from_utf8(buf).map_err(|e| WalletError::Internal(e.to_string()))?;
556    Ok((privileged, reason))
557}
558
559// ---------------------------------------------------------------------------
560// String slice helpers
561// ---------------------------------------------------------------------------
562
563/// Write a string slice (nil-able). Go uses NegativeOne for nil slices.
564pub fn write_string_slice(
565    writer: &mut impl Write,
566    slice: &Option<Vec<String>>,
567) -> Result<(), WalletError> {
568    match slice {
569        None => write_varint(writer, NEGATIVE_ONE),
570        Some(s) => {
571            write_varint(writer, s.len() as u64)?;
572            for item in s {
573                write_string_optional(writer, item)?;
574            }
575            Ok(())
576        }
577    }
578}
579
580/// Read a string slice (nil-able).
581pub fn read_string_slice(reader: &mut impl Read) -> Result<Option<Vec<String>>, WalletError> {
582    let count = read_varint(reader)?;
583    if count == NEGATIVE_ONE {
584        return Ok(None);
585    }
586    let mut result = Vec::with_capacity(count as usize);
587    for _ in 0..count {
588        result.push(read_string_optional(reader)?);
589    }
590    Ok(Some(result))
591}
592
593// ---------------------------------------------------------------------------
594// String map helpers
595// ---------------------------------------------------------------------------
596
597/// Write a sorted string map (key-value pairs).
598pub fn write_string_map(
599    writer: &mut impl Write,
600    map: &HashMap<String, String>,
601) -> Result<(), WalletError> {
602    let mut keys: Vec<&String> = map.keys().collect();
603    keys.sort();
604    write_varint(writer, keys.len() as u64)?;
605    for key in keys {
606        write_string(writer, key)?;
607        write_string(writer, &map[key])?;
608    }
609    Ok(())
610}
611
612/// Read a string map.
613pub fn read_string_map(reader: &mut impl Read) -> Result<HashMap<String, String>, WalletError> {
614    let count = read_varint(reader)?;
615    let mut map = HashMap::with_capacity(count as usize);
616    for _ in 0..count {
617        let key = read_string(reader)?;
618        let value = read_string(reader)?;
619        map.insert(key, value);
620    }
621    Ok(map)
622}
623
624// ---------------------------------------------------------------------------
625// Optional bytes helpers (with flag/options matching Go SDK)
626// ---------------------------------------------------------------------------
627
628/// Write optional bytes with a presence flag byte.
629/// If data is Some and non-empty: write 1 then len-prefixed bytes.
630/// If data is None or empty: write 0.
631pub fn write_optional_bytes_with_flag(
632    writer: &mut impl Write,
633    data: Option<&[u8]>,
634) -> Result<(), WalletError> {
635    match data {
636        Some(b) if !b.is_empty() => {
637            write_byte(writer, 1)?;
638            write_bytes(writer, b)
639        }
640        _ => write_byte(writer, 0),
641    }
642}
643
644/// Read optional bytes with a presence flag byte.
645pub fn read_optional_bytes_with_flag(
646    reader: &mut impl Read,
647) -> Result<Option<Vec<u8>>, WalletError> {
648    let flag = read_byte(reader)?;
649    if flag != 1 {
650        return Ok(None);
651    }
652    let data = read_bytes(reader)?;
653    Ok(Some(data))
654}
655
656/// Write optional bytes with flag but without length prefix (fixed-size like txid).
657pub fn write_optional_bytes_with_flag_fixed(
658    writer: &mut impl Write,
659    data: Option<&[u8]>,
660) -> Result<(), WalletError> {
661    match data {
662        Some(b) if !b.is_empty() => {
663            write_byte(writer, 1)?;
664            write_raw_bytes(writer, b)
665        }
666        _ => write_byte(writer, 0),
667    }
668}
669
670/// Read optional bytes with flag but fixed size (txid = 32 bytes).
671pub fn read_optional_bytes_with_flag_fixed(
672    reader: &mut impl Read,
673    size: usize,
674) -> Result<Option<Vec<u8>>, WalletError> {
675    let flag = read_byte(reader)?;
676    if flag != 1 {
677        return Ok(None);
678    }
679    read_raw_bytes(reader, size).map(Some)
680}
681
682/// Write optional bytes using varint sentinel (NegativeOne = None).
683pub fn write_optional_bytes_varint(
684    writer: &mut impl Write,
685    data: Option<&[u8]>,
686) -> Result<(), WalletError> {
687    match data {
688        Some(b) if !b.is_empty() => write_bytes(writer, b),
689        _ => write_varint(writer, NEGATIVE_ONE),
690    }
691}
692
693/// Read optional bytes using varint sentinel (NegativeOne = None).
694pub fn read_optional_bytes_varint(reader: &mut impl Read) -> Result<Option<Vec<u8>>, WalletError> {
695    let len = read_varint(reader)?;
696    if len == NEGATIVE_ONE || len == 0 {
697        return Ok(None);
698    }
699    let mut buf = vec![0u8; len as usize];
700    reader
701        .read_exact(&mut buf)
702        .map_err(|e| WalletError::Internal(e.to_string()))?;
703    Ok(Some(buf))
704}
705
706// ---------------------------------------------------------------------------
707// Outpoint encoding
708// ---------------------------------------------------------------------------
709
710/// Write an outpoint (txid bytes in display hex order + index as varint).
711/// Expects outpoint in "txid.index" format where txid is display-order hex.
712/// The Go SDK writes outpoint txids in display order on wire
713/// (chainhash stores internal order, WriteBytesReverse converts to display).
714pub fn write_outpoint(writer: &mut impl Write, outpoint: &str) -> Result<(), WalletError> {
715    let parts: Vec<&str> = outpoint.split('.').collect();
716    if parts.len() != 2 {
717        return Err(WalletError::Internal(format!(
718            "invalid outpoint format: {}",
719            outpoint
720        )));
721    }
722    let txid_bytes = hex_decode(parts[0])?;
723    if txid_bytes.len() != 32 {
724        return Err(WalletError::Internal(format!(
725            "invalid txid length: {}",
726            txid_bytes.len()
727        )));
728    }
729    // Write txid in display hex order (same as Go SDK's WriteBytesReverse of internal-order hash)
730    write_raw_bytes(writer, &txid_bytes)?;
731    let index: u32 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
732        WalletError::Internal(format!("invalid outpoint index: {}", e))
733    })?;
734    write_varint(writer, index as u64)
735}
736
737/// Read an outpoint (txid in display hex order + varint index) and return as "txid.index".
738pub fn read_outpoint(reader: &mut impl Read) -> Result<String, WalletError> {
739    let txid_bytes = read_raw_bytes(reader, 32)?;
740    let index = read_varint(reader)? as u32;
741    Ok(format!("{}.{}", hex_encode(&txid_bytes), index))
742}
743
744// ---------------------------------------------------------------------------
745// PublicKey helpers
746// ---------------------------------------------------------------------------
747
748/// Write a compressed public key (33 bytes).
749pub fn write_public_key(writer: &mut impl Write, pk: &PublicKey) -> Result<(), WalletError> {
750    write_raw_bytes(writer, &pk.to_der())
751}
752
753/// Read a compressed public key (33 bytes).
754pub fn read_public_key(reader: &mut impl Read) -> Result<PublicKey, WalletError> {
755    let buf = read_raw_bytes(reader, SIZE_PUB_KEY)?;
756    PublicKey::from_der_bytes(&buf).map_err(|e| WalletError::Internal(e.to_string()))
757}
758
759// ---------------------------------------------------------------------------
760// Hex helpers
761// ---------------------------------------------------------------------------
762
763/// Decode a hex string to bytes.
764pub fn hex_decode(s: &str) -> Result<Vec<u8>, WalletError> {
765    let mut result = Vec::with_capacity(s.len() / 2);
766    let chars: Vec<char> = s.chars().collect();
767    if !chars.len().is_multiple_of(2) {
768        return Err(WalletError::Internal(
769            "hex string has odd length".to_string(),
770        ));
771    }
772    for i in (0..chars.len()).step_by(2) {
773        let hi = hex_nibble(chars[i])?;
774        let lo = hex_nibble(chars[i + 1])?;
775        result.push((hi << 4) | lo);
776    }
777    Ok(result)
778}
779
780fn hex_nibble(c: char) -> Result<u8, WalletError> {
781    match c {
782        '0'..='9' => Ok(c as u8 - b'0'),
783        'a'..='f' => Ok(c as u8 - b'a' + 10),
784        'A'..='F' => Ok(c as u8 - b'A' + 10),
785        _ => Err(WalletError::Internal(format!("invalid hex char: {}", c))),
786    }
787}
788
789/// Encode bytes to lowercase hex string.
790pub fn hex_encode(data: &[u8]) -> String {
791    let mut s = String::with_capacity(data.len() * 2);
792    for b in data {
793        s.push_str(&format!("{:02x}", b));
794    }
795    s
796}
797
798// ---------------------------------------------------------------------------
799// Convenience: serialize to Vec<u8>
800// ---------------------------------------------------------------------------
801
802/// Helper to serialize using a closure that writes to a `Vec<u8>`.
803pub fn serialize_to_vec<F>(f: F) -> Result<Vec<u8>, WalletError>
804where
805    F: FnOnce(&mut Vec<u8>) -> Result<(), WalletError>,
806{
807    let mut buf = Vec::new();
808    f(&mut buf)?;
809    Ok(buf)
810}