1use 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#[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#[derive(Error, PartialEq, Debug)]
53pub enum InvoiceError {
54 #[error("Bech32 error: {0}")]
56 Bech32Error(bech32::Error),
57 #[error("Molecule error: {0}")]
59 MoleculeError(VerificationError),
60 #[error("Failed to parse amount: {0}")]
62 ParseAmountError(ParseIntError),
63 #[error("Unknown currency: {0}")]
65 UnknownCurrency(String),
66 #[error("Unknown si prefix: {0}")]
68 UnknownSiPrefix(String),
69 #[error("Parsing failed with malformed HRP: {0}")]
71 MalformedHRP(String),
72 #[error("Too short data part")]
74 TooShortDataPart,
75 #[error("Unexpected end of tagged fields")]
77 UnexpectedEndOfTaggedFields,
78 #[error("Integer overflow error")]
80 IntegerOverflowError,
81 #[error("Invalid recovery id")]
83 InvalidRecoveryId,
84 #[error("Invalid slice length: {0}")]
86 InvalidSliceLength(String),
87 #[error("Invalid signature")]
89 InvalidSignature,
90 #[error("Duplicated attribute key: {0}")]
92 DuplicatedAttributeKey(String),
93 #[error("Payment secret is required for MPP payments")]
95 PaymentSecretRequiredForMpp,
96 #[error("Both payment_hash and payment_preimage are set")]
98 BothPaymenthashAndPreimage,
99 #[error("Neither payment_hash nor payment_preimage is set")]
101 NeitherPaymenthashNorPreimage,
102 #[error("Sign error")]
104 SignError,
105 #[error("Hex decode error: {0}")]
107 HexDecodeError(#[from] hex::FromHexError),
108 #[error("Duplicated invoice found: {0}")]
110 DuplicatedInvoice(String),
111 #[error("Description with length of {0} is too long, max length is 639")]
113 DescriptionTooLong(usize),
114 #[error("Invoice not found")]
116 InvoiceNotFound,
117 #[error("Invoice already exists")]
119 InvoiceAlreadyExists,
120 #[error("Deprecated attribute: {0}")]
122 DeprecatedAttribute(String),
123 #[error("Failed to decompress invoice data: {0}")]
125 DecompressionError(String),
126 #[error("Invoice data length {len} exceeds max length {max}")]
128 InvoiceDataTooLong { len: usize, max: usize },
129 #[error("Invalid UTF-8 in invoice {0} attribute")]
131 InvalidUtf8Attribute(&'static str),
132 #[error("Invalid payee public key")]
134 InvalidPayeePublicKey,
135 #[error("Invalid signature encoding")]
137 InvalidSignatureEncoding,
138 #[error("Invoice is not signed")]
140 MissingSignature,
141}
142
143pub const SIGNATURE_U5_SIZE: usize = 104;
145
146pub const MAX_DESCRIPTION_LENGTH: usize = 639;
148
149pub const MAX_INVOICE_DATA_LENGTH: usize = 16 * 1024;
155
156pub const DEFAULT_FINAL_TLC_EXPIRY_DELTA: u64 = 24 * 60 * 60 * 1000;
158
159pub(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
204pub 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 data_part.push(u5::try_from_u8(0).expect("u5 from u8"));
213
214 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
233pub 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#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
256pub enum CkbInvoiceStatus {
257 Open,
259 Cancelled,
261 Expired,
263 Received,
265 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#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
283pub enum Currency {
284 Fibb,
286 Fibt,
288 #[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#[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#[repr(u8)]
336#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
337#[serde(rename_all = "snake_case")]
338pub enum HashAlgorithm {
339 #[default]
341 CkbHash = 0,
342 Sha256 = 1,
344}
345
346#[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
376pub 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#[serde_as]
394#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
395pub struct CkbScript(#[serde_as(as = "EntityHex")] pub PackedScript);
396
397#[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 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#[serde_as]
557#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
558#[serde(rename_all = "snake_case")]
559pub enum Attribute {
560 #[serde(with = "U64Hex")]
562 FinalHtlcTimeout(u64),
563 #[serde(with = "U64Hex")]
568 FinalHtlcMinimumExpiryDelta(u64),
569 #[serde(with = "duration_hex")]
571 ExpiryTime(Duration),
572 Description(String),
574 FallbackAddr(String),
576 UdtScript(CkbScript),
578 PayeePublicKey(PublicKey),
580 HashAlgorithm(HashAlgorithm),
582 Feature(FeatureVector),
584 PaymentSecret(Hash256),
586}
587
588#[serde_as]
590#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
591pub struct InvoiceData {
592 #[serde_as(as = "U128Hex")]
594 pub timestamp: u128,
595 pub payment_hash: Hash256,
597 pub attrs: Vec<Attribute>,
599}
600
601#[serde_as]
607#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
608pub struct CkbInvoice {
609 pub currency: Currency,
611 #[serde_as(as = "Option<U128Hex>")]
613 pub amount: Option<u128>,
614 pub signature: Option<InvoiceSignature>,
616 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 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 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 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 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 pub fn is_signed(&self) -> bool {
729 self.signature.is_some()
730 }
731
732 pub fn payment_hash(&self) -> &Hash256 {
734 &self.data.payment_hash
735 }
736
737 pub fn amount(&self) -> Option<u128> {
739 self.amount
740 }
741
742 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1011fn 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
1022fn 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 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 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}