Skip to main content

fiber_types/
invoice.rs

1//! Invoice-related types: status, currency, hash algorithm, script wrapper, signature.
2
3use crate::serde_utils::EntityHex;
4
5use crate::gen::invoice as gen_invoice;
6use arcode::bitbit::{BitReader, BitWriter, MSB};
7use arcode::{ArithmeticDecoder, ArithmeticEncoder, EOFKind, Model};
8use bech32::{encode, u5, FromBase32, ToBase32, Variant, WriteBase32};
9use ckb_hash::blake2b_256;
10use ckb_types::packed::Script as PackedScript;
11use ckb_types::prelude::{Pack, Unpack};
12use gen_invoice::{
13    Description, ExpiryTime, FallbackAddr, Feature, FinalHtlcMinimumExpiryDelta, FinalHtlcTimeout,
14    InvoiceAttr, InvoiceAttrUnion, InvoiceAttrsVec, PayeePublicKey, PaymentHash, PaymentSecret,
15    RawInvoiceDataBuilder, UdtScript,
16};
17use molecule::prelude::Byte;
18use molecule::prelude::{Builder, Entity};
19use nom::{branch::alt, combinator::opt};
20use nom::{
21    bytes::{complete::take_while1, streaming::tag},
22    IResult,
23};
24use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
25use serde::{Deserialize, Serialize};
26use serde_with::serde_as;
27use sha2::{Digest, Sha256};
28use std::cmp::Ordering;
29use std::fmt::Display;
30use std::io::{Cursor, Result as IoResult};
31use std::num::ParseIntError;
32use std::str::FromStr;
33use thiserror::Error;
34
35/// Wrapper for molecule verification errors.
36#[derive(Error, Debug)]
37pub struct VerificationError(pub molecule::error::VerificationError);
38
39impl PartialEq for VerificationError {
40    fn eq(&self, _other: &Self) -> bool {
41        false
42    }
43}
44
45impl Display for VerificationError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        self.0.fmt(f)
48    }
49}
50
51/// Errors that can occur when parsing or validating an invoice.
52#[derive(Error, PartialEq, Debug)]
53pub enum InvoiceError {
54    /// Bech32 encoding/decoding error.
55    #[error("Bech32 error: {0}")]
56    Bech32Error(bech32::Error),
57    /// Molecule serialization error.
58    #[error("Molecule error: {0}")]
59    MoleculeError(VerificationError),
60    /// Failed to parse amount from HRP.
61    #[error("Failed to parse amount: {0}")]
62    ParseAmountError(ParseIntError),
63    /// Unknown currency in HRP.
64    #[error("Unknown currency: {0}")]
65    UnknownCurrency(String),
66    /// Unknown SI prefix in amount.
67    #[error("Unknown si prefix: {0}")]
68    UnknownSiPrefix(String),
69    /// Malformed HRP.
70    #[error("Parsing failed with malformed HRP: {0}")]
71    MalformedHRP(String),
72    /// Data part is too short.
73    #[error("Too short data part")]
74    TooShortDataPart,
75    /// Unexpected end of tagged fields.
76    #[error("Unexpected end of tagged fields")]
77    UnexpectedEndOfTaggedFields,
78    /// Integer overflow error.
79    #[error("Integer overflow error")]
80    IntegerOverflowError,
81    /// Invalid recovery ID in signature.
82    #[error("Invalid recovery id")]
83    InvalidRecoveryId,
84    /// Invalid slice length.
85    #[error("Invalid slice length: {0}")]
86    InvalidSliceLength(String),
87    /// Invalid signature.
88    #[error("Invalid signature")]
89    InvalidSignature,
90    /// Duplicated attribute key.
91    #[error("Duplicated attribute key: {0}")]
92    DuplicatedAttributeKey(String),
93    /// Payment secret is required for MPP payments.
94    #[error("Payment secret is required for MPP payments")]
95    PaymentSecretRequiredForMpp,
96    /// Both payment_hash and payment_preimage are set.
97    #[error("Both payment_hash and payment_preimage are set")]
98    BothPaymenthashAndPreimage,
99    /// Neither payment_hash nor payment_preimage is set.
100    #[error("Neither payment_hash nor payment_preimage is set")]
101    NeitherPaymenthashNorPreimage,
102    /// An error occurred during signing.
103    #[error("Sign error")]
104    SignError,
105    /// Hex decode error.
106    #[error("Hex decode error: {0}")]
107    HexDecodeError(#[from] hex::FromHexError),
108    /// Duplicated invoice found.
109    #[error("Duplicated invoice found: {0}")]
110    DuplicatedInvoice(String),
111    /// Description is too long.
112    #[error("Description with length of {0} is too long, max length is 639")]
113    DescriptionTooLong(usize),
114    /// Invoice not found.
115    #[error("Invoice not found")]
116    InvoiceNotFound,
117    /// Invoice already exists.
118    #[error("Invoice already exists")]
119    InvoiceAlreadyExists,
120    /// Deprecated attribute.
121    #[error("Deprecated attribute: {0}")]
122    DeprecatedAttribute(String),
123    /// Failed to decompress invoice data.
124    #[error("Failed to decompress invoice data: {0}")]
125    DecompressionError(String),
126    /// Decompressed invoice data exceeds the parser limit.
127    #[error("Invoice data length {len} exceeds max length {max}")]
128    InvoiceDataTooLong { len: usize, max: usize },
129    /// Invoice text attribute is not valid UTF-8.
130    #[error("Invalid UTF-8 in invoice {0} attribute")]
131    InvalidUtf8Attribute(&'static str),
132    /// Invoice payee public key is malformed.
133    #[error("Invalid payee public key")]
134    InvalidPayeePublicKey,
135    /// Invoice signature contains malformed base32 data.
136    #[error("Invalid signature encoding")]
137    InvalidSignatureEncoding,
138    /// Invoice is missing a required signature.
139    #[error("Invoice is not signed")]
140    MissingSignature,
141}
142
143/// Size of the signature in u5 encoding.
144pub const SIGNATURE_U5_SIZE: usize = 104;
145
146/// Maximum allowed length for an invoice description.
147pub const MAX_DESCRIPTION_LENGTH: usize = 639;
148
149/// Maximum decompressed molecule payload accepted by the invoice parser.
150///
151/// Current invoices only need a few hundred bytes for fixed fields plus the
152/// 639-byte description limit. This leaves room for scripts, fallback
153/// addresses and future attributes while bounding compressed-input expansion.
154pub const MAX_INVOICE_DATA_LENGTH: usize = 16 * 1024;
155
156/// Default final-hop TLC expiry delta when an invoice does not specify one.
157pub const DEFAULT_FINAL_TLC_EXPIRY_DELTA: u64 = 24 * 60 * 60 * 1000;
158
159/// Encodes bytes and returns the compressed form.
160/// This is used for encoding the invoice data, to make the final Invoice encoded address shorter.
161pub(crate) fn ar_encompress(data: &[u8]) -> IoResult<Vec<u8>> {
162    let mut model = Model::builder().num_bits(8).eof(EOFKind::EndAddOne).build();
163    let mut compressed_writer = BitWriter::new(Cursor::new(vec![]));
164    let mut encoder = ArithmeticEncoder::new(48);
165    for &sym in data {
166        encoder.encode(sym as u32, &model, &mut compressed_writer)?;
167        model.update_symbol(sym as u32);
168    }
169
170    encoder.encode(model.eof(), &model, &mut compressed_writer)?;
171    encoder.finish_encode(&mut compressed_writer)?;
172    compressed_writer.pad_to_byte()?;
173
174    Ok(compressed_writer.get_ref().get_ref().clone())
175}
176
177fn ar_decompress_with_limit(data: &[u8], max_len: usize) -> Result<Vec<u8>, InvoiceError> {
178    let mut model = Model::builder().num_bits(8).eof(EOFKind::EndAddOne).build();
179    let mut input_reader = BitReader::<_, MSB>::new(data);
180    let mut decoder = ArithmeticDecoder::new(48);
181    let mut decompressed_data = vec![];
182
183    while !decoder.finished() {
184        let sym = decoder
185            .decode(&model, &mut input_reader)
186            .map_err(|err| InvoiceError::DecompressionError(err.to_string()))?;
187        model.update_symbol(sym);
188        decompressed_data.push(sym as u8);
189
190        if !decoder.finished() && decompressed_data.len() > max_len {
191            return Err(InvoiceError::InvoiceDataTooLong {
192                len: decompressed_data.len(),
193                max: max_len,
194            });
195        }
196    }
197
198    decompressed_data
199        .pop()
200        .ok_or_else(|| InvoiceError::DecompressionError("missing EOF marker".to_string()))?;
201    Ok(decompressed_data)
202}
203
204/// Construct the invoice's HRP and signatureless data into a preimage to be hashed.
205pub fn construct_invoice_preimage(hrp_bytes: &[u8], data_without_signature: &[u5]) -> Vec<u8> {
206    let mut preimage = Vec::<u8>::from(hrp_bytes);
207
208    let mut data_part = Vec::from(data_without_signature);
209    let overhang = (data_part.len() * 5) % 8;
210    if overhang > 0 {
211        // add padding if data does not end at a byte boundary
212        data_part.push(u5::try_from_u8(0).expect("u5 from u8"));
213
214        // if overhang is in (1..3) we need to add u5(0) padding two times
215        if overhang < 3 {
216            data_part.push(u5::try_from_u8(0).expect("u5 from u8"));
217        }
218    }
219
220    preimage.extend_from_slice(
221        &Vec::<u8>::from_base32(&data_part)
222            .expect("No padding error may occur due to appended zero above."),
223    );
224    preimage
225}
226
227fn nom_scan_hrp(input: &str) -> IResult<&str, (&str, Option<&str>)> {
228    let (input, currency) = alt((tag("fibb"), tag("fibt"), tag("fibd")))(input)?;
229    let (input, amount) = opt(take_while1(|c: char| c.is_numeric()))(input)?;
230    Ok((input, (currency, amount)))
231}
232
233/// Parse the human-readable part of an invoice.
234pub fn parse_hrp(input: &str) -> Result<(Currency, Option<u128>), InvoiceError> {
235    match nom_scan_hrp(input) {
236        Ok((left, (currency, amount))) => {
237            if !left.is_empty() {
238                return Err(InvoiceError::MalformedHRP(format!(
239                    "{}, unexpected ending `{}`",
240                    input, left
241                )));
242            }
243            let currency =
244                Currency::from_str(currency).map_err(|e| InvoiceError::UnknownCurrency(e.0))?;
245            let amount = amount
246                .map(|x| x.parse().map_err(InvoiceError::ParseAmountError))
247                .transpose()?;
248            Ok((currency, amount))
249        }
250        Err(_) => Err(InvoiceError::MalformedHRP(input.to_string())),
251    }
252}
253
254/// The currency of the invoice, can also used to represent the CKB network chain.
255#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
256pub enum CkbInvoiceStatus {
257    /// The invoice is open and can be paid.
258    Open,
259    /// The invoice is cancelled.
260    Cancelled,
261    /// The invoice is expired.
262    Expired,
263    /// The invoice is received, but not settled yet.
264    Received,
265    /// The invoice is paid.
266    Paid,
267}
268
269impl Display for CkbInvoiceStatus {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        match self {
272            CkbInvoiceStatus::Open => write!(f, "Open"),
273            CkbInvoiceStatus::Cancelled => write!(f, "Cancelled"),
274            CkbInvoiceStatus::Expired => write!(f, "Expired"),
275            CkbInvoiceStatus::Received => write!(f, "Received"),
276            CkbInvoiceStatus::Paid => write!(f, "Paid"),
277        }
278    }
279}
280
281/// The currency of the invoice, can also used to represent the CKB network chain.
282#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
283pub enum Currency {
284    /// The mainnet currency of CKB.
285    Fibb,
286    /// The testnet currency of the CKB network.
287    Fibt,
288    /// The devnet currency of the CKB network.
289    #[default]
290    Fibd,
291}
292
293impl Display for Currency {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Currency::Fibb => write!(f, "fibb"),
297            Currency::Fibt => write!(f, "fibt"),
298            Currency::Fibd => write!(f, "fibd"),
299        }
300    }
301}
302
303/// Error for unknown currency
304#[derive(thiserror::Error, Debug)]
305#[error("Unknown currency: {0}")]
306pub struct UnknownCurrencyError(pub String);
307
308impl FromStr for Currency {
309    type Err = UnknownCurrencyError;
310
311    fn from_str(s: &str) -> Result<Self, Self::Err> {
312        match s {
313            "fibb" => Ok(Self::Fibb),
314            "fibt" => Ok(Self::Fibt),
315            "fibd" => Ok(Self::Fibd),
316            _ => Err(UnknownCurrencyError(s.to_string())),
317        }
318    }
319}
320
321impl TryFrom<u8> for Currency {
322    type Error = UnknownCurrencyError;
323
324    fn try_from(byte: u8) -> Result<Self, Self::Error> {
325        match byte {
326            0 => Ok(Self::Fibb),
327            1 => Ok(Self::Fibt),
328            2 => Ok(Self::Fibd),
329            _ => Err(UnknownCurrencyError(byte.to_string())),
330        }
331    }
332}
333
334/// HashAlgorithm is the hash algorithm used in the hash lock.
335#[repr(u8)]
336#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
337#[serde(rename_all = "snake_case")]
338pub enum HashAlgorithm {
339    /// The default hash algorithm, CkbHash
340    #[default]
341    CkbHash = 0,
342    /// The sha256 hash algorithm
343    Sha256 = 1,
344}
345
346/// Error for unknown hash algorithm
347#[derive(thiserror::Error, Debug)]
348#[error("Unknown Hash Algorithm: {0}")]
349pub struct UnknownHashAlgorithmError(pub u8);
350
351impl TryFrom<u8> for HashAlgorithm {
352    type Error = UnknownHashAlgorithmError;
353
354    fn try_from(value: u8) -> Result<Self, Self::Error> {
355        match value {
356            0 => Ok(HashAlgorithm::CkbHash),
357            1 => Ok(HashAlgorithm::Sha256),
358            _ => Err(UnknownHashAlgorithmError(value)),
359        }
360    }
361}
362
363impl HashAlgorithm {
364    pub fn supported_algorithms() -> Vec<HashAlgorithm> {
365        vec![HashAlgorithm::CkbHash, HashAlgorithm::Sha256]
366    }
367
368    pub fn hash<T: AsRef<[u8]>>(&self, s: T) -> [u8; 32] {
369        match self {
370            HashAlgorithm::CkbHash => blake2b_256(s),
371            HashAlgorithm::Sha256 => sha256(s),
372        }
373    }
374}
375
376/// SHA-256 hash helper function.
377pub fn sha256<T: AsRef<[u8]>>(s: T) -> [u8; 32] {
378    let mut hasher = Sha256::new();
379    hasher.update(s.as_ref());
380    hasher.finalize().into()
381}
382
383impl TryFrom<Byte> for HashAlgorithm {
384    type Error = UnknownHashAlgorithmError;
385
386    fn try_from(value: Byte) -> Result<Self, Self::Error> {
387        let value: u8 = value.into();
388        value.try_into()
389    }
390}
391
392/// A wrapper around `ckb_types::packed::Script` with hex serialization.
393#[serde_as]
394#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
395pub struct CkbScript(#[serde_as(as = "EntityHex")] pub PackedScript);
396
397/// Recoverable signature
398#[derive(Clone, Debug, Eq, PartialEq)]
399pub struct InvoiceSignature(pub RecoverableSignature);
400
401impl PartialOrd for InvoiceSignature {
402    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
403        Some(self.cmp(other))
404    }
405}
406
407impl Ord for InvoiceSignature {
408    fn cmp(&self, other: &Self) -> Ordering {
409        self.0
410            .serialize_compact()
411            .1
412            .cmp(&other.0.serialize_compact().1)
413    }
414}
415
416impl Serialize for InvoiceSignature {
417    fn serialize<S>(
418        &self,
419        serializer: S,
420    ) -> Result<<S as serde::Serializer>::Ok, <S as serde::Serializer>::Error>
421    where
422        S: serde::Serializer,
423    {
424        let base32: Vec<u8> = self.to_base32().iter().map(|x| x.to_u8()).collect();
425        let hex_str = hex::encode(base32);
426        hex_str.serialize(serializer)
427    }
428}
429
430impl<'de> Deserialize<'de> for InvoiceSignature {
431    fn deserialize<D>(deserializer: D) -> Result<Self, <D as serde::Deserializer<'de>>::Error>
432    where
433        D: serde::Deserializer<'de>,
434    {
435        let signature_hex: String = String::deserialize(deserializer)?;
436        let signature_bytes = hex::decode(signature_hex).map_err(serde::de::Error::custom)?;
437        let base32_values = signature_bytes
438            .iter()
439            .map(|x| u5::try_from_u8(*x))
440            .collect::<Result<Vec<u5>, _>>()
441            .map_err(serde::de::Error::custom)?;
442        InvoiceSignature::from_base32(&base32_values).map_err(serde::de::Error::custom)
443    }
444}
445
446struct BytesToBase32<'a, W: WriteBase32 + 'a> {
447    writer: &'a mut W,
448    buffer: u8,
449    buffer_bits: u8,
450}
451
452impl<'a, W: WriteBase32> BytesToBase32<'a, W> {
453    fn new(writer: &'a mut W) -> Self {
454        BytesToBase32 {
455            writer,
456            buffer: 0,
457            buffer_bits: 0,
458        }
459    }
460
461    fn append(&mut self, byte: u8) -> Result<(), <W as WriteBase32>::Err> {
462        let mut bits_remaining = 8;
463        while bits_remaining > 0 {
464            let bits_to_take = std::cmp::min(5 - self.buffer_bits, bits_remaining);
465            self.buffer <<= bits_to_take;
466            self.buffer |= (byte >> (bits_remaining - bits_to_take)) & ((1 << bits_to_take) - 1);
467            self.buffer_bits += bits_to_take;
468            bits_remaining -= bits_to_take;
469
470            if self.buffer_bits == 5 {
471                self.writer
472                    .write_u5(u5::try_from_u8(self.buffer).expect("buffer is 5 bits"))?;
473                self.buffer = 0;
474                self.buffer_bits = 0;
475            }
476        }
477        Ok(())
478    }
479
480    fn finalize(mut self) -> Result<(), <W as WriteBase32>::Err> {
481        if self.buffer_bits > 0 {
482            self.buffer <<= 5 - self.buffer_bits;
483            self.writer
484                .write_u5(u5::try_from_u8(self.buffer).expect("buffer is at most 5 bits"))?;
485        }
486        Ok(())
487    }
488}
489
490impl ToBase32 for InvoiceSignature {
491    fn write_base32<W: WriteBase32>(&self, writer: &mut W) -> Result<(), <W as WriteBase32>::Err> {
492        let mut converter = BytesToBase32::new(writer);
493        let (recovery_id, signature) = self.0.serialize_compact();
494        for v in signature
495            .iter()
496            .chain(std::iter::once(&(i32::from(recovery_id) as u8)))
497        {
498            converter.append(*v)?;
499        }
500        converter.finalize()
501    }
502}
503
504impl FromBase32 for InvoiceSignature {
505    type Err = anyhow::Error;
506
507    fn from_base32(field_data: &[u5]) -> Result<InvoiceSignature, Self::Err> {
508        if field_data.len() < 104 {
509            return Err(anyhow::anyhow!(
510                "InvoiceSignature TryFrom<[u5]> failed: unexpected length {}",
511                field_data.len()
512            ));
513        }
514
515        let raw_bytes = Vec::<u8>::from_base32(field_data)?;
516        if raw_bytes.len() != 65 {
517            return Err(anyhow::anyhow!(
518                "InvoiceSignature TryFrom<[u5]> failed: unexpected byte length {}",
519                raw_bytes.len()
520            ));
521        }
522        let recovery_id = RecoveryId::try_from(raw_bytes[64] as i32)?;
523        let signature = RecoverableSignature::from_compact(&raw_bytes[0..64], recovery_id)?;
524        Ok(InvoiceSignature(signature))
525    }
526}
527
528impl InvoiceSignature {
529    /// Parse an `InvoiceSignature` from base32-encoded data, returning `InvoiceError` on failure.
530    pub fn from_base32_checked(signature: &[u5]) -> Result<Self, InvoiceError> {
531        if signature.len() != SIGNATURE_U5_SIZE {
532            return Err(InvoiceError::InvalidSliceLength(
533                "InvoiceSignature::from_base32_checked()".into(),
534            ));
535        }
536        let recoverable_signature_bytes =
537            Vec::<u8>::from_base32(signature).map_err(InvoiceError::Bech32Error)?;
538        let sig = &recoverable_signature_bytes[0..64];
539        let recovery_id = RecoveryId::try_from(recoverable_signature_bytes[64] as i32)
540            .map_err(|_| InvoiceError::InvalidRecoveryId)?;
541
542        Ok(InvoiceSignature(
543            RecoverableSignature::from_compact(sig, recovery_id)
544                .map_err(|_| InvoiceError::InvalidSignature)?,
545        ))
546    }
547}
548
549use crate::protocol::FeatureVector;
550use crate::serde_utils::{duration_hex, U128Hex, U64Hex};
551use crate::Hash256;
552use secp256k1::PublicKey;
553use std::time::Duration;
554
555/// The attributes of the invoice.
556#[serde_as]
557#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
558#[serde(rename_all = "snake_case")]
559pub enum Attribute {
560    /// This attribute is deprecated since v0.6.0. The final TLC timeout, in milliseconds.
561    #[serde(with = "U64Hex")]
562    FinalHtlcTimeout(u64),
563    /// The final TLC minimum expiry delta, in milliseconds.
564    ///
565    /// When omitted, the protocol default is 24 hours. Invoices created through
566    /// the node RPC normally contain an explicit value, defaulting to 160 minutes.
567    #[serde(with = "U64Hex")]
568    FinalHtlcMinimumExpiryDelta(u64),
569    /// The expiry time of the invoice, in seconds.
570    #[serde(with = "duration_hex")]
571    ExpiryTime(Duration),
572    /// The description of the invoice.
573    Description(String),
574    /// The fallback address of the invoice.
575    FallbackAddr(String),
576    /// The UDT type script of the invoice.
577    UdtScript(CkbScript),
578    /// The payee public key of the invoice.
579    PayeePublicKey(PublicKey),
580    /// The hash algorithm of the invoice.
581    HashAlgorithm(HashAlgorithm),
582    /// The feature flags of the invoice.
583    Feature(FeatureVector),
584    /// The payment secret of the invoice.
585    PaymentSecret(Hash256),
586}
587
588/// The metadata of the invoice.
589#[serde_as]
590#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
591pub struct InvoiceData {
592    /// The timestamp of the invoice.
593    #[serde_as(as = "U128Hex")]
594    pub timestamp: u128,
595    /// The payment hash of the invoice.
596    pub payment_hash: Hash256,
597    /// The attributes of the invoice, e.g. description, expiry time, etc.
598    pub attrs: Vec<Attribute>,
599}
600
601/// Represents a syntactically and semantically correct Fiber invoice.
602///
603/// There are three ways to construct a `CkbInvoice`:
604///  1. using `CkbInvoiceBuilder`
605///  2. using `str::parse::<CkbInvoice>(&str)` (see `CkbInvoice::from_str`)
606#[serde_as]
607#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
608pub struct CkbInvoice {
609    /// The currency of the invoice.
610    pub currency: Currency,
611    /// The amount of the invoice.
612    #[serde_as(as = "Option<U128Hex>")]
613    pub amount: Option<u128>,
614    /// The signature of the invoice.
615    pub signature: Option<InvoiceSignature>,
616    /// The invoice data, including the payment hash, timestamp and other attributes.
617    pub data: InvoiceData,
618}
619
620impl CkbInvoice {
621    fn hrp_part(&self) -> String {
622        format!(
623            "{}{}",
624            self.currency,
625            self.amount
626                .map_or_else(|| "".to_string(), |x| x.to_string()),
627        )
628    }
629
630    // Use the lossless compression algorithm to compress the invoice data.
631    // To make sure the final encoded invoice address is shorter
632    fn data_part(&self) -> Vec<u5> {
633        let invoice_data = gen_invoice::RawInvoiceData::from(self.data.clone());
634        let compressed = ar_encompress(invoice_data.as_slice()).expect("compress invoice data");
635        let mut base32 = Vec::with_capacity(compressed.len());
636        compressed
637            .write_base32(&mut base32)
638            .expect("encode in base32");
639        base32
640    }
641
642    /// Check that the invoice is signed correctly and that key recovery works.
643    pub fn check_signature(&self) -> Result<(), InvoiceError> {
644        if self.signature.is_none() {
645            return Ok(());
646        }
647        match self.recover_payee_pub_key() {
648            Err(secp256k1::Error::InvalidRecoveryId) => {
649                return Err(InvoiceError::InvalidRecoveryId);
650            }
651            Err(secp256k1::Error::InvalidSignature) => return Err(InvoiceError::InvalidSignature),
652            Err(e) => panic!("no other error may occur, got {:?}", e),
653            Ok(_) => {}
654        }
655
656        if !self.validate_signature() {
657            return Err(InvoiceError::InvalidSignature);
658        }
659
660        Ok(())
661    }
662
663    fn validate_signature(&self) -> bool {
664        let Some(signature) = self.signature.as_ref() else {
665            return true;
666        };
667        let included_pub_key = self.payee_pub_key();
668
669        let mut recovered_pub_key = Option::None;
670        if included_pub_key.is_none() {
671            let recovered = match self.recover_payee_pub_key() {
672                Ok(pk) => pk,
673                Err(_) => return false,
674            };
675            recovered_pub_key = Some(recovered);
676        }
677
678        let Some(pub_key) = included_pub_key.or(recovered_pub_key.as_ref()) else {
679            return false;
680        };
681
682        let hash = secp256k1::Message::from_digest_slice(&self.hash()[..])
683            .expect("Hash is 32 bytes long, same as MESSAGE_SIZE");
684
685        let verification_result =
686            secp256k1::SECP256K1.verify_ecdsa(&hash, &signature.0.to_standard(), pub_key);
687        match verification_result {
688            Ok(()) => true,
689            Err(_) => false,
690        }
691    }
692
693    fn hash(&self) -> [u8; 32] {
694        let hrp = self.hrp_part();
695        let data = self.data_part();
696        let preimage = construct_invoice_preimage(hrp.as_bytes(), &data);
697        sha256(&preimage)
698    }
699
700    /// Recovers the public key used for signing the invoice from the recoverable signature.
701    pub fn recover_payee_pub_key(&self) -> Result<PublicKey, secp256k1::Error> {
702        let hash = secp256k1::Message::from_digest_slice(&self.hash()[..])
703            .expect("Hash is 32 bytes long, same as MESSAGE_SIZE");
704
705        secp256k1::SECP256K1.recover_ecdsa(
706            &hash,
707            &self
708                .signature
709                .as_ref()
710                .ok_or(secp256k1::Error::InvalidSignature)?
711                .0,
712        )
713    }
714
715    /// Returns the payee public key if set in the invoice attributes.
716    pub fn payee_pub_key(&self) -> Option<&PublicKey> {
717        self.data
718            .attrs
719            .iter()
720            .filter_map(|attr| match attr {
721                Attribute::PayeePublicKey(val) => Some(val),
722                _ => None,
723            })
724            .next()
725    }
726
727    /// Returns whether the invoice has a signature.
728    pub fn is_signed(&self) -> bool {
729        self.signature.is_some()
730    }
731
732    /// Returns the payment hash of the invoice.
733    pub fn payment_hash(&self) -> &Hash256 {
734        &self.data.payment_hash
735    }
736
737    /// Returns the amount of the invoice.
738    pub fn amount(&self) -> Option<u128> {
739        self.amount
740    }
741
742    /// Returns the UDT type script if set in the invoice attributes.
743    pub fn udt_type_script(&self) -> Option<&PackedScript> {
744        self.data
745            .attrs
746            .iter()
747            .filter_map(|attr| match attr {
748                Attribute::UdtScript(script) => Some(&script.0),
749                _ => None,
750            })
751            .next()
752    }
753
754    /// Returns the expiry time if set in the invoice attributes.
755    pub fn expiry_time(&self) -> Option<&Duration> {
756        self.data
757            .attrs
758            .iter()
759            .filter_map(|attr| match attr {
760                Attribute::ExpiryTime(val) => Some(val),
761                _ => None,
762            })
763            .next()
764    }
765
766    /// Returns the description if set in the invoice attributes.
767    pub fn description(&self) -> Option<&String> {
768        self.data
769            .attrs
770            .iter()
771            .filter_map(|attr| match attr {
772                Attribute::Description(val) => Some(val),
773                _ => None,
774            })
775            .next()
776    }
777
778    /// Returns the final TLC minimum expiry delta if set in the invoice attributes.
779    pub fn final_tlc_minimum_expiry_delta(&self) -> Option<&u64> {
780        self.data
781            .attrs
782            .iter()
783            .filter_map(|attr| match attr {
784                Attribute::FinalHtlcMinimumExpiryDelta(val) => Some(val),
785                _ => None,
786            })
787            .next()
788    }
789
790    /// Returns the effective final TLC minimum expiry delta.
791    ///
792    /// Uses [`DEFAULT_FINAL_TLC_EXPIRY_DELTA`] when the invoice omits the
793    /// corresponding attribute.
794    pub fn final_tlc_minimum_expiry_delta_or_default(&self) -> u64 {
795        self.final_tlc_minimum_expiry_delta()
796            .copied()
797            .unwrap_or(DEFAULT_FINAL_TLC_EXPIRY_DELTA)
798    }
799
800    /// Returns the fallback address if set in the invoice attributes.
801    pub fn fallback_address(&self) -> Option<&String> {
802        self.data
803            .attrs
804            .iter()
805            .filter_map(|attr| match attr {
806                Attribute::FallbackAddr(val) => Some(val),
807                _ => None,
808            })
809            .next()
810    }
811
812    /// Returns the hash algorithm if set in the invoice attributes.
813    pub fn hash_algorithm(&self) -> Option<&HashAlgorithm> {
814        self.data
815            .attrs
816            .iter()
817            .filter_map(|attr| match attr {
818                Attribute::HashAlgorithm(val) => Some(val),
819                _ => None,
820            })
821            .next()
822    }
823
824    /// Returns the payment secret if set in the invoice attributes.
825    pub fn payment_secret(&self) -> Option<&Hash256> {
826        self.data
827            .attrs
828            .iter()
829            .filter_map(|attr| match attr {
830                Attribute::PaymentSecret(val) => Some(val),
831                _ => None,
832            })
833            .next()
834    }
835
836    /// Returns whether the invoice allows MPP (multi-part payments).
837    pub fn allow_mpp(&self) -> bool {
838        self.data
839            .attrs
840            .iter()
841            .any(|attr| matches!(attr, Attribute::Feature(feature) if feature.supports_basic_mpp()))
842    }
843
844    /// Returns whether the invoice allows trampoline routing.
845    pub fn allow_trampoline_routing(&self) -> bool {
846        self.data
847            .attrs
848            .iter()
849            .any(|attr| matches!(attr, Attribute::Feature(feature) if feature.supports_trampoline_routing()))
850    }
851
852    /// Returns whether the invoice has expired based on the current time.
853    pub fn is_expired(&self) -> bool {
854        self.expiry_time().is_some_and(|expiry| {
855            self.data
856                .timestamp
857                .checked_add(expiry.as_millis())
858                .is_some_and(|expiry_time| {
859                    let now = crate::crate_time::UNIX_EPOCH
860                        .elapsed()
861                        .expect("Duration since unix epoch")
862                        .as_millis();
863                    expiry_time < now
864                })
865        })
866    }
867
868    /// Returns whether the TLC expiry is too soon for the given invoice.
869    pub fn is_tlc_expire_too_soon(&self, tlc_expiry: u64) -> bool {
870        let now = crate::crate_time::UNIX_EPOCH
871            .elapsed()
872            .expect("Duration since unix epoch")
873            .as_millis();
874        let required_expiry = now + u128::from(self.final_tlc_minimum_expiry_delta_or_default());
875        (tlc_expiry as u128) < required_expiry
876    }
877
878    /// Updates the invoice signature using the provided signing function.
879    pub fn update_signature<F>(&mut self, sign_function: F) -> Result<(), InvoiceError>
880    where
881        F: FnOnce(&secp256k1::Message) -> RecoverableSignature,
882    {
883        let hash = self.hash();
884        let message =
885            secp256k1::Message::from_digest_slice(&hash).expect("message from digest slice");
886        let signature = sign_function(&message);
887        self.signature = Some(InvoiceSignature(signature));
888        self.check_signature()?;
889        Ok(())
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    #[test]
898    fn invoice_without_final_expiry_delta_uses_protocol_default() {
899        let now = crate::now_timestamp_as_millis_u64();
900        let invoice = CkbInvoice {
901            currency: Currency::Fibd,
902            amount: Some(1_000),
903            signature: None,
904            data: InvoiceData {
905                timestamp: now.into(),
906                payment_hash: Hash256::default(),
907                attrs: Vec::new(),
908            },
909        };
910
911        assert_eq!(
912            invoice.final_tlc_minimum_expiry_delta_or_default(),
913            DEFAULT_FINAL_TLC_EXPIRY_DELTA
914        );
915        assert!(invoice.is_tlc_expire_too_soon(now + 12 * 60 * 60 * 1_000));
916        assert!(!invoice.is_tlc_expire_too_soon(now + 48 * 60 * 60 * 1_000));
917
918        let explicit_delta = 36 * 60 * 60 * 1_000;
919        let mut invoice_with_explicit_delta = invoice;
920        invoice_with_explicit_delta
921            .data
922            .attrs
923            .push(Attribute::FinalHtlcMinimumExpiryDelta(explicit_delta));
924        assert_eq!(
925            invoice_with_explicit_delta.final_tlc_minimum_expiry_delta_or_default(),
926            explicit_delta
927        );
928    }
929}
930
931impl Display for CkbInvoice {
932    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
933        let hrp = self.hrp_part();
934        let mut data = self.data_part();
935        data.insert(
936            0,
937            u5::try_from_u8(if self.signature.is_some() { 1 } else { 0 }).expect("u5 from u8"),
938        );
939        if let Some(signature) = &self.signature {
940            data.extend_from_slice(&signature.to_base32());
941        }
942        write!(
943            f,
944            "{}",
945            encode(&hrp, data, Variant::Bech32m).expect("encode invoice using Bech32m")
946        )
947    }
948}
949
950impl CkbInvoice {
951    /// Parses an invoice from its bech32m string representation, accepting both
952    /// signed and unsigned invoices.
953    ///
954    /// Most callers should use [`FromStr::from_str`], which additionally requires
955    /// the invoice to be signed. Only use this method in contexts where an
956    /// unsigned invoice is legitimately expected (for example, decoding an
957    /// invoice purely for inspection).
958    pub fn from_str_allowing_unsigned(s: &str) -> Result<Self, InvoiceError> {
959        let (hrp, data, var) = bech32::decode(s).map_err(InvoiceError::Bech32Error)?;
960
961        if var == bech32::Variant::Bech32 {
962            return Err(InvoiceError::Bech32Error(bech32::Error::InvalidChecksum));
963        }
964
965        if data.len() < SIGNATURE_U5_SIZE {
966            return Err(InvoiceError::TooShortDataPart);
967        }
968        let (currency, amount) = parse_hrp(&hrp)?;
969        let is_signed = data[0].to_u8() == 1;
970        let data_end = if is_signed {
971            data.len() - SIGNATURE_U5_SIZE
972        } else {
973            data.len()
974        };
975        let data_part =
976            Vec::<u8>::from_base32(&data[1..data_end]).map_err(InvoiceError::Bech32Error)?;
977        let data_part = ar_decompress_with_limit(&data_part, MAX_INVOICE_DATA_LENGTH)?;
978        let invoice_data = gen_invoice::RawInvoiceData::from_slice(&data_part)
979            .map_err(|err| InvoiceError::MoleculeError(VerificationError(err)))?;
980        let signature = if is_signed {
981            Some(InvoiceSignature::from_base32(
982                &data[data.len() - SIGNATURE_U5_SIZE..],
983            )?)
984        } else {
985            None
986        };
987
988        let invoice = CkbInvoice {
989            currency,
990            amount,
991            signature,
992            data: invoice_data.try_into()?,
993        };
994        invoice.check_signature()?;
995        Ok(invoice)
996    }
997}
998
999impl FromStr for CkbInvoice {
1000    type Err = InvoiceError;
1001
1002    fn from_str(s: &str) -> Result<Self, Self::Err> {
1003        let invoice = CkbInvoice::from_str_allowing_unsigned(s)?;
1004        if !invoice.is_signed() {
1005            return Err(InvoiceError::MissingSignature);
1006        }
1007        Ok(invoice)
1008    }
1009}
1010
1011/// Converts a `[u8]` slice to `[Byte; 32]`.
1012fn u8_slice_to_bytes(slice: &[u8]) -> Result<[Byte; 32], &'static str> {
1013    let vec: Vec<Byte> = slice.iter().map(|&x| Byte::new(x)).collect();
1014    let boxed_slice = vec.into_boxed_slice();
1015    let boxed_array: Box<[Byte; 32]> = match boxed_slice.try_into() {
1016        Ok(ba) => ba,
1017        Err(_) => return Err("Slice length doesn't match array length"),
1018    };
1019    Ok(*boxed_array)
1020}
1021
1022/// Converts molecule bytes to `[u8; 32]`.
1023fn bytes_to_u8_array(array: &molecule::bytes::Bytes) -> [u8; 32] {
1024    let mut res = [0u8; 32];
1025    res.copy_from_slice(array);
1026    res
1027}
1028
1029impl From<InvoiceData> for gen_invoice::RawInvoiceData {
1030    fn from(data: InvoiceData) -> Self {
1031        RawInvoiceDataBuilder::default()
1032            .timestamp(data.timestamp.pack())
1033            .payment_hash(
1034                PaymentHash::new_builder()
1035                    .set(
1036                        u8_slice_to_bytes(data.payment_hash.as_ref()).expect("bytes from u8 slice"),
1037                    )
1038                    .build(),
1039            )
1040            .attrs(
1041                InvoiceAttrsVec::new_builder()
1042                    .set(
1043                        data.attrs
1044                            .iter()
1045                            .map(|a| a.to_owned().into())
1046                            .collect::<Vec<InvoiceAttr>>(),
1047                    )
1048                    .build(),
1049            )
1050            .build()
1051    }
1052}
1053
1054impl TryFrom<gen_invoice::RawInvoiceData> for InvoiceData {
1055    type Error = InvoiceError;
1056
1057    fn try_from(data: gen_invoice::RawInvoiceData) -> Result<Self, Self::Error> {
1058        Ok(InvoiceData {
1059            timestamp: data.timestamp().unpack(),
1060            payment_hash: bytes_to_u8_array(&data.payment_hash().as_bytes()).into(),
1061            attrs: data
1062                .attrs()
1063                .into_iter()
1064                .map(Attribute::try_from)
1065                .collect::<Result<Vec<Attribute>, InvoiceError>>()?,
1066        })
1067    }
1068}
1069
1070impl From<Attribute> for InvoiceAttr {
1071    fn from(attr: Attribute) -> Self {
1072        let a = match attr {
1073            Attribute::ExpiryTime(x) => {
1074                let seconds = x.as_secs();
1075                let value = ExpiryTime::new_builder().value(seconds.pack()).build();
1076                InvoiceAttrUnion::ExpiryTime(value)
1077            }
1078            Attribute::Description(value) => InvoiceAttrUnion::Description(
1079                Description::new_builder().value(value.pack()).build(),
1080            ),
1081            Attribute::FinalHtlcTimeout(value) => InvoiceAttrUnion::FinalHtlcTimeout(
1082                FinalHtlcTimeout::new_builder().value(value.pack()).build(),
1083            ),
1084            Attribute::FinalHtlcMinimumExpiryDelta(value) => {
1085                InvoiceAttrUnion::FinalHtlcMinimumExpiryDelta(
1086                    FinalHtlcMinimumExpiryDelta::new_builder()
1087                        .value(value.pack())
1088                        .build(),
1089                )
1090            }
1091            Attribute::FallbackAddr(value) => InvoiceAttrUnion::FallbackAddr(
1092                FallbackAddr::new_builder().value(value.pack()).build(),
1093            ),
1094            Attribute::Feature(value) => InvoiceAttrUnion::Feature(
1095                Feature::new_builder().value(value.bytes().pack()).build(),
1096            ),
1097            Attribute::UdtScript(script) => {
1098                InvoiceAttrUnion::UdtScript(UdtScript::new_builder().value(script.0).build())
1099            }
1100            Attribute::PayeePublicKey(pubkey) => InvoiceAttrUnion::PayeePublicKey(
1101                PayeePublicKey::new_builder()
1102                    .value(pubkey.serialize().pack())
1103                    .build(),
1104            ),
1105            Attribute::HashAlgorithm(hash_algorithm) => InvoiceAttrUnion::HashAlgorithm(
1106                gen_invoice::HashAlgorithm::new_builder()
1107                    .value(Byte::new(hash_algorithm as u8))
1108                    .build(),
1109            ),
1110            Attribute::PaymentSecret(payment_secret) => InvoiceAttrUnion::PaymentSecret(
1111                PaymentSecret::new_builder()
1112                    .value(payment_secret.into())
1113                    .build(),
1114            ),
1115        };
1116        InvoiceAttr::new_builder().set(a).build()
1117    }
1118}
1119
1120impl TryFrom<InvoiceAttr> for Attribute {
1121    type Error = InvoiceError;
1122
1123    fn try_from(attr: InvoiceAttr) -> Result<Self, Self::Error> {
1124        let attr = match attr.to_enum() {
1125            InvoiceAttrUnion::Description(x) => {
1126                let value: Vec<u8> = x.value().unpack();
1127                Attribute::Description(
1128                    String::from_utf8(value)
1129                        .map_err(|_| InvoiceError::InvalidUtf8Attribute("description"))?,
1130                )
1131            }
1132            InvoiceAttrUnion::ExpiryTime(x) => {
1133                let seconds: u64 = x.value().unpack();
1134                Attribute::ExpiryTime(Duration::from_secs(seconds))
1135            }
1136
1137            InvoiceAttrUnion::FinalHtlcTimeout(x) => {
1138                // This attribute is deprecated since v0.6.0, but we still keep it in molecule for consistency
1139                Attribute::FinalHtlcTimeout(x.value().unpack())
1140            }
1141            InvoiceAttrUnion::FinalHtlcMinimumExpiryDelta(x) => {
1142                Attribute::FinalHtlcMinimumExpiryDelta(x.value().unpack())
1143            }
1144            InvoiceAttrUnion::FallbackAddr(x) => {
1145                let value: Vec<u8> = x.value().unpack();
1146                Attribute::FallbackAddr(
1147                    String::from_utf8(value)
1148                        .map_err(|_| InvoiceError::InvalidUtf8Attribute("fallback_addr"))?,
1149                )
1150            }
1151            InvoiceAttrUnion::Feature(x) => {
1152                Attribute::Feature(FeatureVector::from(x.value().unpack()))
1153            }
1154            InvoiceAttrUnion::UdtScript(x) => Attribute::UdtScript(CkbScript(x.value())),
1155            InvoiceAttrUnion::PayeePublicKey(x) => {
1156                let value: Vec<u8> = x.value().unpack();
1157                Attribute::PayeePublicKey(
1158                    PublicKey::from_slice(&value)
1159                        .map_err(|_| InvoiceError::InvalidPayeePublicKey)?,
1160                )
1161            }
1162            InvoiceAttrUnion::HashAlgorithm(x) => {
1163                let value = x.value();
1164                // Consider unknown algorithm as the default one.
1165                let hash_algorithm = value.try_into().unwrap_or_default();
1166                Attribute::HashAlgorithm(hash_algorithm)
1167            }
1168            InvoiceAttrUnion::PaymentSecret(x) => Attribute::PaymentSecret(x.value().into()),
1169        };
1170        Ok(attr)
1171    }
1172}
1173
1174impl From<anyhow::Error> for InvoiceError {
1175    fn from(_err: anyhow::Error) -> Self {
1176        InvoiceError::InvalidSignature
1177    }
1178}
1179
1180impl TryFrom<gen_invoice::RawCkbInvoice> for CkbInvoice {
1181    type Error = InvoiceError;
1182
1183    fn try_from(invoice: gen_invoice::RawCkbInvoice) -> Result<Self, Self::Error> {
1184        Ok(CkbInvoice {
1185            currency: (u8::from(invoice.currency()))
1186                .try_into()
1187                .map_err(|e: UnknownCurrencyError| InvoiceError::UnknownCurrency(e.0))?,
1188            amount: invoice.amount().to_opt().map(|x| x.unpack()),
1189            signature: invoice
1190                .signature()
1191                .to_opt()
1192                .map(|x| {
1193                    let signature = x
1194                        .as_bytes()
1195                        .into_iter()
1196                        .map(|x| {
1197                            u5::try_from_u8(x).map_err(|_| InvoiceError::InvalidSignatureEncoding)
1198                        })
1199                        .collect::<Result<Vec<u5>, InvoiceError>>()?;
1200                    InvoiceSignature::from_base32_checked(&signature)
1201                })
1202                .transpose()?,
1203            data: InvoiceData::try_from(invoice.data())?,
1204        })
1205    }
1206}
1207
1208impl From<CkbInvoice> for gen_invoice::RawCkbInvoice {
1209    fn from(invoice: CkbInvoice) -> Self {
1210        gen_invoice::RawCkbInvoiceBuilder::default()
1211            .currency((invoice.currency as u8).into())
1212            .amount(
1213                gen_invoice::AmountOpt::new_builder()
1214                    .set(invoice.amount.map(|x| x.pack()))
1215                    .build(),
1216            )
1217            .signature(
1218                gen_invoice::SignatureOpt::new_builder()
1219                    .set({
1220                        invoice.signature.map(|x| {
1221                            let bytes: [Byte; SIGNATURE_U5_SIZE] = x
1222                                .to_base32()
1223                                .iter()
1224                                .map(|x| Byte::new(x.to_u8()))
1225                                .collect::<Vec<_>>()
1226                                .as_slice()
1227                                .try_into()
1228                                .expect("[Byte; 104] from [Byte] slice");
1229                            gen_invoice::Signature::new_builder().set(bytes).build()
1230                        })
1231                    })
1232                    .build(),
1233            )
1234            .data(invoice.data.into())
1235            .build()
1236    }
1237}
1238
1239#[cfg(test)]
1240#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1241#[cfg_attr(not(target_arch = "wasm32"), test)]
1242fn test_parse_hrp() {
1243    use super::InvoiceError;
1244
1245    let res = parse_hrp("fibb1280");
1246    assert_eq!(res, Ok((Currency::Fibb, Some(1280))));
1247
1248    let res = parse_hrp("fibb");
1249    assert_eq!(res, Ok((Currency::Fibb, None)));
1250
1251    let res = parse_hrp("fibt1023");
1252    assert_eq!(res, Ok((Currency::Fibt, Some(1023))));
1253
1254    let res = parse_hrp("fibt10");
1255    assert_eq!(res, Ok((Currency::Fibt, Some(10))));
1256
1257    let res = parse_hrp("fibt");
1258    assert_eq!(res, Ok((Currency::Fibt, None)));
1259
1260    let res = parse_hrp("xnfibb");
1261    assert_eq!(res, Err(InvoiceError::MalformedHRP("xnfibb".to_string())));
1262
1263    let res = parse_hrp("lxfibt");
1264    assert_eq!(res, Err(InvoiceError::MalformedHRP("lxfibt".to_string())));
1265
1266    let res = parse_hrp("fibt");
1267    assert_eq!(res, Ok((Currency::Fibt, None)));
1268
1269    let res = parse_hrp("fixt");
1270    assert_eq!(res, Err(InvoiceError::MalformedHRP("fixt".to_string())));
1271
1272    let res = parse_hrp("fibtt");
1273    assert_eq!(
1274        res,
1275        Err(InvoiceError::MalformedHRP(
1276            "fibtt, unexpected ending `t`".to_string()
1277        ))
1278    );
1279
1280    let res = parse_hrp("fibt1x24");
1281    assert_eq!(
1282        res,
1283        Err(InvoiceError::MalformedHRP(
1284            "fibt1x24, unexpected ending `x24`".to_string()
1285        ))
1286    );
1287
1288    let res = parse_hrp("fibt000");
1289    assert_eq!(res, Ok((Currency::Fibt, Some(0))));
1290
1291    let res = parse_hrp("fibt1024444444444444444444444444444444444444444444444444444444444444");
1292    assert!(matches!(res, Err(InvoiceError::ParseAmountError(_))));
1293
1294    let res = parse_hrp("fibt0x");
1295    assert!(matches!(res, Err(InvoiceError::MalformedHRP(_))));
1296
1297    let res = parse_hrp("");
1298    assert!(matches!(res, Err(InvoiceError::MalformedHRP(_))));
1299}
1300
1301#[cfg(test)]
1302#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1303#[cfg_attr(not(target_arch = "wasm32"), test)]
1304fn test_compress() {
1305    let input = "hrp1gyqsqqq5qqqqq9gqqqqp6qqqqq0qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2qqqqqqqqqqqyvqsqqqsqqqqqvqqqqq8";
1306    let bytes = input.as_bytes();
1307    let compressed = ar_encompress(input.as_bytes()).unwrap();
1308
1309    let decompressed = ar_decompress_with_limit(&compressed, MAX_INVOICE_DATA_LENGTH).unwrap();
1310    let decompressed_str = std::str::from_utf8(&decompressed).unwrap();
1311    assert_eq!(input, decompressed_str);
1312    assert!(compressed.len() < bytes.len());
1313}
1314
1315#[cfg(test)]
1316fn raw_invoice_data_with_attrs(attrs: Vec<InvoiceAttr>) -> gen_invoice::RawInvoiceData {
1317    RawInvoiceDataBuilder::default()
1318        .timestamp(0u128.pack())
1319        .payment_hash(PaymentHash::new_builder().set([Byte::new(0); 32]).build())
1320        .attrs(InvoiceAttrsVec::new_builder().set(attrs).build())
1321        .build()
1322}
1323
1324#[cfg(test)]
1325fn encode_unsigned_invoice(raw_invoice_data: gen_invoice::RawInvoiceData) -> String {
1326    let compressed = ar_encompress(raw_invoice_data.as_slice()).unwrap();
1327    let mut data = vec![u5::try_from_u8(0).unwrap()];
1328    data.extend(compressed.to_base32());
1329    assert!(data.len() >= SIGNATURE_U5_SIZE);
1330    encode("fibb", data, Variant::Bech32m).unwrap()
1331}
1332
1333#[cfg(test)]
1334#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1335#[cfg_attr(not(target_arch = "wasm32"), test)]
1336fn test_parse_malformed_compressed_invoice_returns_error_without_panic() {
1337    let mut data = vec![u5::try_from_u8(0).unwrap()];
1338    data.extend(std::iter::repeat(u5::try_from_u8(31).unwrap()).take(SIGNATURE_U5_SIZE));
1339    let invoice = encode("fibb", data, Variant::Bech32m).unwrap();
1340
1341    let result = std::panic::catch_unwind(|| CkbInvoice::from_str_allowing_unsigned(&invoice));
1342
1343    assert!(result.is_ok());
1344    assert!(result.unwrap().is_err());
1345}
1346
1347#[cfg(test)]
1348#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1349#[cfg_attr(not(target_arch = "wasm32"), test)]
1350fn test_decompressed_invoice_data_length_is_limited() {
1351    let payload = vec![0u8; MAX_INVOICE_DATA_LENGTH + 1];
1352    let compressed = ar_encompress(&payload).unwrap();
1353
1354    let result = ar_decompress_with_limit(&compressed, MAX_INVOICE_DATA_LENGTH);
1355
1356    assert!(matches!(
1357        result,
1358        Err(InvoiceError::InvoiceDataTooLong {
1359            len,
1360            max: MAX_INVOICE_DATA_LENGTH,
1361        }) if len > MAX_INVOICE_DATA_LENGTH
1362    ));
1363}
1364
1365#[cfg(test)]
1366#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1367#[cfg_attr(not(target_arch = "wasm32"), test)]
1368fn test_malformed_text_attribute_returns_error_without_panic() {
1369    let attr = InvoiceAttr::new_builder()
1370        .set(InvoiceAttrUnion::Description(
1371            Description::new_builder()
1372                .value(vec![0xff; 200].pack())
1373                .build(),
1374        ))
1375        .build();
1376    let invoice = encode_unsigned_invoice(raw_invoice_data_with_attrs(vec![attr]));
1377
1378    let result = std::panic::catch_unwind(|| CkbInvoice::from_str_allowing_unsigned(&invoice));
1379
1380    assert!(matches!(
1381        result,
1382        Ok(Err(InvoiceError::InvalidUtf8Attribute("description")))
1383    ));
1384}
1385
1386#[cfg(test)]
1387#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1388#[cfg_attr(not(target_arch = "wasm32"), test)]
1389fn test_malformed_payee_public_key_returns_error_without_panic() {
1390    let attr = InvoiceAttr::new_builder()
1391        .set(InvoiceAttrUnion::PayeePublicKey(
1392            PayeePublicKey::new_builder()
1393                .value(vec![1, 2, 3].pack())
1394                .build(),
1395        ))
1396        .build();
1397    let raw_invoice_data = raw_invoice_data_with_attrs(vec![attr]);
1398
1399    let result = std::panic::catch_unwind(|| InvoiceData::try_from(raw_invoice_data));
1400
1401    assert!(matches!(
1402        result,
1403        Ok(Err(InvoiceError::InvalidPayeePublicKey))
1404    ));
1405}
1406
1407#[cfg(test)]
1408#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1409#[cfg_attr(not(target_arch = "wasm32"), test)]
1410fn test_malformed_raw_invoice_signature_returns_error_without_panic() {
1411    let signature = gen_invoice::Signature::new_builder()
1412        .set([Byte::new(32); SIGNATURE_U5_SIZE])
1413        .build();
1414    let raw_invoice = gen_invoice::RawCkbInvoiceBuilder::default()
1415        .currency(Byte::new(Currency::Fibb as u8))
1416        .signature(
1417            gen_invoice::SignatureOpt::new_builder()
1418                .set(Some(signature))
1419                .build(),
1420        )
1421        .data(raw_invoice_data_with_attrs(vec![]))
1422        .build();
1423
1424    let result = std::panic::catch_unwind(|| CkbInvoice::try_from(raw_invoice));
1425
1426    assert!(matches!(
1427        result,
1428        Ok(Err(InvoiceError::InvalidSignatureEncoding))
1429    ));
1430}