kaspa_addresses/
lib.rs

1//!
2//! Kaspa [`Address`] implementation.
3//!
4//! In it's string form, the Kaspa [`Address`] is represented by a `bech32`-encoded
5//! address string combined with a network type.  The `bech32` string encoding is
6//! comprised of a public key, the public key version and the resulting checksum.
7//!
8
9use borsh::{BorshDeserialize, BorshSerialize};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use smallvec::SmallVec;
12use std::fmt::{Display, Formatter};
13use thiserror::Error;
14use wasm_bindgen::prelude::*;
15use workflow_wasm::{
16    convert::{Cast, CastFromJs, TryCastFromJs},
17    extensions::object::*,
18};
19
20mod bech32;
21
22/// Error type produced by [`Address`] operations.
23#[derive(Error, PartialEq, Eq, Debug, Clone)]
24pub enum AddressError {
25    #[error("The address has an invalid prefix {0}")]
26    InvalidPrefix(String),
27
28    #[error("The address prefix is missing")]
29    MissingPrefix,
30
31    #[error("The address has an invalid version {0}")]
32    InvalidVersion(u8),
33
34    #[error("The address has an invalid version {0}")]
35    InvalidVersionString(String),
36
37    #[error("The address contains an invalid character {0}")]
38    DecodingError(char),
39
40    #[error("The address checksum is invalid (must be exactly 8 bytes)")]
41    BadChecksumSize,
42
43    #[error("The address checksum is invalid")]
44    BadChecksum,
45
46    #[error("The address payload is invalid")]
47    BadPayload,
48
49    #[error("The address is invalid")]
50    InvalidAddress,
51
52    #[error("The address array is invalid")]
53    InvalidAddressArray,
54
55    #[error("{0}")]
56    WASM(String),
57}
58
59impl From<workflow_wasm::error::Error> for AddressError {
60    fn from(e: workflow_wasm::error::Error) -> Self {
61        AddressError::WASM(e.to_string())
62    }
63}
64
65/// Address prefix identifying the network type this address belongs to (such as `kaspa`, `kaspatest`, `kaspasim`, `kaspadev`).
66#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
67#[borsh(use_discriminant = true)]
68pub enum Prefix {
69    #[serde(rename = "kaspa")]
70    Mainnet,
71    #[serde(rename = "kaspatest")]
72    Testnet,
73    #[serde(rename = "kaspasim")]
74    Simnet,
75    #[serde(rename = "kaspadev")]
76    Devnet,
77    #[cfg(test)]
78    A,
79    #[cfg(test)]
80    B,
81}
82
83impl Prefix {
84    fn as_str(&self) -> &'static str {
85        match self {
86            Prefix::Mainnet => "kaspa",
87            Prefix::Testnet => "kaspatest",
88            Prefix::Simnet => "kaspasim",
89            Prefix::Devnet => "kaspadev",
90            #[cfg(test)]
91            Prefix::A => "a",
92            #[cfg(test)]
93            Prefix::B => "b",
94        }
95    }
96
97    #[inline(always)]
98    fn is_test(&self) -> bool {
99        #[cfg(not(test))]
100        return false;
101        #[cfg(test)]
102        matches!(self, Prefix::A | Prefix::B)
103    }
104}
105
106impl Display for Prefix {
107    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108        f.write_str(self.as_str())
109    }
110}
111
112impl TryFrom<&str> for Prefix {
113    type Error = AddressError;
114
115    fn try_from(prefix: &str) -> Result<Self, Self::Error> {
116        match prefix {
117            "kaspa" => Ok(Prefix::Mainnet),
118            "kaspatest" => Ok(Prefix::Testnet),
119            "kaspasim" => Ok(Prefix::Simnet),
120            "kaspadev" => Ok(Prefix::Devnet),
121            #[cfg(test)]
122            "a" => Ok(Prefix::A),
123            #[cfg(test)]
124            "b" => Ok(Prefix::B),
125            _ => Err(AddressError::InvalidPrefix(prefix.to_string())),
126        }
127    }
128}
129
130///
131///  Kaspa `Address` version (`PubKey`, `PubKey ECDSA`, `ScriptHash`)
132///
133/// @category Address
134#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
135#[repr(u8)]
136#[borsh(use_discriminant = true)]
137#[wasm_bindgen(js_name = "AddressVersion")]
138pub enum Version {
139    /// PubKey addresses always have the version byte set to 0
140    PubKey = 0,
141    /// PubKey ECDSA addresses always have the version byte set to 1
142    PubKeyECDSA = 1,
143    /// ScriptHash addresses always have the version byte set to 8
144    ScriptHash = 8,
145}
146
147impl TryFrom<&str> for Version {
148    type Error = AddressError;
149
150    fn try_from(value: &str) -> Result<Self, Self::Error> {
151        match value {
152            "PubKey" => Ok(Version::PubKey),
153            "PubKeyECDSA" => Ok(Version::PubKeyECDSA),
154            "ScriptHash" => Ok(Version::ScriptHash),
155            _ => Err(AddressError::InvalidVersionString(value.to_owned())),
156        }
157    }
158}
159
160impl Version {
161    pub fn public_key_len(&self) -> usize {
162        match self {
163            Version::PubKey => 32,
164            Version::PubKeyECDSA => 33,
165            Version::ScriptHash => 32,
166        }
167    }
168}
169
170impl TryFrom<u8> for Version {
171    type Error = AddressError;
172
173    fn try_from(value: u8) -> Result<Self, Self::Error> {
174        match value {
175            0 => Ok(Version::PubKey),
176            1 => Ok(Version::PubKeyECDSA),
177            8 => Ok(Version::ScriptHash),
178            _ => Err(AddressError::InvalidVersion(value)),
179        }
180    }
181}
182
183impl Display for Version {
184    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
185        match self {
186            Version::PubKey => write!(f, "PubKey"),
187            Version::PubKeyECDSA => write!(f, "PubKeyECDSA"),
188            Version::ScriptHash => write!(f, "ScriptHash"),
189        }
190    }
191}
192
193/// Size of the payload vector of an address.
194///
195/// This size is the smallest SmallVec supported backing store size greater or equal to the largest
196/// possible payload, which is 33 for [`Version::PubKeyECDSA`].
197pub const PAYLOAD_VECTOR_SIZE: usize = 36;
198
199/// Used as the underlying type for address payload, optimized for the largest version length (33).
200pub type PayloadVec = SmallVec<[u8; PAYLOAD_VECTOR_SIZE]>;
201
202/// Kaspa [`Address`] struct that serializes to and from an address format string: `kaspa:qz0s...t8cv`.
203///
204/// @category Address
205#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, CastFromJs)]
206#[wasm_bindgen(inspectable)]
207pub struct Address {
208    #[wasm_bindgen(skip)]
209    pub prefix: Prefix,
210    #[wasm_bindgen(skip)]
211    pub version: Version,
212    #[wasm_bindgen(skip)]
213    pub payload: PayloadVec,
214}
215
216impl std::fmt::Debug for Address {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        if self.version == Version::PubKey {
219            write!(f, "{}", String::from(self))
220        } else {
221            write!(f, "{} ({})", String::from(self), self.version)
222        }
223    }
224}
225
226impl Address {
227    pub fn new(prefix: Prefix, version: Version, payload: &[u8]) -> Self {
228        if !prefix.is_test() {
229            assert_eq!(payload.len(), version.public_key_len());
230        }
231        Self { prefix, payload: PayloadVec::from_slice(payload), version }
232    }
233}
234
235#[wasm_bindgen]
236impl Address {
237    #[wasm_bindgen(constructor)]
238    pub fn constructor(address: &str) -> Address {
239        address.try_into().unwrap_or_else(|err| panic!("Address::constructor() - address error `{}`: {err}", address))
240    }
241
242    #[wasm_bindgen(js_name=validate)]
243    pub fn validate(address: &str) -> bool {
244        Self::try_from(address).is_ok()
245    }
246
247    /// Convert an address to a string.
248    #[wasm_bindgen(js_name = toString)]
249    pub fn address_to_string(&self) -> String {
250        self.into()
251    }
252
253    #[wasm_bindgen(getter, js_name = "version")]
254    pub fn version_to_string(&self) -> String {
255        self.version.to_string()
256    }
257
258    #[wasm_bindgen(getter, js_name = "prefix")]
259    pub fn prefix_to_string(&self) -> String {
260        self.prefix.to_string()
261    }
262
263    #[wasm_bindgen(setter, js_name = "setPrefix")]
264    pub fn set_prefix_from_str(&mut self, prefix: &str) {
265        self.prefix = Prefix::try_from(prefix).unwrap_or_else(|err| panic!("Address::prefix() - invalid prefix `{prefix}`: {err}"));
266    }
267
268    #[wasm_bindgen(getter, js_name = "payload")]
269    pub fn payload_to_string(&self) -> String {
270        self.encode_payload()
271    }
272
273    pub fn short(&self, n: usize) -> String {
274        let payload = self.encode_payload();
275        let n = std::cmp::min(n, payload.len() / 4);
276        format!("{}:{}....{}", self.prefix, &payload[0..n], &payload[payload.len() - n..])
277    }
278}
279
280impl Display for Address {
281    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
282        write!(f, "{}", String::from(self))
283    }
284}
285
286//
287// Borsh serializers need to be manually implemented for `Address` since
288// smallvec does not currently support Borsh
289//
290
291impl BorshSerialize for Address {
292    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
293        borsh::BorshSerialize::serialize(&self.prefix, writer)?;
294        borsh::BorshSerialize::serialize(&self.version, writer)?;
295        // Vectors and slices are all serialized internally the same way
296        borsh::BorshSerialize::serialize(&self.payload.as_slice(), writer)?;
297        Ok(())
298    }
299}
300
301impl BorshDeserialize for Address {
302    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
303        let prefix: Prefix = borsh::BorshDeserialize::deserialize_reader(reader)?;
304        let version: Version = borsh::BorshDeserialize::deserialize_reader(reader)?;
305        let payload: Vec<u8> = borsh::BorshDeserialize::deserialize_reader(reader)?;
306        Ok(Self::new(prefix, version, &payload))
307    }
308}
309
310impl From<Address> for String {
311    fn from(address: Address) -> Self {
312        (&address).into()
313    }
314}
315
316impl From<&Address> for String {
317    fn from(address: &Address) -> Self {
318        format!("{}:{}", address.prefix, address.encode_payload())
319    }
320}
321
322impl TryFrom<String> for Address {
323    type Error = AddressError;
324
325    fn try_from(value: String) -> Result<Self, Self::Error> {
326        value.as_str().try_into()
327    }
328}
329
330impl TryFrom<&str> for Address {
331    type Error = AddressError;
332
333    fn try_from(value: &str) -> Result<Self, Self::Error> {
334        match value.split_once(':') {
335            Some((prefix, payload)) => Self::decode_payload(prefix.try_into()?, payload),
336            None => Err(AddressError::MissingPrefix),
337        }
338    }
339}
340
341impl Serialize for Address {
342    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
343    where
344        S: Serializer,
345    {
346        serializer.serialize_str(&self.to_string())
347    }
348}
349
350impl<'de> Deserialize<'de> for Address {
351    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
352    where
353        D: Deserializer<'de>,
354    {
355        #[derive(Default)]
356        pub struct AddressVisitor<'de> {
357            marker: std::marker::PhantomData<Address>,
358            lifetime: std::marker::PhantomData<&'de ()>,
359        }
360        impl<'de> serde::de::Visitor<'de> for AddressVisitor<'de> {
361            type Value = Address;
362
363            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
364                #[cfg(target_arch = "wasm32")]
365                {
366                    write!(formatter, "string-type: string, str; bytes-type: slice of bytes, vec of bytes; map; number-type - pointer")
367                }
368                #[cfg(not(target_arch = "wasm32"))]
369                {
370                    write!(formatter, "string-type: string, str; bytes-type: slice of bytes, vec of bytes; map")
371                }
372            }
373
374            // TODO: see related comment in script_public_key.rs
375            #[cfg(target_arch = "wasm32")]
376            fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
377            where
378                E: serde::de::Error,
379            {
380                self.visit_u32(v as u32)
381            }
382            #[cfg(target_arch = "wasm32")]
383            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
384            where
385                E: serde::de::Error,
386            {
387                self.visit_u32(v as u32)
388            }
389
390            #[cfg(target_arch = "wasm32")]
391            fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
392            where
393                E: serde::de::Error,
394            {
395                self.visit_u32(v as u32)
396            }
397            #[cfg(target_arch = "wasm32")]
398            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
399            where
400                E: serde::de::Error,
401            {
402                self.visit_u32(v as u32)
403            }
404            #[cfg(target_arch = "wasm32")]
405            fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
406            where
407                E: serde::de::Error,
408            {
409                use wasm_bindgen::convert::RefFromWasmAbi;
410                let instance_ref = unsafe { Self::Value::ref_from_abi(v) }; // todo add checks for safecast
411                Ok(instance_ref.clone())
412            }
413            #[cfg(target_arch = "wasm32")]
414            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
415            where
416                E: serde::de::Error,
417            {
418                self.visit_u32(v as u32)
419            }
420
421            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
422            where
423                E: serde::de::Error,
424            {
425                Address::try_from(v).map_err(serde::de::Error::custom)
426            }
427
428            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
429            where
430                E: serde::de::Error,
431            {
432                Address::try_from(v).map_err(serde::de::Error::custom)
433            }
434
435            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
436            where
437                E: serde::de::Error,
438            {
439                Address::try_from(v).map_err(serde::de::Error::custom)
440            }
441
442            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
443            where
444                E: serde::de::Error,
445            {
446                let str = std::str::from_utf8(v).map_err(serde::de::Error::custom)?;
447                Address::try_from(str).map_err(serde::de::Error::custom)
448            }
449
450            fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
451            where
452                E: serde::de::Error,
453            {
454                let str = std::str::from_utf8(v).map_err(serde::de::Error::custom)?;
455                Address::try_from(str).map_err(serde::de::Error::custom)
456            }
457
458            fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
459            where
460                E: serde::de::Error,
461            {
462                let str = std::str::from_utf8(&v).map_err(serde::de::Error::custom)?;
463                Address::try_from(str).map_err(serde::de::Error::custom)
464            }
465
466            fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
467            where
468                A: serde::de::MapAccess<'de>,
469            {
470                let mut prefix: Option<String> = None;
471                let mut payload: Option<String> = None;
472
473                while let Some((key, value)) = access.next_entry::<String, String>()? {
474                    #[cfg(test)]
475                    web_sys::console::log_3(&"key value: ".into(), &key.clone().into(), &value.clone().into());
476
477                    match key.as_ref() {
478                        "prefix" => {
479                            prefix = Some(value.to_string());
480                        }
481                        "payload" => {
482                            payload = Some(value.to_string());
483                        }
484                        "version" => continue,
485                        unknown_field => {
486                            return Err(serde::de::Error::unknown_field(unknown_field, &["prefix", "payload", "version"]))
487                        }
488                    }
489                    if prefix.is_some() && payload.is_some() {
490                        break;
491                    }
492                }
493                let (prefix, payload) = match (prefix, payload) {
494                    (Some(prefix), Some(payload)) => (prefix, payload),
495                    (None, _) => return Err(serde::de::Error::missing_field("prefix")),
496                    (_, None) => return Err(serde::de::Error::missing_field("payload")),
497                };
498                Address::decode_payload(prefix.as_str().try_into().map_err(serde::de::Error::custom)?, &payload)
499                    .map_err(serde::de::Error::custom)
500            }
501        }
502
503        deserializer.deserialize_any(AddressVisitor::default())
504    }
505}
506
507impl TryCastFromJs for Address {
508    type Error = AddressError;
509    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<Self>, Self::Error>
510    where
511        R: AsRef<JsValue> + 'a,
512    {
513        Self::resolve(value, || {
514            if let Some(string) = value.as_ref().as_string() {
515                Address::try_from(string)
516            } else if let Some(object) = js_sys::Object::try_from(value.as_ref()) {
517                let prefix: Prefix = object.get_string("prefix")?.as_str().try_into()?;
518                let payload = object.get_string("payload")?; //.as_str();
519                Address::decode_payload(prefix, &payload)
520            } else {
521                Err(AddressError::InvalidAddress)
522            }
523        })
524    }
525}
526
527#[wasm_bindgen]
528extern "C" {
529    /// WASM (TypeScript) type representing an Address-like object: `Address | string`.
530    ///
531    /// @category Address
532    #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address | string")]
533    pub type AddressT;
534    /// WASM (TypeScript) type representing an array of Address-like objects: `(Address | string)[]`.
535    ///
536    /// @category Address
537    #[wasm_bindgen(extends = js_sys::Array, typescript_type = "(Address | string)[]")]
538    pub type AddressOrStringArrayT;
539    /// WASM (TypeScript) type representing an array of [`Address`] objects: `Address[]`.
540    ///
541    /// @category Address
542    #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address[]")]
543    pub type AddressArrayT;
544    /// WASM (TypeScript) type representing an [`Address`] or an undefined value: `Address | undefined`.
545    ///
546    /// @category Address
547    #[wasm_bindgen(typescript_type = "Address | undefined")]
548    pub type AddressOrUndefinedT;
549}
550
551impl TryFrom<AddressOrStringArrayT> for Vec<Address> {
552    type Error = AddressError;
553    fn try_from(js_value: AddressOrStringArrayT) -> Result<Self, Self::Error> {
554        if js_value.is_array() {
555            js_value.iter().map(Address::try_owned_from).collect::<Result<Vec<Address>, AddressError>>()
556        } else {
557            Err(AddressError::InvalidAddressArray)
558        }
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use crate::*;
565
566    fn cases() -> Vec<(Address, &'static str)> {
567        // cspell:disable
568        vec![
569            (Address::new(Prefix::A, Version::PubKey, b""), "a:qqeq69uvrh"),
570            (Address::new(Prefix::A, Version::ScriptHash, b""), "a:pq99546ray"),
571            (Address::new(Prefix::B, Version::ScriptHash, b" "), "b:pqsqzsjd64fv"),
572            (Address::new(Prefix::B, Version::ScriptHash, b"-"), "b:pqksmhczf8ud"),
573            (Address::new(Prefix::B, Version::ScriptHash, b"0"), "b:pqcq53eqrk0e"),
574            (Address::new(Prefix::B, Version::ScriptHash, b"1"), "b:pqcshg75y0vf"),
575            (Address::new(Prefix::B, Version::ScriptHash, b"-1"), "b:pqknzl4e9y0zy"),
576            (Address::new(Prefix::B, Version::ScriptHash, b"11"), "b:pqcnzt888ytdg"),
577            (Address::new(Prefix::B, Version::ScriptHash, b"abc"), "b:ppskycc8txxxn2w"),
578            (Address::new(Prefix::B, Version::ScriptHash, b"1234598760"), "b:pqcnyve5x5unsdekxqeusxeyu2"),
579            (Address::new(Prefix::B, Version::ScriptHash, b"abcdefghijklmnopqrstuvwxyz"), "b:ppskycmyv4nxw6rfdf4kcmtwdac8zunnw36hvamc09aqtpppz8lk"),
580            (Address::new(Prefix::B, Version::ScriptHash, b"000000000000000000000000000000000000000000"), "b:pqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrq7ag684l3"),
581            (Address::new(Prefix::Testnet, Version::PubKey, &[0u8; 32]),      "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya"),
582            (Address::new(Prefix::Testnet, Version::PubKeyECDSA, &[0u8; 33]), "kaspatest:qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhe837j2d"),
583            (Address::new(Prefix::Testnet, Version::PubKeyECDSA, b"\xba\x01\xfc\x5f\x4e\x9d\x98\x79\x59\x9c\x69\xa3\xda\xfd\xb8\x35\xa7\x25\x5e\x5f\x2e\x93\x4e\x93\x22\xec\xd3\xaf\x19\x0a\xb0\xf6\x0e"), "kaspatest:qxaqrlzlf6wes72en3568khahq66wf27tuhfxn5nytkd8tcep2c0vrse6gdmpks"),
584            (Address::new(Prefix::Mainnet, Version::PubKey, &[0u8; 32]),      "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e"),
585            (Address::new(Prefix::Mainnet, Version::PubKey, b"\x5f\xff\x3c\x4d\xa1\x8f\x45\xad\xcd\xd4\x99\xe4\x46\x11\xe9\xff\xf1\x48\xba\x69\xdb\x3c\x4e\xa2\xdd\xd9\x55\xfc\x46\xa5\x95\x22"), "kaspa:qp0l70zd5x85ttwd6jv7g3s3a8llzj96d8dncn4zmhv4tlzx5k2jyqh70xmfj"),
586        ]
587        // cspell:enable
588    }
589
590    #[test]
591    fn check_into_string() {
592        for (address, expected_address_str) in cases() {
593            let address_str: String = address.into();
594            assert_eq!(address_str, expected_address_str);
595        }
596    }
597
598    #[test]
599    fn check_from_string() {
600        for (expected_address, address_str) in cases() {
601            let address: Address = address_str.to_string().try_into().expect("Test failed");
602            assert_eq!(address, expected_address);
603        }
604    }
605
606    #[test]
607    fn test_errors() {
608        // cspell:disable
609        let address_str: String = "kaspa:qqqqqqqqqqqqq1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e".to_string();
610        let address: Result<Address, AddressError> = address_str.try_into();
611        assert_eq!(Err(AddressError::DecodingError('1')), address);
612
613        let invalid_char = 124u8 as char;
614        let address_str: String = format!("kaspa:qqqqqqqqqqqqq{invalid_char}qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e");
615        let address: Result<Address, AddressError> = address_str.try_into();
616        assert_eq!(Err(AddressError::DecodingError(invalid_char)), address);
617
618        let invalid_char = 129u8 as char;
619        let address_str: String = format!("kaspa:qqqqqqqqqqqqq{invalid_char}qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e");
620        let address: Result<Address, AddressError> = address_str.try_into();
621        assert!(matches!(address, Err(AddressError::DecodingError(_))));
622
623        let address_str: String = "kaspa1:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e".to_string();
624        let address: Result<Address, AddressError> = address_str.try_into();
625        assert_eq!(Err(AddressError::InvalidPrefix("kaspa1".into())), address);
626
627        let address_str: String = "kaspaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e".to_string();
628        let address: Result<Address, AddressError> = address_str.try_into();
629        assert_eq!(Err(AddressError::MissingPrefix), address);
630
631        let address_str: String = "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4l".to_string();
632        let address: Result<Address, AddressError> = address_str.try_into();
633        assert_eq!(Err(AddressError::BadChecksum), address);
634
635        let address_str: String = "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e".to_string();
636        let address: Result<Address, AddressError> = address_str.try_into();
637        assert_eq!(Err(AddressError::BadChecksum), address);
638        // cspell:enable
639    }
640
641    use js_sys::Object;
642    use wasm_bindgen::{JsValue, __rt::IntoJsResult};
643    use wasm_bindgen_test::wasm_bindgen_test;
644    use workflow_wasm::{extensions::ObjectExtension, serde::from_value, serde::to_value};
645
646    #[wasm_bindgen_test]
647    pub fn test_wasm_serde_constructor() {
648        let str = "kaspa:qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjsgzthw5j";
649        let a = Address::constructor(str);
650        let value = to_value(&a).unwrap();
651
652        assert_eq!(JsValue::from_str("string"), value.js_typeof());
653        assert_eq!(value, JsValue::from_str(str));
654        assert_eq!(a, from_value(value).unwrap());
655    }
656
657    #[wasm_bindgen_test]
658    pub fn test_wasm_js_serde_object() {
659        let expected = Address::constructor("kaspa:qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjsgzthw5j");
660
661        use web_sys::console;
662        console::log_4(
663            &"address: ".into(),
664            &expected.version_to_string().into(),
665            &expected.prefix_to_string().into(),
666            &expected.payload_to_string().into(),
667        );
668
669        let obj = Object::new();
670        obj.set("version", &JsValue::from_str("PubKey")).unwrap();
671        obj.set("prefix", &JsValue::from_str("kaspa")).unwrap();
672        obj.set("payload", &JsValue::from_str("qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjsgzthw5j")).unwrap();
673
674        assert_eq!(JsValue::from_str("object"), obj.js_typeof());
675
676        let obj_js = obj.into_js_result().unwrap();
677        let actual = from_value(obj_js).unwrap();
678        assert_eq!(expected, actual);
679    }
680
681    #[wasm_bindgen_test]
682    pub fn test_wasm_serde_object() {
683        use wasm_bindgen::convert::IntoWasmAbi;
684
685        let expected = Address::constructor("kaspa:qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjsgzthw5j");
686        let wasm_js_value: JsValue = expected.clone().into_abi().into();
687
688        let actual = from_value(wasm_js_value).unwrap();
689        assert_eq!(expected, actual);
690    }
691}