Skip to main content

bsv/wallet/
types.rs

1//! Semantic type aliases and core types for the wallet module.
2//!
3//! Mirrors the Go SDK wallet/wallet.go and wallet/interfaces.go type
4//! definitions, providing strongly-typed aliases for protocol parameters.
5
6use crate::primitives::private_key::PrivateKey;
7use crate::primitives::public_key::PublicKey;
8
9// ---------------------------------------------------------------------------
10// Semantic type aliases
11// ---------------------------------------------------------------------------
12
13/// Hex-encoded public key string.
14pub type PubKeyHex = String;
15
16/// Satoshi value (unsigned 64-bit integer).
17pub type SatoshiValue = u64;
18
19/// Outpoint string in "txid.index" format.
20pub type OutpointString = String;
21
22/// Transaction ID as a hex string.
23pub type TXIDHexString = String;
24
25/// Description string (5 to 50 bytes).
26pub type DescriptionString5to50Bytes = String;
27
28/// Basket name string (under 300 bytes).
29pub type BasketStringUnder300Bytes = String;
30
31/// Output tag string (under 300 bytes).
32pub type OutputTagStringUnder300Bytes = String;
33
34/// Label string (under 300 bytes).
35pub type LabelStringUnder300Bytes = String;
36
37/// Key ID string (under 800 bytes).
38pub type KeyIDStringUnder800Bytes = String;
39
40/// Originator domain name string (under 250 bytes).
41pub type OriginatorDomainNameStringUnder250Bytes = String;
42
43/// Certificate field name (under 50 bytes).
44pub type CertificateFieldNameUnder50Bytes = String;
45
46/// Base64-encoded string.
47pub type Base64String = String;
48
49/// Hex-encoded string.
50pub type HexString = String;
51
52/// Boolean that defaults to true when None.
53///
54/// A newtype around `Option<bool>` that dereferences to `true` when the inner
55/// value is `None` or `Some(true)`. Serializes transparently as `Option<bool>`
56/// on the wire and in JSON.
57#[derive(Clone, Debug, PartialEq)]
58#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
59#[cfg_attr(feature = "network", serde(transparent))]
60pub struct BooleanDefaultTrue(pub Option<bool>);
61
62impl Default for BooleanDefaultTrue {
63    fn default() -> Self {
64        BooleanDefaultTrue(Some(true))
65    }
66}
67
68impl std::ops::Deref for BooleanDefaultTrue {
69    type Target = bool;
70    fn deref(&self) -> &bool {
71        static TRUE: bool = true;
72        static FALSE: bool = false;
73        match self.0 {
74            Some(true) | None => &TRUE,
75            Some(false) => &FALSE,
76        }
77    }
78}
79
80impl From<BooleanDefaultTrue> for Option<bool> {
81    fn from(v: BooleanDefaultTrue) -> Self {
82        v.0
83    }
84}
85
86impl From<Option<bool>> for BooleanDefaultTrue {
87    fn from(v: Option<bool>) -> Self {
88        BooleanDefaultTrue(v)
89    }
90}
91
92impl BooleanDefaultTrue {
93    /// Returns true if the inner value is None.
94    /// Used by serde `skip_serializing_if` to omit fields that were absent in
95    /// the source JSON (or constructed via `none()`), matching TS's
96    /// `JSON.stringify(undefined)` omission. Explicit `Some(true)` /
97    /// `Some(false)` round-trip as `true` / `false`.
98    pub fn is_none(&self) -> bool {
99        self.0.is_none()
100    }
101
102    /// Constructs the "absent" representation: `Self(None)`.
103    ///
104    /// Used as a serde `default` for missing-field deserialization so the
105    /// distinction between "field absent" (round-trips as omitted) and "field
106    /// present at default value" (round-trips as the explicit value) is
107    /// preserved. `Default::default()` still returns `Self(Some(true))` for
108    /// runtime convenience — `None` is reserved for the wire-absent state.
109    pub fn none() -> Self {
110        BooleanDefaultTrue(None)
111    }
112}
113
114/// Boolean that defaults to false when None.
115///
116/// A newtype around `Option<bool>` that dereferences to `false` when the inner
117/// value is `None` or `Some(false)`. Serializes transparently as `Option<bool>`
118/// on the wire and in JSON.
119#[derive(Clone, Debug, PartialEq)]
120#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
121#[cfg_attr(feature = "network", serde(transparent))]
122pub struct BooleanDefaultFalse(pub Option<bool>);
123
124impl Default for BooleanDefaultFalse {
125    fn default() -> Self {
126        BooleanDefaultFalse(Some(false))
127    }
128}
129
130impl std::ops::Deref for BooleanDefaultFalse {
131    type Target = bool;
132    fn deref(&self) -> &bool {
133        static TRUE: bool = true;
134        static FALSE: bool = false;
135        match self.0 {
136            Some(true) => &TRUE,
137            Some(false) | None => &FALSE,
138        }
139    }
140}
141
142impl From<BooleanDefaultFalse> for Option<bool> {
143    fn from(v: BooleanDefaultFalse) -> Self {
144        v.0
145    }
146}
147
148impl From<Option<bool>> for BooleanDefaultFalse {
149    fn from(v: Option<bool>) -> Self {
150        BooleanDefaultFalse(v)
151    }
152}
153
154impl BooleanDefaultFalse {
155    /// Returns true if the inner value is None.
156    /// Used by serde `skip_serializing_if` to omit fields that were absent in
157    /// the source JSON (or constructed via `none()`), matching TS's
158    /// `JSON.stringify(undefined)` omission. Explicit `Some(true)` /
159    /// `Some(false)` round-trip as `true` / `false`.
160    pub fn is_none(&self) -> bool {
161        self.0.is_none()
162    }
163
164    /// Constructs the "absent" representation: `Self(None)`.
165    ///
166    /// Used as a serde `default` for missing-field deserialization so the
167    /// distinction between "field absent" (round-trips as omitted) and "field
168    /// present at default value" (round-trips as the explicit value) is
169    /// preserved. `Default::default()` still returns `Self(Some(false))` for
170    /// runtime convenience — `None` is reserved for the wire-absent state.
171    pub fn none() -> Self {
172        BooleanDefaultFalse(None)
173    }
174}
175
176/// Positive integer defaulting to 10, max 10000.
177pub type PositiveIntegerDefault10Max10000 = Option<u32>;
178
179/// Positive integer or zero.
180pub type PositiveIntegerOrZero = u32;
181
182// ---------------------------------------------------------------------------
183// Protocol
184// ---------------------------------------------------------------------------
185
186/// Defines a protocol with its security level and name.
187///
188/// The security level determines how strictly the wallet enforces
189/// user confirmation:
190/// - 0: Silent (no user interaction)
191/// - 1: Every app (user confirms per app)
192/// - 2: Every app and counterparty (user confirms per app + counterparty)
193///
194/// Serializes as a JSON array `[securityLevel, "protocolName"]` matching
195/// the Go SDK encoding.
196#[derive(Clone, Debug)]
197pub struct Protocol {
198    pub security_level: u8,
199    pub protocol: String,
200}
201
202#[cfg(feature = "network")]
203impl serde::Serialize for Protocol {
204    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205        use serde::ser::SerializeSeq;
206        let mut seq = serializer.serialize_seq(Some(2))?;
207        seq.serialize_element(&self.security_level)?;
208        seq.serialize_element(&self.protocol)?;
209        seq.end()
210    }
211}
212
213#[cfg(feature = "network")]
214impl<'de> serde::Deserialize<'de> for Protocol {
215    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
216        use serde::de::{self, SeqAccess, Visitor};
217        use std::fmt;
218
219        struct ProtocolVisitor;
220
221        impl<'de> Visitor<'de> for ProtocolVisitor {
222            type Value = Protocol;
223
224            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
225                formatter.write_str("an array [securityLevel, protocolName]")
226            }
227
228            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Protocol, A::Error> {
229                let security_level: u8 = seq
230                    .next_element()?
231                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
232                let protocol: String = seq
233                    .next_element()?
234                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
235                Ok(Protocol {
236                    security_level,
237                    protocol,
238                })
239            }
240        }
241
242        deserializer.deserialize_seq(ProtocolVisitor)
243    }
244}
245
246// ---------------------------------------------------------------------------
247// Counterparty
248// ---------------------------------------------------------------------------
249
250/// The type of counterparty in a cryptographic operation.
251#[derive(Clone, Debug, PartialEq)]
252pub enum CounterpartyType {
253    /// Uninitialized / unknown.
254    Uninitialized,
255    /// The wallet itself.
256    Self_,
257    /// The special "anyone" key (PrivateKey(1)).
258    Anyone,
259    /// A specific other party identified by public key.
260    Other,
261}
262
263/// Represents the other party in a cryptographic operation.
264///
265/// Can be a specific public key, or one of the special values
266/// "self" or "anyone".
267///
268/// Serializes as a JSON string: "anyone", "self", or DER hex public key,
269/// matching the Go SDK encoding.
270#[derive(Clone, Debug)]
271pub struct Counterparty {
272    pub counterparty_type: CounterpartyType,
273    pub public_key: Option<PublicKey>,
274}
275
276impl Default for Counterparty {
277    /// Default to `Uninitialized` — the sentinel that `ProtoWallet::default_counterparty()`
278    /// substitutes with the correct per-op default (`self` for most crypto ops,
279    /// `anyone` for `createSignature`). Returning a concrete value here would bypass
280    /// that per-op dispatch and silently mis-derive keys across SDKs.
281    fn default() -> Self {
282        Self {
283            counterparty_type: CounterpartyType::Uninitialized,
284            public_key: None,
285        }
286    }
287}
288
289#[cfg(feature = "network")]
290impl serde::Serialize for Counterparty {
291    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
292        match self.counterparty_type {
293            CounterpartyType::Anyone => serializer.serialize_str("anyone"),
294            CounterpartyType::Self_ => serializer.serialize_str("self"),
295            CounterpartyType::Other => {
296                if let Some(ref pk) = self.public_key {
297                    serializer.serialize_str(&pk.to_der_hex())
298                } else {
299                    serializer.serialize_none()
300                }
301            }
302            CounterpartyType::Uninitialized => serializer.serialize_str(""),
303        }
304    }
305}
306
307#[cfg(feature = "network")]
308impl<'de> serde::Deserialize<'de> for Counterparty {
309    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
310        // Accept both a missing field (handled by `#[serde(default)]` at the
311        // call site) and an explicit `null` as equivalent: both map to
312        // `Uninitialized`, which `ProtoWallet::default_counterparty()` then
313        // resolves to the correct per-op default. Without the `Option` layer,
314        // an explicit `"counterparty": null` would raise `invalid type: null,
315        // expected a string`, producing asymmetric behavior from omission.
316        let s: Option<String> = Option::<String>::deserialize(deserializer)?;
317        let s = match s {
318            None => {
319                return Ok(Counterparty {
320                    counterparty_type: CounterpartyType::Uninitialized,
321                    public_key: None,
322                })
323            }
324            Some(s) => s,
325        };
326        match s.as_str() {
327            "anyone" => Ok(Counterparty {
328                counterparty_type: CounterpartyType::Anyone,
329                public_key: None,
330            }),
331            "self" => Ok(Counterparty {
332                counterparty_type: CounterpartyType::Self_,
333                public_key: None,
334            }),
335            "" => Ok(Counterparty {
336                counterparty_type: CounterpartyType::Uninitialized,
337                public_key: None,
338            }),
339            hex_str => {
340                let pk = PublicKey::from_string(hex_str).map_err(serde::de::Error::custom)?;
341                Ok(Counterparty {
342                    counterparty_type: CounterpartyType::Other,
343                    public_key: Some(pk),
344                })
345            }
346        }
347    }
348}
349
350// ---------------------------------------------------------------------------
351// Anyone key helper
352// ---------------------------------------------------------------------------
353
354/// Returns the "anyone" public key: the public key corresponding to
355/// PrivateKey(1), which is the generator point G.
356///
357/// This is used when no specific counterparty is specified, making
358/// operations available to anyone who knows the protocol.
359pub fn anyone_pubkey() -> PublicKey {
360    let priv_key = PrivateKey::from_bytes(&{
361        let mut buf = [0u8; 32];
362        buf[31] = 1;
363        buf
364    })
365    // SAFETY: PrivateKey(1) is always valid -- 1 is within the secp256k1 scalar range.
366    .expect("PrivateKey(1) is always valid");
367    priv_key.to_public_key()
368}
369
370/// Returns the "anyone" private key: PrivateKey(1).
371pub fn anyone_private_key() -> PrivateKey {
372    PrivateKey::from_bytes(&{
373        let mut buf = [0u8; 32];
374        buf[31] = 1;
375        buf
376    })
377    // SAFETY: PrivateKey(1) is always valid -- 1 is within the secp256k1 scalar range.
378    .expect("PrivateKey(1) is always valid")
379}
380
381#[cfg(all(test, feature = "network"))]
382mod serde_tests {
383    //! Serde-level regression tests for `Counterparty` and its interaction
384    //! with `#[serde(default)]` on BRC-100 arg structs.
385
386    use super::*;
387    use crate::wallet::interfaces::{CreateSignatureArgs, EncryptArgs};
388
389    #[test]
390    fn counterparty_default_is_uninitialized() {
391        // C1 guard at the type level: `#[serde(default)]` on arg structs calls
392        // `Counterparty::default()`, which MUST yield `Uninitialized` so that
393        // `ProtoWallet::default_counterparty()` can substitute the correct
394        // per-op default ('anyone' for createSignature, 'self' for all others).
395        let cp = Counterparty::default();
396        assert_eq!(cp.counterparty_type, CounterpartyType::Uninitialized);
397        assert!(cp.public_key.is_none());
398    }
399
400    #[test]
401    fn create_signature_args_omit_counterparty_is_uninitialized() {
402        // C1 wire-format guard: a TS client that omits `counterparty` on a
403        // createSignature request must deserialize into an `Uninitialized`
404        // value so that `create_signature_sync` can route it to `Anyone`.
405        // Before the fix, this deserialized to `Self_` and silently derived
406        // against the wrong key.
407        let json = serde_json::json!({
408            "protocolID": [0, "cross-sdk test"],
409            "keyID": "x",
410            "data": [1, 2, 3],
411        });
412        let args: CreateSignatureArgs = serde_json::from_value(json).unwrap();
413        assert_eq!(
414            args.counterparty.counterparty_type,
415            CounterpartyType::Uninitialized,
416            "omitted counterparty must deserialize to Uninitialized, not Self_ — \
417             otherwise createSignature will derive against the wrong key"
418        );
419    }
420
421    #[test]
422    fn encrypt_args_omit_counterparty_is_uninitialized() {
423        // Non-createSignature ops also yield Uninitialized at the serde layer;
424        // the 'self' default is applied downstream by `default_counterparty()`.
425        let json = serde_json::json!({
426            "protocolID": [0, "cross-sdk test"],
427            "keyID": "x",
428            "plaintext": [1, 2, 3],
429        });
430        let args: EncryptArgs = serde_json::from_value(json).unwrap();
431        assert_eq!(
432            args.counterparty.counterparty_type,
433            CounterpartyType::Uninitialized
434        );
435    }
436
437    #[test]
438    fn counterparty_explicit_null_is_uninitialized() {
439        // M3: an explicit `"counterparty": null` must behave the same as
440        // omitting the field (both -> Uninitialized). Before the fix, null
441        // raised `invalid type: null, expected a string`, producing
442        // asymmetric behavior from omission for no useful reason.
443        let json = serde_json::json!({
444            "protocolID": [0, "cross-sdk test"],
445            "keyID": "x",
446            "data": [1, 2, 3],
447            "counterparty": serde_json::Value::Null,
448        });
449        let args: CreateSignatureArgs = serde_json::from_value(json).unwrap();
450        assert_eq!(
451            args.counterparty.counterparty_type,
452            CounterpartyType::Uninitialized
453        );
454    }
455
456    #[test]
457    fn counterparty_explicit_self_and_anyone_parse() {
458        // Sanity: the null/omission handling in the custom Deserialize impl
459        // must not break the pre-existing string-tag parsing.
460        let self_cp: Counterparty = serde_json::from_str("\"self\"").unwrap();
461        assert_eq!(self_cp.counterparty_type, CounterpartyType::Self_);
462
463        let anyone_cp: Counterparty = serde_json::from_str("\"anyone\"").unwrap();
464        assert_eq!(anyone_cp.counterparty_type, CounterpartyType::Anyone);
465
466        let empty_cp: Counterparty = serde_json::from_str("\"\"").unwrap();
467        assert_eq!(empty_cp.counterparty_type, CounterpartyType::Uninitialized);
468    }
469}