From 110b5d58b4b4ae81c300c74c5b825972f1fc9be5 Mon Sep 17 00:00:00 2001 From: yukang Date: Tue, 18 Jun 2024 09:46:32 +0800 Subject: [PATCH 1/2] add invoice spec --- docs/specs/payment-invoice.md | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/specs/payment-invoice.md diff --git a/docs/specs/payment-invoice.md b/docs/specs/payment-invoice.md new file mode 100644 index 000000000..3d144e828 --- /dev/null +++ b/docs/specs/payment-invoice.md @@ -0,0 +1,68 @@ +# CKB Fiber Network Invoice Protocol + +## Overall Design + +CKB Fiber network invoice will be generated and parsed by CKB Fiber Node, we referred to the design of [BOLT 11](https://github.com/lightning/bolts/blob/master/11-payment-encoding.md), and made some adjustments from an implementation perspective. + +- The invoice is encode/decoded base on [molecule](https://github.com/nervosnetwork/molecule), which is widely used in the CKB projects. +- Instead of using `bech32`, we switch to `bech32m`. +- The interface and usage is similar to [lightning-invoice](https://github.com/lightningdevkit/rust-lightning/tree/main/lightning-invoice/src) as possible, but not compatible with lightning invoice, any cross-chain compatibility needs will be handled through the hub in ckb-pcn. + +## Human-readable part + +The human-readable part contains these two most important fields: + +1. `prefix`: [mandatory] Specify the currency and network of payment + - `fibb` for the CKB mainnet, `fibb` means `fiber bytes`, since in CKB ecosystem 1 CKB equals 1 Byte. + - `fibt` for the CKB testnet + - `fibd` for the CKB dev +2. `amount`: [optional] An optional number in that currency. + - A standalone number, means the amount of CKB or UDT, for CKB it will be in unit of `shannon`, 1 CKB = 10^8 shannon + - An empty value for this field means the amount of payment is not specified, which maybe used in the scenario of donation. + + +## Encoding and Decoding + +With `molecule`, the data part can be easily converted to bytes. Considering that the bytes generated by molecule are not optimized for space and may contain consecutive zeros when certain fields are empty, the result from `bechm32` encoding is relatively long. We use [arcode-rs](https://github.com/cgbur/arcode-rs) to compress the bytes losslessly before `bechm32` encoding, resulting in a length reduction of almost half: + +`data = compressed(data part molecule bytes) + signature` + +`encode(&hrp, data, Variant::Bech32m)` + +For decoding, we simply perform the inverse decompression operation. + +The `signature` field: [optional] with type of `[u8; 65]` = 520 bits + +- The secp256k1 signature of the entire invoice, can be used to verify the integrity and correctness of the invoice, may also be used to imply the generator node of this invoice. +By default, this filed is none, the method to generate signature: + - `message_hash = SHA256-hash (((human-readable part) → bytes) + (data bytes))` + then sign it with `Secp256k1` + - It may use a customized sign function: `Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)` + +## Data Part + +The data part is designed to add non-mandatory fields easily, and it is very likely that new field will be added in the future: + +1. `timestamp`: [mandatory] 128 bits + - milliseconds since 1970 + - The time the invoice was generated +2. `payment_hash`: [mandatory] 256 bits + - SHA256 payment_hash, could specified when creating a invoice, but we need to make sure `payment_hash` is the unique identifier of a invoice. + - If creating a `HODL` invoice, a `preimage` parameter must be provided, and the `payment_hash` is generated using `blake2b_256(preimage)` when the invoice is created. + - For `AMP invoices` (Atomic Multi-path Payments), the `payment_hash` is randomly generated. +3. `expiry`: [optional] 32 bits + - `timestamp + expiry` is the expiration time of the invoice + - Unit: seconds +4. `description`: [optional] variable length + - A string of UTF-8 text to display payment information, such as "a cup of coffee" +5. `final htlc timeout`: [optional] 32 bits + - Specifies the final htlc timeout, which may be longer because it may take more hops to reach CKB network + - Unit: seconds +6. `fallback`: [optional] variable length + - A CKB address used for fallback in case the invoice payment fails +7. `feature`: [optional] 32 bits + - Feature flag to specify features supported by the payment +8. `payee_public_key`: [optional] 33 bytes + - The public key of the payee +9. `udt_script`: [optional] variable length + - The script specified for the UDT token \ No newline at end of file From 17ae7f9285b7b892213da017052d114d76289f33 Mon Sep 17 00:00:00 2001 From: yukang Date: Tue, 18 Jun 2024 09:46:54 +0800 Subject: [PATCH 2/2] fix invoice address prefix, and remove si_prefix --- src/ckb/schema/invoice.mol | 3 - src/invoice/invoice_impl.rs | 236 +++--------------- src/invoice/utils.rs | 77 +++++- src/rpc/invoice.rs | 4 +- .../3-nodes-transfer/11-node3-gen-invoice.bru | 2 +- .../3-nodes-transfer/16-node3-gen-invoice.bru | 2 +- tests/bruno/e2e/invoice-ops/1-gen-invoice.bru | 5 +- .../invoice-ops/2-gen-invoice-duplicate.bru | 2 +- .../e2e/invoice-ops/3-invoice-decode.bru | 2 +- tests/bruno/e2e/udt/05-node2-gen-invoice.bru | 2 +- 10 files changed, 112 insertions(+), 223 deletions(-) diff --git a/src/ckb/schema/invoice.mol b/src/ckb/schema/invoice.mol index ba30d9f0f..c8e83f09b 100644 --- a/src/ckb/schema/invoice.mol +++ b/src/ckb/schema/invoice.mol @@ -8,8 +8,6 @@ option SignatureOpt (Signature); option AmountOpt (Uint128); -// SI prefix comes from: https://en.wikipedia.org/wiki/International_System_of_Units -option SiPrefixOpt (byte); option FeatureOpt (Uint32); table Duration { @@ -85,7 +83,6 @@ table RawInvoiceData { table RawCkbInvoice { currency: byte, amount: AmountOpt, - prefix: SiPrefixOpt, signature: SignatureOpt, data: RawInvoiceData, } diff --git a/src/invoice/invoice_impl.rs b/src/invoice/invoice_impl.rs index 8c6feb73d..936c6a7b7 100644 --- a/src/invoice/invoice_impl.rs +++ b/src/invoice/invoice_impl.rs @@ -32,8 +32,9 @@ const SIGNATURE_U5_SIZE: usize = 104; /// The currency of the invoice, can also used to represent the CKB network chain. #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] pub enum Currency { - Ckb, - CkbTestNet, + Fibb, + Fibt, + Fibd, } impl TryFrom for Currency { @@ -41,8 +42,9 @@ impl TryFrom for Currency { fn try_from(byte: u8) -> Result { match byte { - 0 => Ok(Self::Ckb), - 1 => Ok(Self::CkbTestNet), + 0 => Ok(Self::Fibb), + 1 => Ok(Self::Fibt), + 2 => Ok(Self::Fibd), _ => Err(InvoiceError::UnknownCurrency(byte.to_string())), } } @@ -51,8 +53,9 @@ impl TryFrom for Currency { impl ToString for Currency { fn to_string(&self) -> String { match self { - Currency::Ckb => "ckb".to_string(), - Currency::CkbTestNet => "ckt".to_string(), + Currency::Fibb => "fibb".to_string(), + Currency::Fibt => "fibt".to_string(), + Currency::Fibd => "fibd".to_string(), } } } @@ -62,71 +65,14 @@ impl FromStr for Currency { fn from_str(s: &str) -> Result { match s { - "ckb" => Ok(Self::Ckb), - "ckt" => Ok(Self::CkbTestNet), + "fibb" => Ok(Self::Fibb), + "fibt" => Ok(Self::Fibt), + "fibd" => Ok(Self::Fibd), _ => Err(InvoiceError::UnknownCurrency(s.to_string())), } } } -// FYI: https://en.wikipedia.org/wiki/International_System_of_Units -#[derive(Eq, PartialEq, Debug, Clone, Copy, Ord, PartialOrd, Serialize, Deserialize)] -pub enum SiPrefix { - /// 10^-3 - Milli, - /// 10^-6 - Micro, - /// 10^3 - Kilo, -} - -impl SiPrefix { - // 1 CKB = 100_000_000 shannons - pub fn multiplier(&self) -> u64 { - match *self { - SiPrefix::Milli => 100_000, - SiPrefix::Micro => 100, - SiPrefix::Kilo => 100_000_000_000, - } - } -} - -impl ToString for SiPrefix { - fn to_string(&self) -> String { - match self { - SiPrefix::Milli => "m".to_string(), - SiPrefix::Micro => "u".to_string(), - SiPrefix::Kilo => "k".to_string(), - } - } -} - -impl TryFrom for SiPrefix { - type Error = InvoiceError; - - fn try_from(byte: u8) -> Result { - match byte { - 0 => Ok(Self::Milli), - 1 => Ok(Self::Micro), - 2 => Ok(Self::Kilo), - _ => Err(InvoiceError::UnknownSiPrefix(format!("{}", byte))), - } - } -} - -impl FromStr for SiPrefix { - type Err = InvoiceError; - - fn from_str(s: &str) -> Result { - match s { - "m" => Ok(Self::Milli), - "u" => Ok(Self::Micro), - "k" => Ok(Self::Kilo), - _ => Err(InvoiceError::UnknownSiPrefix(s.to_string())), - } - } -} - #[serde_as] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct CkbScript(#[serde_as(as = "EntityHex")] pub Script); @@ -166,7 +112,6 @@ pub struct CkbInvoice { pub currency: Currency, #[serde_as(as = "Option")] pub amount: Option, - pub prefix: Option, pub signature: Option, pub data: InvoiceData, } @@ -189,12 +134,10 @@ macro_rules! attr_getter { impl CkbInvoice { fn hrp_part(&self) -> String { format!( - "ln{}{}{}", + "{}{}", self.currency.to_string(), self.amount .map_or_else(|| "".to_string(), |x| x.to_string()), - self.prefix - .map_or_else(|| "".to_string(), |x| x.to_string()), ) } @@ -303,13 +246,8 @@ impl CkbInvoice { Ok(()) } - pub fn amount_ckb_shannons(&self) -> Option { - self.amount.map(|v| { - v * self - .prefix - .as_ref() - .map_or(100_000_000, |si| si.multiplier() as u128) - }) + pub fn amount(&self) -> Option { + self.amount } pub fn udt_type_script(&self) -> Option<&Script> { @@ -446,7 +384,7 @@ impl FromStr for CkbInvoice { if data.len() < SIGNATURE_U5_SIZE { return Err(InvoiceError::TooShortDataPart); } - let (currency, amount, prefix) = parse_hrp(&hrp)?; + let (currency, amount) = parse_hrp(&hrp)?; let is_signed = data[0].to_u8() == 1; let data_end = if is_signed { data.len() - SIGNATURE_U5_SIZE @@ -469,7 +407,6 @@ impl FromStr for CkbInvoice { let invoice = CkbInvoice { currency, amount, - prefix, signature, data: invoice_data.try_into().unwrap(), }; @@ -574,14 +511,13 @@ impl From for Attribute { pub struct InvoiceBuilder { currency: Currency, amount: Option, - prefix: Option, payment_hash: Option, attrs: Vec, } impl Default for InvoiceBuilder { fn default() -> Self { - Self::new(Currency::Ckb) + Self::new(Currency::Fibb) } } @@ -598,7 +534,6 @@ impl InvoiceBuilder { Self { currency, amount: None, - prefix: None, payment_hash: None, attrs: Vec::new(), } @@ -614,11 +549,6 @@ impl InvoiceBuilder { self } - pub fn prefix(mut self, prefix: Option) -> Self { - self.prefix = prefix; - self - } - fn add_attr(mut self, attr: Attribute) -> Self { self.attrs.push(attr); self @@ -666,7 +596,6 @@ impl InvoiceBuilder { Ok(CkbInvoice { currency: self.currency, amount: self.amount, - prefix: self.prefix, signature: None, data: InvoiceData { timestamp, @@ -705,11 +634,6 @@ impl TryFrom for CkbInvoice { Ok(CkbInvoice { currency: (u8::from(invoice.currency())).try_into().unwrap(), amount: invoice.amount().to_opt().map(|x| x.unpack()), - prefix: invoice - .prefix() - .to_opt() - .map(|x| u8::from(x).try_into()) - .transpose()?, signature: invoice.signature().to_opt().map(|x| { InvoiceSignature::from_base32( &x.as_bytes() @@ -733,11 +657,6 @@ impl From for RawCkbInvoice { .set(invoice.amount.map(|x| x.pack())) .build(), ) - .prefix( - SiPrefixOpt::new_builder() - .set(invoice.prefix.map(|x| (x as u8).into())) - .build(), - ) .signature( SignatureOpt::new_builder() .set({ @@ -832,9 +751,8 @@ mod tests { fn mock_invoice() -> CkbInvoice { let (public_key, private_key) = gen_rand_keypair(); let mut invoice = CkbInvoice { - currency: Currency::Ckb, + currency: Currency::Fibb, amount: Some(1280), - prefix: Some(SiPrefix::Kilo), signature: None, data: InvoiceData { payment_hash: rand_sha256_hash(), @@ -859,71 +777,6 @@ mod tests { invoice } - #[test] - fn test_parse_hrp() { - let res = parse_hrp("lnckb1280k"); - assert_eq!(res, Ok((Currency::Ckb, Some(1280), Some(SiPrefix::Kilo)))); - - let res = parse_hrp("lnckb"); - assert_eq!(res, Ok((Currency::Ckb, None, None))); - - let res = parse_hrp("lnckt1023"); - assert_eq!(res, Ok((Currency::CkbTestNet, Some(1023), None))); - - let res = parse_hrp("lnckt1023u"); - assert_eq!( - res, - Ok((Currency::CkbTestNet, Some(1023), Some(SiPrefix::Micro))) - ); - - let res = parse_hrp("lncktk"); - assert_eq!(res, Ok((Currency::CkbTestNet, None, Some(SiPrefix::Kilo)))); - - let res = parse_hrp("xnckb"); - assert_eq!(res, Err(InvoiceError::MalformedHRP("xnckb".to_string()))); - - let res = parse_hrp("lxckb"); - assert_eq!(res, Err(InvoiceError::MalformedHRP("lxckb".to_string()))); - - let res = parse_hrp("lnckt"); - assert_eq!(res, Ok((Currency::CkbTestNet, None, None))); - - let res = parse_hrp("lnxkt"); - assert_eq!(res, Err(InvoiceError::MalformedHRP("lnxkt".to_string()))); - - let res = parse_hrp("lncktt"); - assert_eq!( - res, - Err(InvoiceError::MalformedHRP( - "lncktt, unexpected ending `t`".to_string() - )) - ); - - let res = parse_hrp("lnckt1x24"); - assert_eq!( - res, - Err(InvoiceError::MalformedHRP( - "lnckt1x24, unexpected ending `x24`".to_string() - )) - ); - - let res = parse_hrp("lnckt000k"); - assert_eq!( - res, - Ok((Currency::CkbTestNet, Some(0), Some(SiPrefix::Kilo))) - ); - - let res = - parse_hrp("lnckt1024444444444444444444444444444444444444444444444444444444444444"); - assert!(matches!(res, Err(InvoiceError::ParseAmountError(_)))); - - let res = parse_hrp("lnckt0x"); - assert!(matches!(res, Err(InvoiceError::MalformedHRP(_)))); - - let res = parse_hrp(""); - assert!(matches!(res, Err(InvoiceError::MalformedHRP(_)))); - } - #[test] fn test_signature() { let private_key = gen_rand_private_key(); @@ -945,7 +798,7 @@ mod tests { let decoded_invoice: CkbInvoice = raw_invoice.try_into().unwrap(); assert_eq!(decoded_invoice, ckb_invoice_clone); let address = ckb_invoice_clone.to_string(); - assert!(address.starts_with("lnckb1280k1")); + assert!(address.starts_with("fibb1280")); } #[test] @@ -955,15 +808,12 @@ mod tests { assert_eq!(invoice.check_signature(), Ok(())); let address = invoice.to_string(); - assert!(address.starts_with("lnckb1280k1")); + assert!(address.starts_with("fibb1280")); let decoded_invoice = address.parse::().unwrap(); assert_eq!(decoded_invoice, invoice); assert_eq!(decoded_invoice.is_signed(), true); - assert_eq!( - decoded_invoice.amount_ckb_shannons(), - Some(1280 * 1000 * 100_000_000) - ); + assert_eq!(decoded_invoice.amount(), Some(1280)); } #[test] @@ -971,7 +821,7 @@ mod tests { let invoice = mock_invoice(); let address = invoice.to_string(); - assert!(address.starts_with("lnckb1280k1")); + assert!(address.starts_with("fibb1280")); let mut wrong = address.clone(); wrong.push_str("1"); @@ -1013,9 +863,8 @@ mod tests { let signature = Secp256k1::new() .sign_ecdsa_recoverable(&Message::from_slice(&[0u8; 32]).unwrap(), &private_key); let invoice = CkbInvoice { - currency: Currency::Ckb, + currency: Currency::Fibb, amount: Some(1280), - prefix: Some(SiPrefix::Kilo), signature: Some(InvoiceSignature(signature)), data: InvoiceData { payment_hash: [0u8; 32].into(), @@ -1056,9 +905,8 @@ mod tests { let gen_payment_hash = rand_sha256_hash(); let (public_key, private_key) = gen_rand_keypair(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(gen_payment_hash) .fallback_address("address".to_string()) .expiry_time(Duration::from_secs(1024)) @@ -1074,9 +922,8 @@ mod tests { assert_eq!(invoice, address.parse::().unwrap()); - assert_eq!(invoice.currency, Currency::Ckb); + assert_eq!(invoice.currency, Currency::Fibb); assert_eq!(invoice.amount, Some(1280)); - assert_eq!(invoice.prefix, Some(SiPrefix::Kilo)); assert_eq!(invoice.payment_hash(), &gen_payment_hash); assert_eq!(invoice.payment_preimage(), None); assert_eq!(invoice.data.attrs.len(), 7); @@ -1088,9 +935,8 @@ mod tests { let (_, private_key) = gen_rand_keypair(); let public_key = gen_rand_public_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(gen_payment_hash) .fallback_address("address".to_string()) .expiry_time(Duration::from_secs(1024)) @@ -1108,9 +954,8 @@ mod tests { fn test_invoice_builder_duplicated_attr() { let gen_payment_hash = rand_sha256_hash(); let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(gen_payment_hash) .add_attr(Attribute::FinalHtlcTimeout(5)) .add_attr(Attribute::FinalHtlcTimeout(6)) @@ -1128,17 +973,15 @@ mod tests { #[test] fn test_invoice_builder_missing() { let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_preimage(rand_sha256_hash()) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)); assert_eq!(invoice.err(), None); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(rand_sha256_hash()) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)); @@ -1149,9 +992,8 @@ mod tests { fn test_invoice_builder_preimage() { let preimage = rand_sha256_hash(); let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_preimage(preimage) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) .unwrap(); @@ -1169,9 +1011,8 @@ mod tests { let preimage = rand_sha256_hash(); let payment_hash = rand_sha256_hash(); let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(payment_hash) .payment_preimage(preimage) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)); @@ -1195,16 +1036,14 @@ mod tests { fn test_invoice_timestamp() { let payment_hash = rand_sha256_hash(); let private_key = gen_rand_private_key(); - let invoice1 = InvoiceBuilder::new(Currency::Ckb) + let invoice1 = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(payment_hash) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) .unwrap(); - let invoice2 = InvoiceBuilder::new(Currency::Ckb) + let invoice2 = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(payment_hash) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) .unwrap(); @@ -1216,9 +1055,8 @@ mod tests { #[test] fn test_invoice_gen_payment_hash() { let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_preimage(rand_sha256_hash()) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) .unwrap(); @@ -1231,9 +1069,8 @@ mod tests { #[test] fn test_invoice_rand_payment_hash() { let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) .unwrap(); let payment_preimage = invoice.payment_preimage(); @@ -1244,9 +1081,8 @@ mod tests { fn test_invoice_udt_script() { let script = Script::default(); let private_key = gen_rand_private_key(); - let invoice = InvoiceBuilder::new(Currency::Ckb) + let invoice = InvoiceBuilder::new(Currency::Fibb) .amount(Some(1280)) - .prefix(Some(SiPrefix::Kilo)) .payment_hash(rand_sha256_hash()) .udt_type_script(script.clone()) .build_with_sign(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) diff --git a/src/invoice/utils.rs b/src/invoice/utils.rs index 49fcb6bbe..14026ce9b 100644 --- a/src/invoice/utils.rs +++ b/src/invoice/utils.rs @@ -10,7 +10,7 @@ use nom::{ use rand::Rng; use std::io::{Cursor, Result as IoResult}; -use super::invoice_impl::{Currency, SiPrefix}; +use super::invoice_impl::Currency; use super::InvoiceError; use crate::ckb::types::Hash256; use std::str::FromStr; @@ -164,19 +164,15 @@ impl<'a, W: WriteBase32> Drop for BytesToBase32<'a, W> { } } -fn nom_scan_hrp(input: &str) -> IResult<&str, (&str, Option<&str>, Option<&str>)> { - let (input, _) = tag("ln")(input)?; - let (input, currency) = alt((tag("ckb"), tag("ckt")))(input)?; +fn nom_scan_hrp(input: &str) -> IResult<&str, (&str, Option<&str>)> { + let (input, currency) = alt((tag("fibb"), tag("fibt"), tag("fibd")))(input)?; let (input, amount) = opt(take_while1(|c: char| c.is_numeric()))(input)?; - let (input, si) = opt(take_while1(|c: char| ['m', 'u', 'k'].contains(&c)))(input)?; - Ok((input, (currency, amount, si))) + Ok((input, (currency, amount))) } -pub(crate) fn parse_hrp( - input: &str, -) -> Result<(Currency, Option, Option), InvoiceError> { +pub(crate) fn parse_hrp(input: &str) -> Result<(Currency, Option), InvoiceError> { match nom_scan_hrp(input) { - Ok((left, (currency, amount, si_prefix))) => { + Ok((left, (currency, amount))) => { if !left.is_empty() { return Err(InvoiceError::MalformedHRP(format!( "{}, unexpected ending `{}`", @@ -187,8 +183,7 @@ pub(crate) fn parse_hrp( let amount = amount .map(|x| x.parse().map_err(InvoiceError::ParseAmountError)) .transpose()?; - let si_prefix = si_prefix.map(SiPrefix::from_str).transpose()?; - Ok((currency, amount, si_prefix)) + Ok((currency, amount)) } Err(_) => Err(InvoiceError::MalformedHRP(input.to_string())), } @@ -221,3 +216,61 @@ pub(crate) fn rand_sha256_hash() -> Hash256 { rng.fill(&mut result[..]); result.into() } + +#[test] +fn test_parse_hrp() { + let res = parse_hrp("fibb1280"); + assert_eq!(res, Ok((Currency::Fibb, Some(1280)))); + + let res = parse_hrp("fibb"); + assert_eq!(res, Ok((Currency::Fibb, None))); + + let res = parse_hrp("fibt1023"); + assert_eq!(res, Ok((Currency::Fibt, Some(1023)))); + + let res = parse_hrp("fibt10"); + assert_eq!(res, Ok((Currency::Fibt, Some(10)))); + + let res = parse_hrp("fibt"); + assert_eq!(res, Ok((Currency::Fibt, None))); + + let res = parse_hrp("xnfibb"); + assert_eq!(res, Err(InvoiceError::MalformedHRP("xnfibb".to_string()))); + + let res = parse_hrp("lxfibt"); + assert_eq!(res, Err(InvoiceError::MalformedHRP("lxfibt".to_string()))); + + let res = parse_hrp("fibt"); + assert_eq!(res, Ok((Currency::Fibt, None))); + + let res = parse_hrp("fixt"); + assert_eq!(res, Err(InvoiceError::MalformedHRP("fixt".to_string()))); + + let res = parse_hrp("fibtt"); + assert_eq!( + res, + Err(InvoiceError::MalformedHRP( + "fibtt, unexpected ending `t`".to_string() + )) + ); + + let res = parse_hrp("fibt1x24"); + assert_eq!( + res, + Err(InvoiceError::MalformedHRP( + "fibt1x24, unexpected ending `x24`".to_string() + )) + ); + + let res = parse_hrp("fibt000"); + assert_eq!(res, Ok((Currency::Fibt, Some(0)))); + + let res = parse_hrp("fibt1024444444444444444444444444444444444444444444444444444444444444"); + assert!(matches!(res, Err(InvoiceError::ParseAmountError(_)))); + + let res = parse_hrp("fibt0x"); + assert!(matches!(res, Err(InvoiceError::MalformedHRP(_)))); + + let res = parse_hrp(""); + assert!(matches!(res, Err(InvoiceError::MalformedHRP(_)))); +} diff --git a/src/rpc/invoice.rs b/src/rpc/invoice.rs index 82b4a83e6..0cf5ab234 100644 --- a/src/rpc/invoice.rs +++ b/src/rpc/invoice.rs @@ -29,7 +29,7 @@ pub struct NewInvoiceParams { #[derive(Clone, Serialize, Deserialize)] pub struct NewInvoiceResult { - pub invoice_string: String, + pub invoice_address: String, pub invoice: CkbInvoice, } @@ -100,7 +100,7 @@ where match invoice_builder.build() { Ok(invoice) => match self.store.insert_invoice(invoice.clone()) { Ok(_) => Ok(NewInvoiceResult { - invoice_string: invoice.to_string(), + invoice_address: invoice.to_string(), invoice, }), Err(e) => { diff --git a/tests/bruno/e2e/3-nodes-transfer/11-node3-gen-invoice.bru b/tests/bruno/e2e/3-nodes-transfer/11-node3-gen-invoice.bru index a7b259c1f..311ae411b 100644 --- a/tests/bruno/e2e/3-nodes-transfer/11-node3-gen-invoice.bru +++ b/tests/bruno/e2e/3-nodes-transfer/11-node3-gen-invoice.bru @@ -36,7 +36,7 @@ body:json { "params": [ { "amount": "0x613ae6500", - "currency": "Ckb", + "currency": "Fibb", "description": "test invoice generated by node3", "expiry": "0xe10", "final_cltv": "0x28", diff --git a/tests/bruno/e2e/3-nodes-transfer/16-node3-gen-invoice.bru b/tests/bruno/e2e/3-nodes-transfer/16-node3-gen-invoice.bru index 7dbe6564f..20e7a722c 100644 --- a/tests/bruno/e2e/3-nodes-transfer/16-node3-gen-invoice.bru +++ b/tests/bruno/e2e/3-nodes-transfer/16-node3-gen-invoice.bru @@ -36,7 +36,7 @@ body:json { "params": [ { "amount": "0x867ba4900", - "currency": "Ckb", + "currency": "Fibb", "description": "test invoice generated by node3", "expiry": "0xe10", "final_cltv": "0x28", diff --git a/tests/bruno/e2e/invoice-ops/1-gen-invoice.bru b/tests/bruno/e2e/invoice-ops/1-gen-invoice.bru index f05c72100..cacace8e4 100644 --- a/tests/bruno/e2e/invoice-ops/1-gen-invoice.bru +++ b/tests/bruno/e2e/invoice-ops/1-gen-invoice.bru @@ -36,7 +36,7 @@ body:json { "params": [ { "amount": "0x64", - "currency": "Ckb", + "currency": "Fibb", "description": "test invoice", "expiry": "0xe10", "final_cltv": "0x28", @@ -55,4 +55,7 @@ script:post-response { // Sleep for sometime to make sure current operation finishes before next request starts. await new Promise(r => setTimeout(r, 100)); console.log("generated result: ", res.body.result); + bru.setVar("INVOICE_ADDR", res.body.result.invoice_address); + + } diff --git a/tests/bruno/e2e/invoice-ops/2-gen-invoice-duplicate.bru b/tests/bruno/e2e/invoice-ops/2-gen-invoice-duplicate.bru index 978b8a90d..a77e7c737 100644 --- a/tests/bruno/e2e/invoice-ops/2-gen-invoice-duplicate.bru +++ b/tests/bruno/e2e/invoice-ops/2-gen-invoice-duplicate.bru @@ -27,7 +27,7 @@ body:json { "params": [ { "amount": "0x64", - "currency": "Ckb", + "currency": "Fibb", "description": "test invoice", "expiry": "0xe10", "final_cltv": "0x28", diff --git a/tests/bruno/e2e/invoice-ops/3-invoice-decode.bru b/tests/bruno/e2e/invoice-ops/3-invoice-decode.bru index 6828745f4..f6b1ae4e7 100644 --- a/tests/bruno/e2e/invoice-ops/3-invoice-decode.bru +++ b/tests/bruno/e2e/invoice-ops/3-invoice-decode.bru @@ -22,7 +22,7 @@ body:json { "method": "parse_invoice", "params": [ { - "invoice": "lnckb1001qkeymvj3p67gfw0pmm6vk2ka0fc6py4a9g3sv09rfdzpw2lsw4glgkd9gz7lxleqxznxxcxx6kk0w6lw45a448gk2ap3r6ew8xrp9uhv9axfnnwe2lagmy6yhx5ku4pv7fsqckefkje45wjuptt3hknrajkvwgy74zrpxy8ht668pvr26jdcdgw0m3cf5vl9u2mjl8s" + "invoice": "{{INVOICE_ADDR}}" } ] } diff --git a/tests/bruno/e2e/udt/05-node2-gen-invoice.bru b/tests/bruno/e2e/udt/05-node2-gen-invoice.bru index cd19c848a..f8f0b3340 100644 --- a/tests/bruno/e2e/udt/05-node2-gen-invoice.bru +++ b/tests/bruno/e2e/udt/05-node2-gen-invoice.bru @@ -36,7 +36,7 @@ body:json { "params": [ { "amount": "0xc8", - "currency": "Ckb", + "currency": "Fibb", "description": "test invoice generated by node2", "expiry": "0xe10", "final_cltv": "0x28",