mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-02-25 07:17:40 +01:00
Merge pull request #1057 from TheBlueMatt/2021-08-invoice-fails
Fix and modernize lightning-invoice API
This commit is contained in:
commit
6bd1af4f9f
8 changed files with 454 additions and 139 deletions
|
@ -16,4 +16,5 @@ num-traits = "0.2.8"
|
||||||
bitcoin_hashes = "0.10"
|
bitcoin_hashes = "0.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
hex = "0.3"
|
||||||
lightning = { version = "0.0.100", path = "../lightning", features = ["_test_utils"] }
|
lightning = { version = "0.0.100", path = "../lightning", features = ["_test_utils"] }
|
||||||
|
|
|
@ -77,7 +77,7 @@ mod hrp_sm {
|
||||||
} else if ['m', 'u', 'n', 'p'].contains(&read_symbol) {
|
} else if ['m', 'u', 'n', 'p'].contains(&read_symbol) {
|
||||||
Ok(States::ParseAmountSiPrefix)
|
Ok(States::ParseAmountSiPrefix)
|
||||||
} else {
|
} else {
|
||||||
Err(super::ParseError::MalformedHRP)
|
Err(super::ParseError::UnknownSiPrefix)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
States::ParseAmountSiPrefix => Err(super::ParseError::MalformedHRP),
|
States::ParseAmountSiPrefix => Err(super::ParseError::MalformedHRP),
|
||||||
|
@ -209,10 +209,18 @@ impl FromStr for SiPrefix {
|
||||||
/// ```
|
/// ```
|
||||||
/// use lightning_invoice::Invoice;
|
/// use lightning_invoice::Invoice;
|
||||||
///
|
///
|
||||||
/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\
|
///
|
||||||
/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\
|
/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\
|
||||||
/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\
|
/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\
|
||||||
/// ky03ylcqca784w";
|
/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\
|
||||||
|
/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\
|
||||||
|
/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\
|
||||||
|
/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\
|
||||||
|
/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\
|
||||||
|
/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\
|
||||||
|
/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\
|
||||||
|
/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\
|
||||||
|
/// j5r6drg6k6zcqj0fcwg";
|
||||||
///
|
///
|
||||||
/// assert!(invoice.parse::<Invoice>().is_ok());
|
/// assert!(invoice.parse::<Invoice>().is_ok());
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -228,10 +236,17 @@ impl FromStr for Invoice {
|
||||||
/// ```
|
/// ```
|
||||||
/// use lightning_invoice::*;
|
/// use lightning_invoice::*;
|
||||||
///
|
///
|
||||||
/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\
|
/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\
|
||||||
/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\
|
/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\
|
||||||
/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\
|
/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\
|
||||||
/// ky03ylcqca784w";
|
/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\
|
||||||
|
/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\
|
||||||
|
/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\
|
||||||
|
/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\
|
||||||
|
/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\
|
||||||
|
/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\
|
||||||
|
/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\
|
||||||
|
/// j5r6drg6k6zcqj0fcwg";
|
||||||
///
|
///
|
||||||
/// let parsed_1 = invoice.parse::<Invoice>();
|
/// let parsed_1 = invoice.parse::<Invoice>();
|
||||||
///
|
///
|
||||||
|
@ -404,7 +419,7 @@ fn parse_tagged_parts(data: &[u5]) -> Result<Vec<RawTaggedField>, ParseError> {
|
||||||
Ok(field) => {
|
Ok(field) => {
|
||||||
parts.push(RawTaggedField::KnownSemantics(field))
|
parts.push(RawTaggedField::KnownSemantics(field))
|
||||||
},
|
},
|
||||||
Err(ParseError::Skip) => {
|
Err(ParseError::Skip)|Err(ParseError::Bech32Error(bech32::Error::InvalidLength)) => {
|
||||||
parts.push(RawTaggedField::UnknownSemantics(field.into()))
|
parts.push(RawTaggedField::UnknownSemantics(field.into()))
|
||||||
},
|
},
|
||||||
Err(e) => {return Err(e)}
|
Err(e) => {return Err(e)}
|
||||||
|
|
|
@ -127,6 +127,7 @@ pub fn check_platform() {
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// extern crate secp256k1;
|
/// extern crate secp256k1;
|
||||||
|
/// extern crate lightning;
|
||||||
/// extern crate lightning_invoice;
|
/// extern crate lightning_invoice;
|
||||||
/// extern crate bitcoin_hashes;
|
/// extern crate bitcoin_hashes;
|
||||||
///
|
///
|
||||||
|
@ -136,6 +137,8 @@ pub fn check_platform() {
|
||||||
/// use secp256k1::Secp256k1;
|
/// use secp256k1::Secp256k1;
|
||||||
/// use secp256k1::key::SecretKey;
|
/// use secp256k1::key::SecretKey;
|
||||||
///
|
///
|
||||||
|
/// use lightning::ln::PaymentSecret;
|
||||||
|
///
|
||||||
/// use lightning_invoice::{Currency, InvoiceBuilder};
|
/// use lightning_invoice::{Currency, InvoiceBuilder};
|
||||||
///
|
///
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
|
@ -148,10 +151,12 @@ pub fn check_platform() {
|
||||||
/// ).unwrap();
|
/// ).unwrap();
|
||||||
///
|
///
|
||||||
/// let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
|
/// let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
|
||||||
|
/// let payment_secret = PaymentSecret([42u8; 32]);
|
||||||
///
|
///
|
||||||
/// let invoice = InvoiceBuilder::new(Currency::Bitcoin)
|
/// let invoice = InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
/// .description("Coins pls!".into())
|
/// .description("Coins pls!".into())
|
||||||
/// .payment_hash(payment_hash)
|
/// .payment_hash(payment_hash)
|
||||||
|
/// .payment_secret(payment_secret)
|
||||||
/// .current_timestamp()
|
/// .current_timestamp()
|
||||||
/// .min_final_cltv_expiry(144)
|
/// .min_final_cltv_expiry(144)
|
||||||
/// .build_signed(|hash| {
|
/// .build_signed(|hash| {
|
||||||
|
@ -321,7 +326,7 @@ impl SiPrefix {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enum representing the crypto currencies (or networks) supported by this library
|
/// Enum representing the crypto currencies (or networks) supported by this library
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub enum Currency {
|
pub enum Currency {
|
||||||
/// Bitcoin mainnet
|
/// Bitcoin mainnet
|
||||||
Bitcoin,
|
Bitcoin,
|
||||||
|
@ -342,7 +347,7 @@ pub enum Currency {
|
||||||
/// Tagged field which may have an unknown tag
|
/// Tagged field which may have an unknown tag
|
||||||
///
|
///
|
||||||
/// (C-not exported) as we don't currently support TaggedField
|
/// (C-not exported) as we don't currently support TaggedField
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub enum RawTaggedField {
|
pub enum RawTaggedField {
|
||||||
/// Parsed tagged field with known tag
|
/// Parsed tagged field with known tag
|
||||||
KnownSemantics(TaggedField),
|
KnownSemantics(TaggedField),
|
||||||
|
@ -357,7 +362,7 @@ pub enum RawTaggedField {
|
||||||
/// (C-not exported) As we don't yet support enum variants with the same name the struct contained
|
/// (C-not exported) As we don't yet support enum variants with the same name the struct contained
|
||||||
/// in the variant.
|
/// in the variant.
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub enum TaggedField {
|
pub enum TaggedField {
|
||||||
PaymentHash(Sha256),
|
PaymentHash(Sha256),
|
||||||
Description(Description),
|
Description(Description),
|
||||||
|
@ -372,18 +377,18 @@ pub enum TaggedField {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SHA-256 hash
|
/// SHA-256 hash
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct Sha256(pub sha256::Hash);
|
pub struct Sha256(pub sha256::Hash);
|
||||||
|
|
||||||
/// Description string
|
/// Description string
|
||||||
///
|
///
|
||||||
/// # Invariants
|
/// # Invariants
|
||||||
/// The description can be at most 639 __bytes__ long
|
/// The description can be at most 639 __bytes__ long
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct Description(String);
|
pub struct Description(String);
|
||||||
|
|
||||||
/// Payee public key
|
/// Payee public key
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct PayeePubKey(pub PublicKey);
|
pub struct PayeePubKey(pub PublicKey);
|
||||||
|
|
||||||
/// Positive duration that defines when (relatively to the timestamp) in the future the invoice
|
/// Positive duration that defines when (relatively to the timestamp) in the future the invoice
|
||||||
|
@ -393,17 +398,17 @@ pub struct PayeePubKey(pub PublicKey);
|
||||||
/// The number of seconds this expiry time represents has to be in the range
|
/// The number of seconds this expiry time represents has to be in the range
|
||||||
/// `0...(SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME)` to avoid overflows when adding it to a
|
/// `0...(SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME)` to avoid overflows when adding it to a
|
||||||
/// timestamp
|
/// timestamp
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct ExpiryTime(Duration);
|
pub struct ExpiryTime(Duration);
|
||||||
|
|
||||||
/// `min_final_cltv_expiry` to use for the last HTLC in the route
|
/// `min_final_cltv_expiry` to use for the last HTLC in the route
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct MinFinalCltvExpiry(pub u64);
|
pub struct MinFinalCltvExpiry(pub u64);
|
||||||
|
|
||||||
// TODO: better types instead onf byte arrays
|
// TODO: better types instead onf byte arrays
|
||||||
/// Fallback address in case no LN payment is possible
|
/// Fallback address in case no LN payment is possible
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub enum Fallback {
|
pub enum Fallback {
|
||||||
SegWitProgram {
|
SegWitProgram {
|
||||||
version: u5,
|
version: u5,
|
||||||
|
@ -414,7 +419,7 @@ pub enum Fallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recoverable signature
|
/// Recoverable signature
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct InvoiceSignature(pub RecoverableSignature);
|
pub struct InvoiceSignature(pub RecoverableSignature);
|
||||||
|
|
||||||
/// Private routing information
|
/// Private routing information
|
||||||
|
@ -422,7 +427,7 @@ pub struct InvoiceSignature(pub RecoverableSignature);
|
||||||
/// # Invariants
|
/// # Invariants
|
||||||
/// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops)
|
/// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops)
|
||||||
///
|
///
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct PrivateRoute(RouteHint);
|
pub struct PrivateRoute(RouteHint);
|
||||||
|
|
||||||
/// Tag constants as specified in BOLT11
|
/// Tag constants as specified in BOLT11
|
||||||
|
@ -480,8 +485,9 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the amount in pico BTC. The optimal SI prefix is choosen automatically.
|
/// Sets the amount in millisatoshis. The optimal SI prefix is chosen automatically.
|
||||||
pub fn amount_pico_btc(mut self, amount: u64) -> Self {
|
pub fn amount_milli_satoshis(mut self, amount_msat: u64) -> Self {
|
||||||
|
let amount = amount_msat * 10; // Invoices are denominated in "pico BTC"
|
||||||
let biggest_possible_si_prefix = SiPrefix::values_desc()
|
let biggest_possible_si_prefix = SiPrefix::values_desc()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|prefix| amount % prefix.multiplier() == 0)
|
.find(|prefix| amount % prefix.multiplier() == 0)
|
||||||
|
@ -633,7 +639,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
|
impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, tb::True> {
|
||||||
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
|
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
|
||||||
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
|
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
|
||||||
/// the included payee public key.
|
/// the included payee public key.
|
||||||
|
@ -673,6 +679,7 @@ impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
|
||||||
|
|
||||||
invoice.check_field_counts().expect("should be ensured by type signature of builder");
|
invoice.check_field_counts().expect("should be ensured by type signature of builder");
|
||||||
invoice.check_feature_bits().expect("should be ensured by type signature of builder");
|
invoice.check_feature_bits().expect("should be ensured by type signature of builder");
|
||||||
|
invoice.check_amount().expect("should be ensured by type signature of builder");
|
||||||
|
|
||||||
Ok(invoice)
|
Ok(invoice)
|
||||||
}
|
}
|
||||||
|
@ -1016,35 +1023,54 @@ impl Invoice {
|
||||||
return Err(SemanticError::MultipleDescriptions);
|
return Err(SemanticError::MultipleDescriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.check_payment_secret()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that there is exactly one payment secret field
|
||||||
|
fn check_payment_secret(&self) -> Result<(), SemanticError> {
|
||||||
|
// "A writer MUST include exactly one `s` field."
|
||||||
|
let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf {
|
||||||
|
TaggedField::PaymentSecret(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}).count();
|
||||||
|
if payment_secret_count < 1 {
|
||||||
|
return Err(SemanticError::NoPaymentSecret);
|
||||||
|
} else if payment_secret_count > 1 {
|
||||||
|
return Err(SemanticError::MultiplePaymentSecrets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that amount is a whole number of millisatoshis
|
||||||
|
fn check_amount(&self) -> Result<(), SemanticError> {
|
||||||
|
if let Some(amount_pico_btc) = self.amount_pico_btc() {
|
||||||
|
if amount_pico_btc % 10 != 0 {
|
||||||
|
return Err(SemanticError::ImpreciseAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check that feature bits are set as required
|
/// Check that feature bits are set as required
|
||||||
fn check_feature_bits(&self) -> Result<(), SemanticError> {
|
fn check_feature_bits(&self) -> Result<(), SemanticError> {
|
||||||
// "If the payment_secret feature is set, MUST include exactly one s field."
|
self.check_payment_secret()?;
|
||||||
let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf {
|
|
||||||
TaggedField::PaymentSecret(_) => true,
|
|
||||||
_ => false,
|
|
||||||
}).count();
|
|
||||||
if payment_secret_count > 1 {
|
|
||||||
return Err(SemanticError::MultiplePaymentSecrets);
|
|
||||||
}
|
|
||||||
|
|
||||||
// "A writer MUST set an s field if and only if the payment_secret feature is set."
|
// "A writer MUST set an s field if and only if the payment_secret feature is set."
|
||||||
let has_payment_secret = payment_secret_count == 1;
|
// (this requirement has been since removed, and we now require the payment secret
|
||||||
|
// feature bit always).
|
||||||
let features = self.tagged_fields().find(|&tf| match *tf {
|
let features = self.tagged_fields().find(|&tf| match *tf {
|
||||||
TaggedField::Features(_) => true,
|
TaggedField::Features(_) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
});
|
});
|
||||||
match features {
|
match features {
|
||||||
None if has_payment_secret => Err(SemanticError::InvalidFeatures),
|
None => Err(SemanticError::InvalidFeatures),
|
||||||
None => Ok(()),
|
|
||||||
Some(TaggedField::Features(features)) => {
|
Some(TaggedField::Features(features)) => {
|
||||||
if features.supports_payment_secret() && has_payment_secret {
|
if features.requires_unknown_bits() {
|
||||||
Ok(())
|
|
||||||
} else if has_payment_secret {
|
|
||||||
Err(SemanticError::InvalidFeatures)
|
Err(SemanticError::InvalidFeatures)
|
||||||
} else if features.supports_payment_secret() {
|
} else if !features.supports_payment_secret() {
|
||||||
Err(SemanticError::InvalidFeatures)
|
Err(SemanticError::InvalidFeatures)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1059,7 +1085,9 @@ impl Invoice {
|
||||||
match self.signed_invoice.recover_payee_pub_key() {
|
match self.signed_invoice.recover_payee_pub_key() {
|
||||||
Err(secp256k1::Error::InvalidRecoveryId) =>
|
Err(secp256k1::Error::InvalidRecoveryId) =>
|
||||||
return Err(SemanticError::InvalidRecoveryId),
|
return Err(SemanticError::InvalidRecoveryId),
|
||||||
Err(_) => panic!("no other error may occur"),
|
Err(secp256k1::Error::InvalidSignature) =>
|
||||||
|
return Err(SemanticError::InvalidSignature),
|
||||||
|
Err(e) => panic!("no other error may occur, got {:?}", e),
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1074,10 +1102,17 @@ impl Invoice {
|
||||||
/// ```
|
/// ```
|
||||||
/// use lightning_invoice::*;
|
/// use lightning_invoice::*;
|
||||||
///
|
///
|
||||||
/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\
|
/// let invoice = "lnbc100p1psj9jhxdqud3jxktt5w46x7unfv9kz6mn0v3jsnp4q0d3p2sfluzdx45tqcs\
|
||||||
/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\
|
/// h2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5q6rmq35js88zp5dvwrv9m459tnk2zunwj5jalqtyxqulh0l\
|
||||||
/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\
|
/// 5gflssp5nf55ny5gcrfl30xuhzj3nphgj27rstekmr9fw3ny5989s300gyus9qyysgqcqpcrzjqw2sxwe993\
|
||||||
/// ky03ylcqca784w";
|
/// h5pcm4dxzpvttgza8zhkqxpgffcrf5v25nwpr3cmfg7z54kuqq8rgqqqqqqqq2qqqqq9qq9qrzjqd0ylaqcl\
|
||||||
|
/// j9424x9m8h2vcukcgnm6s56xfgu3j78zyqzhgs4hlpzvznlugqq9vsqqqqqqqlgqqqqqeqq9qrzjqwldmj9d\
|
||||||
|
/// ha74df76zhx6l9we0vjdquygcdt3kssupehe64g6yyp5yz5rhuqqwccqqyqqqqlgqqqqjcqq9qrzjqf9e58a\
|
||||||
|
/// guqr0rcun0ajlvmzq3ek63cw2w282gv3z5uupmuwvgjtq2z55qsqqg6qqqyqqqrtnqqqzq3cqygrzjqvphms\
|
||||||
|
/// ywntrrhqjcraumvc4y6r8v4z5v593trte429v4hredj7ms5z52usqq9ngqqqqqqqlgqqqqqqgq9qrzjq2v0v\
|
||||||
|
/// p62g49p7569ev48cmulecsxe59lvaw3wlxm7r982zxa9zzj7z5l0cqqxusqqyqqqqlgqqqqqzsqygarl9fh3\
|
||||||
|
/// 8s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5m7ke54n2d5sym4xcmxtl8238xxvw5h5h5\
|
||||||
|
/// j5r6drg6k6zcqj0fcwg";
|
||||||
///
|
///
|
||||||
/// let signed = invoice.parse::<SignedRawInvoice>().unwrap();
|
/// let signed = invoice.parse::<SignedRawInvoice>().unwrap();
|
||||||
///
|
///
|
||||||
|
@ -1090,6 +1125,7 @@ impl Invoice {
|
||||||
invoice.check_field_counts()?;
|
invoice.check_field_counts()?;
|
||||||
invoice.check_feature_bits()?;
|
invoice.check_feature_bits()?;
|
||||||
invoice.check_signature()?;
|
invoice.check_signature()?;
|
||||||
|
invoice.check_amount()?;
|
||||||
|
|
||||||
Ok(invoice)
|
Ok(invoice)
|
||||||
}
|
}
|
||||||
|
@ -1130,8 +1166,8 @@ impl Invoice {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the payment secret if one was included in the invoice
|
/// Get the payment secret if one was included in the invoice
|
||||||
pub fn payment_secret(&self) -> Option<&PaymentSecret> {
|
pub fn payment_secret(&self) -> &PaymentSecret {
|
||||||
self.signed_invoice.payment_secret()
|
self.signed_invoice.payment_secret().expect("was checked by constructor")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the invoice features if they were included in the invoice
|
/// Get the invoice features if they were included in the invoice
|
||||||
|
@ -1388,6 +1424,10 @@ pub enum SemanticError {
|
||||||
/// The invoice contains multiple descriptions and/or description hashes which isn't allowed
|
/// The invoice contains multiple descriptions and/or description hashes which isn't allowed
|
||||||
MultipleDescriptions,
|
MultipleDescriptions,
|
||||||
|
|
||||||
|
/// The invoice is missing the mandatory payment secret, which all modern lightning nodes
|
||||||
|
/// should provide.
|
||||||
|
NoPaymentSecret,
|
||||||
|
|
||||||
/// The invoice contains multiple payment secrets
|
/// The invoice contains multiple payment secrets
|
||||||
MultiplePaymentSecrets,
|
MultiplePaymentSecrets,
|
||||||
|
|
||||||
|
@ -1399,6 +1439,9 @@ pub enum SemanticError {
|
||||||
|
|
||||||
/// The invoice's signature is invalid
|
/// The invoice's signature is invalid
|
||||||
InvalidSignature,
|
InvalidSignature,
|
||||||
|
|
||||||
|
/// The invoice's amount was not a whole number of millisatoshis
|
||||||
|
ImpreciseAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for SemanticError {
|
impl Display for SemanticError {
|
||||||
|
@ -1408,10 +1451,12 @@ impl Display for SemanticError {
|
||||||
SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"),
|
SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"),
|
||||||
SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"),
|
SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"),
|
||||||
SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"),
|
SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"),
|
||||||
|
SemanticError::NoPaymentSecret => f.write_str("The invoice is missing the mandatory payment secret"),
|
||||||
SemanticError::MultiplePaymentSecrets => f.write_str("The invoice contains multiple payment secrets"),
|
SemanticError::MultiplePaymentSecrets => f.write_str("The invoice contains multiple payment secrets"),
|
||||||
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
|
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
|
||||||
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
|
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
|
||||||
SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"),
|
SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"),
|
||||||
|
SemanticError::ImpreciseAmount => f.write_str("The invoice's amount was not a whole number of millisatoshis"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1623,7 +1668,7 @@ mod test {
|
||||||
let invoice = invoice_template.clone();
|
let invoice = invoice_template.clone();
|
||||||
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
|
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
|
||||||
}.unwrap();
|
}.unwrap();
|
||||||
assert!(Invoice::from_signed(invoice).is_ok());
|
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
|
||||||
|
|
||||||
// No payment secret or feature bits
|
// No payment secret or feature bits
|
||||||
let invoice = {
|
let invoice = {
|
||||||
|
@ -1631,7 +1676,7 @@ mod test {
|
||||||
invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into());
|
invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into());
|
||||||
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
|
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
|
||||||
}.unwrap();
|
}.unwrap();
|
||||||
assert!(Invoice::from_signed(invoice).is_ok());
|
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
|
||||||
|
|
||||||
// Missing payment secret
|
// Missing payment secret
|
||||||
let invoice = {
|
let invoice = {
|
||||||
|
@ -1639,7 +1684,7 @@ mod test {
|
||||||
invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into());
|
invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into());
|
||||||
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
|
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
|
||||||
}.unwrap();
|
}.unwrap();
|
||||||
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures));
|
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::NoPaymentSecret));
|
||||||
|
|
||||||
// Multiple payment secrets
|
// Multiple payment secrets
|
||||||
let invoice = {
|
let invoice = {
|
||||||
|
@ -1661,7 +1706,7 @@ mod test {
|
||||||
.current_timestamp();
|
.current_timestamp();
|
||||||
|
|
||||||
let invoice = builder.clone()
|
let invoice = builder.clone()
|
||||||
.amount_pico_btc(15000)
|
.amount_milli_satoshis(1500)
|
||||||
.build_raw()
|
.build_raw()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1670,7 +1715,7 @@ mod test {
|
||||||
|
|
||||||
|
|
||||||
let invoice = builder.clone()
|
let invoice = builder.clone()
|
||||||
.amount_pico_btc(1500)
|
.amount_milli_satoshis(150)
|
||||||
.build_raw()
|
.build_raw()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1725,6 +1770,7 @@ mod test {
|
||||||
|
|
||||||
let sign_error_res = builder.clone()
|
let sign_error_res = builder.clone()
|
||||||
.description("Test".into())
|
.description("Test".into())
|
||||||
|
.payment_secret(PaymentSecret([0; 32]))
|
||||||
.try_build_signed(|_| {
|
.try_build_signed(|_| {
|
||||||
Err("ImaginaryError")
|
Err("ImaginaryError")
|
||||||
});
|
});
|
||||||
|
@ -1801,7 +1847,7 @@ mod test {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let builder = InvoiceBuilder::new(Currency::BitcoinTestnet)
|
let builder = InvoiceBuilder::new(Currency::BitcoinTestnet)
|
||||||
.amount_pico_btc(123)
|
.amount_milli_satoshis(123)
|
||||||
.timestamp(UNIX_EPOCH + Duration::from_secs(1234567))
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1234567))
|
||||||
.payee_pub_key(public_key.clone())
|
.payee_pub_key(public_key.clone())
|
||||||
.expiry_time(Duration::from_secs(54321))
|
.expiry_time(Duration::from_secs(54321))
|
||||||
|
@ -1821,7 +1867,7 @@ mod test {
|
||||||
assert!(invoice.check_signature().is_ok());
|
assert!(invoice.check_signature().is_ok());
|
||||||
assert_eq!(invoice.tagged_fields().count(), 10);
|
assert_eq!(invoice.tagged_fields().count(), 10);
|
||||||
|
|
||||||
assert_eq!(invoice.amount_pico_btc(), Some(123));
|
assert_eq!(invoice.amount_pico_btc(), Some(1230));
|
||||||
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
|
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||||
|
@ -1837,7 +1883,7 @@ mod test {
|
||||||
InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap()))
|
InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap()))
|
||||||
);
|
);
|
||||||
assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap());
|
assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap());
|
||||||
assert_eq!(invoice.payment_secret(), Some(&PaymentSecret([42; 32])));
|
assert_eq!(invoice.payment_secret(), &PaymentSecret([42; 32]));
|
||||||
assert_eq!(invoice.features(), Some(&InvoiceFeatures::known()));
|
assert_eq!(invoice.features(), Some(&InvoiceFeatures::known()));
|
||||||
|
|
||||||
let raw_invoice = builder.build_raw().unwrap();
|
let raw_invoice = builder.build_raw().unwrap();
|
||||||
|
@ -1853,6 +1899,7 @@ mod test {
|
||||||
let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
|
let signed_invoice = InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
.description("Test".into())
|
.description("Test".into())
|
||||||
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
|
.payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap())
|
||||||
|
.payment_secret(PaymentSecret([0; 32]))
|
||||||
.current_timestamp()
|
.current_timestamp()
|
||||||
.build_raw()
|
.build_raw()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
|
@ -68,7 +68,7 @@ where
|
||||||
.basic_mpp()
|
.basic_mpp()
|
||||||
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
|
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
|
||||||
if let Some(amt) = amt_msat {
|
if let Some(amt) = amt_msat {
|
||||||
invoice = invoice.amount_pico_btc(amt * 10);
|
invoice = invoice.amount_milli_satoshis(amt);
|
||||||
}
|
}
|
||||||
for hint in route_hints {
|
for hint in route_hints {
|
||||||
invoice = invoice.private_route(hint);
|
invoice = invoice.private_route(hint);
|
||||||
|
@ -132,7 +132,7 @@ mod test {
|
||||||
let payment_event = {
|
let payment_event = {
|
||||||
let mut payment_hash = PaymentHash([0; 32]);
|
let mut payment_hash = PaymentHash([0; 32]);
|
||||||
payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]);
|
payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]);
|
||||||
nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().unwrap().clone())).unwrap();
|
nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().clone())).unwrap();
|
||||||
let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap();
|
let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap();
|
||||||
assert_eq!(added_monitors.len(), 1);
|
assert_eq!(added_monitors.len(), 1);
|
||||||
added_monitors.clear();
|
added_monitors.clear();
|
||||||
|
|
|
@ -1,27 +1,30 @@
|
||||||
|
extern crate bech32;
|
||||||
extern crate bitcoin_hashes;
|
extern crate bitcoin_hashes;
|
||||||
extern crate lightning;
|
extern crate lightning;
|
||||||
extern crate lightning_invoice;
|
extern crate lightning_invoice;
|
||||||
extern crate secp256k1;
|
extern crate secp256k1;
|
||||||
|
extern crate hex;
|
||||||
|
|
||||||
use bitcoin_hashes::hex::FromHex;
|
use bitcoin_hashes::hex::FromHex;
|
||||||
use bitcoin_hashes::sha256;
|
use bitcoin_hashes::{sha256, Hash};
|
||||||
|
use bech32::u5;
|
||||||
use lightning::ln::PaymentSecret;
|
use lightning::ln::PaymentSecret;
|
||||||
|
use lightning::routing::router::{RouteHint, RouteHintHop};
|
||||||
|
use lightning::routing::network_graph::RoutingFees;
|
||||||
use lightning_invoice::*;
|
use lightning_invoice::*;
|
||||||
use secp256k1::Secp256k1;
|
use secp256k1::PublicKey;
|
||||||
use secp256k1::key::SecretKey;
|
|
||||||
use secp256k1::recovery::{RecoverableSignature, RecoveryId};
|
use secp256k1::recovery::{RecoverableSignature, RecoveryId};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::time::{Duration, UNIX_EPOCH};
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
// TODO: add more of the examples from BOLT11 and generate ones causing SemanticErrors
|
fn get_test_tuples() -> Vec<(String, SignedRawInvoice, bool, bool)> {
|
||||||
|
|
||||||
fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
|
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmw\
|
"lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql".to_owned(),
|
||||||
wd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9\
|
|
||||||
ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w".to_owned(),
|
|
||||||
InvoiceBuilder::new(Currency::Bitcoin)
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
.payment_hash(sha256::Hash::from_hex(
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
"0001020304050607080900010203040506070809000102030405060708090102"
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
).unwrap())
|
).unwrap())
|
||||||
|
@ -30,26 +33,19 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.sign(|_| {
|
.sign(|_| {
|
||||||
RecoverableSignature::from_compact(
|
RecoverableSignature::from_compact(
|
||||||
& [
|
&hex::decode("8d3ce9e28357337f62da0162d9454df827f83cfe499aeb1c1db349d4d81127425e434ca29929406c23bba1ae8ac6ca32880b38d4bf6ff874024cac34ba9625f1").unwrap(),
|
||||||
0x38u8, 0xec, 0x68, 0x91, 0x34, 0x5e, 0x20, 0x41, 0x45, 0xbe, 0x8a,
|
RecoveryId::from_i32(1).unwrap()
|
||||||
0x3a, 0x99, 0xde, 0x38, 0xe9, 0x8a, 0x39, 0xd6, 0xa5, 0x69, 0x43,
|
|
||||||
0x4e, 0x18, 0x45, 0xc8, 0xaf, 0x72, 0x05, 0xaf, 0xcf, 0xcc, 0x7f,
|
|
||||||
0x42, 0x5f, 0xcd, 0x14, 0x63, 0xe9, 0x3c, 0x32, 0x88, 0x1e, 0xad,
|
|
||||||
0x0d, 0x6e, 0x35, 0x6d, 0x46, 0x7e, 0xc8, 0xc0, 0x25, 0x53, 0xf9,
|
|
||||||
0xaa, 0xb1, 0x5e, 0x57, 0x38, 0xb1, 0x1f, 0x12, 0x7f
|
|
||||||
],
|
|
||||||
RecoveryId::from_i32(0).unwrap()
|
|
||||||
)
|
)
|
||||||
}).unwrap(),
|
}).unwrap(),
|
||||||
None
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3\
|
"lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh".to_owned(),
|
||||||
k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\
|
|
||||||
9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(),
|
|
||||||
InvoiceBuilder::new(Currency::Bitcoin)
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
.amount_pico_btc(2500000000)
|
.amount_milli_satoshis(250_000_000)
|
||||||
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
.payment_hash(sha256::Hash::from_hex(
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
"0001020304050607080900010203040506070809000102030405060708090102"
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
).unwrap())
|
).unwrap())
|
||||||
|
@ -59,93 +55,341 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.sign(|_| {
|
.sign(|_| {
|
||||||
RecoverableSignature::from_compact(
|
RecoverableSignature::from_compact(
|
||||||
& [
|
&hex::decode("e59e3ffbd3945e4334879158d31e89b076dff54f3fa7979ae79df2db9dcaf5896cbfe1a478b8d2307e92c88139464cb7e6ef26e414c4abe33337961ddc5e8ab1").unwrap(),
|
||||||
0xe8, 0x96, 0x39, 0xba, 0x68, 0x14, 0xe3, 0x66, 0x89, 0xd4, 0xb9, 0x1b,
|
|
||||||
0xf1, 0x25, 0xf1, 0x03, 0x51, 0xb5, 0x5d, 0xa0, 0x57, 0xb0, 0x06, 0x47,
|
|
||||||
0xa8, 0xda, 0xba, 0xeb, 0x8a, 0x90, 0xc9, 0x5f, 0x16, 0x0f, 0x9d, 0x5a,
|
|
||||||
0x6e, 0x0f, 0x79, 0xd1, 0xfc, 0x2b, 0x96, 0x42, 0x38, 0xb9, 0x44, 0xe2,
|
|
||||||
0xfa, 0x4a, 0xa6, 0x77, 0xc6, 0xf0, 0x20, 0xd4, 0x66, 0x47, 0x2a, 0xb8,
|
|
||||||
0x42, 0xbd, 0x75, 0x0e
|
|
||||||
],
|
|
||||||
RecoveryId::from_i32(1).unwrap()
|
RecoveryId::from_i32(1).unwrap()
|
||||||
)
|
)
|
||||||
}).unwrap(),
|
}).unwrap(),
|
||||||
None
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qq\
|
"lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr".to_owned(),
|
||||||
dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\
|
|
||||||
hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(),
|
|
||||||
InvoiceBuilder::new(Currency::Bitcoin)
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
.amount_pico_btc(20000000000)
|
.amount_milli_satoshis(250_000_000)
|
||||||
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
.payment_hash(sha256::Hash::from_hex(
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
"0001020304050607080900010203040506070809000102030405060708090102"
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
).unwrap())
|
).unwrap())
|
||||||
.description_hash(sha256::Hash::from_hex(
|
.description("ナンセンス 1杯".to_owned())
|
||||||
"3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1"
|
.expiry_time(Duration::from_secs(60))
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("bae41ef385e0fc972977c7ea42b12cbd76577d2412919da8a8a22f9577b6507710c0e96dd78c821dea16453037f717f44aa7e3d196ebb18fbb97307dcb7336c3").unwrap(),
|
||||||
|
RecoveryId::from_i32(1).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_000_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
).unwrap())
|
).unwrap())
|
||||||
.build_raw()
|
.build_raw()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.sign(|_| {
|
.sign(|_| {
|
||||||
RecoverableSignature::from_compact(
|
RecoverableSignature::from_compact(
|
||||||
& [
|
&hex::decode("f67a5f696648fa4fb102e1a07b230e54722f8e024cee71e80b4847ac191da3fb2d2cdb28cc32344d7e9a9cf5c9b6a0ee0582ae46e9938b9c81e344a4dbb5289d").unwrap(),
|
||||||
0xc6, 0x34, 0x86, 0xe8, 0x1f, 0x8c, 0x87, 0x8a, 0x10, 0x5b, 0xc9, 0xd9,
|
RecoveryId::from_i32(1).unwrap()
|
||||||
0x59, 0xaf, 0x19, 0x73, 0x85, 0x4c, 0x4d, 0xc5, 0x52, 0xc4, 0xf0, 0xe0,
|
|
||||||
0xe0, 0xc7, 0x38, 0x96, 0x03, 0xd6, 0xbd, 0xc6, 0x77, 0x07, 0xbf, 0x6b,
|
|
||||||
0xe9, 0x92, 0xa8, 0xce, 0x7b, 0xf5, 0x00, 0x16, 0xbb, 0x41, 0xd8, 0xa9,
|
|
||||||
0xb5, 0x35, 0x86, 0x52, 0xc4, 0x96, 0x04, 0x45, 0xa1, 0x70, 0xd0, 0x49,
|
|
||||||
0xce, 0xd4, 0x55, 0x8c
|
|
||||||
],
|
|
||||||
RecoveryId::from_i32(0).unwrap()
|
|
||||||
)
|
)
|
||||||
}).unwrap(),
|
}).unwrap(),
|
||||||
None
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp59g4z52329g4z52329g4z52329g4z52329g4z52329g4z52329g4q9qrsgqzfhag3vsafx4e5qssalvw4rn0phsvpp3e5h2xxyk9l8fxsutvndx9t840dqvdrlu2gqmk0q8apqrgnjy9amc07hmjl9e9yzqjks5w2gqgjnyms".to_owned(),
|
"lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8".to_owned(),
|
||||||
InvoiceBuilder::new(Currency::Bitcoin)
|
InvoiceBuilder::new(Currency::BitcoinTestnet)
|
||||||
|
.amount_milli_satoshis(2_000_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
.payment_hash(sha256::Hash::from_hex(
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
"0001020304050607080900010203040506070809000102030405060708090102"
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
).unwrap())
|
).unwrap())
|
||||||
.description("coffee beans".to_string())
|
.fallback(Fallback::PubKeyHash([49, 114, 181, 101, 79, 102, 131, 200, 251, 20, 105, 89, 211, 71, 206, 48, 60, 174, 76, 167]))
|
||||||
.amount_pico_btc(20000000000)
|
|
||||||
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
|
||||||
.payment_secret(PaymentSecret([42; 32]))
|
|
||||||
.build_raw()
|
.build_raw()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.sign::<_, ()>(|msg_hash| {
|
.sign(|_| {
|
||||||
let privkey = SecretKey::from_slice(&[41; 32]).unwrap();
|
RecoverableSignature::from_compact(
|
||||||
let secp_ctx = Secp256k1::new();
|
&hex::decode("6ca95a74dc32e69ced6175b15a5cc56a92bf19f5dace0f134b7d94d464b9f5cf6090a18d48b243f289394d17bdf89466d8e6b37df5981f696bc3dd5986e1bee1").unwrap(),
|
||||||
Ok(secp_ctx.sign_recoverable(msg_hash, &privkey))
|
RecoveryId::from_i32(1).unwrap()
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
None
|
|
||||||
)
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_000_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.fallback(Fallback::PubKeyHash([4, 182, 31, 125, 193, 234, 13, 201, 148, 36, 70, 76, 196, 6, 77, 197, 100, 217, 30, 137]))
|
||||||
|
.private_route(RouteHint(vec![RouteHintHop {
|
||||||
|
src_node_id: PublicKey::from_slice(&hex::decode(
|
||||||
|
"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
|
||||||
|
).unwrap()).unwrap(),
|
||||||
|
short_channel_id: (66051 << 40) | (263430 << 16) | 1800,
|
||||||
|
fees: RoutingFees { base_msat: 1, proportional_millionths: 20 },
|
||||||
|
cltv_expiry_delta: 3,
|
||||||
|
htlc_maximum_msat: None, htlc_minimum_msat: None,
|
||||||
|
}, RouteHintHop {
|
||||||
|
src_node_id: PublicKey::from_slice(&hex::decode(
|
||||||
|
"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"
|
||||||
|
).unwrap()).unwrap(),
|
||||||
|
short_channel_id: (197637 << 40) | (395016 << 16) | 2314,
|
||||||
|
fees: RoutingFees { base_msat: 2, proportional_millionths: 30 },
|
||||||
|
cltv_expiry_delta: 4,
|
||||||
|
htlc_maximum_msat: None, htlc_minimum_msat: None,
|
||||||
|
}]))
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("6a6586db4e8f6d40e3a5bb92e4df5110c627e9ce493af237e20a046b4e86ea200178c59564ecf892f33a9558bf041b6ad2cb8292d7a6c351fbb7f2ae2d16b54e").unwrap(),
|
||||||
|
RecoveryId::from_i32(0).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_000_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.fallback(Fallback::ScriptHash([143, 85, 86, 59, 154, 25, 243, 33, 194, 17, 233, 185, 243, 140, 223, 104, 110, 160, 120, 69]))
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("16810439d1a9bfd5a65acc61340dc92448bb2d456a80b58ce012b73cb5202438020500c9ab7ef5573a4d174c811f669885ae27f895bb3a3be52c243589f87518").unwrap(),
|
||||||
|
RecoveryId::from_i32(1).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_000_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.fallback(Fallback::SegWitProgram { version: u5::try_from_u8(0).unwrap(),
|
||||||
|
program: vec![117, 30, 118, 232, 25, 145, 150, 212, 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214]
|
||||||
|
})
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("5a8bd7b97c1cc9055ee60cf2356621f8752248e037a953886a1782b44a58f5ff2d94e6bc89b7b514541a3603bb33722b6c08aa1a3639d34becc549a99fea6eae").unwrap(),
|
||||||
|
RecoveryId::from_i32(0).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_000_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.description_hash(sha256::Hash::hash(b"One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.fallback(Fallback::SegWitProgram { version: u5::try_from_u8(0).unwrap(),
|
||||||
|
program: vec![24, 99, 20, 60, 20, 197, 22, 104, 4, 189, 25, 32, 51, 86, 218, 19, 108, 152, 86, 120, 205, 77, 39, 161, 184, 198, 50, 150, 4, 144, 50, 98]
|
||||||
|
})
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("2b3ec248f80301a421817369194f012cdd8af8df1c279981420f9e901e20fa3309d791e11355e609b59ce4a220852a0cd55ab862b1785a83b206c90fa74d01c8").unwrap(),
|
||||||
|
RecoveryId::from_i32(1).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(967878534)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1572468703))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f"
|
||||||
|
).unwrap())
|
||||||
|
.expiry_time(Duration::from_secs(604800))
|
||||||
|
.min_final_cltv_expiry(10)
|
||||||
|
.description("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items".to_owned())
|
||||||
|
.private_route(RouteHint(vec![RouteHintHop {
|
||||||
|
src_node_id: PublicKey::from_slice(&hex::decode(
|
||||||
|
"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"
|
||||||
|
).unwrap()).unwrap(),
|
||||||
|
short_channel_id: (589390 << 40) | (3312 << 16) | 1,
|
||||||
|
fees: RoutingFees { base_msat: 1000, proportional_millionths: 2500 },
|
||||||
|
cltv_expiry_delta: 40,
|
||||||
|
htlc_maximum_msat: None, htlc_minimum_msat: None,
|
||||||
|
}]))
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("1b1160cf6186b55722c1ac7ea502086baaccaabdc76b326e666b7f309d972b15069bfca11cd365304b36f48230cc12f3f13a017aab65f7c165a169df32282a58").unwrap(),
|
||||||
|
RecoveryId::from_i32(1).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
false, // Same features as set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_500_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.description("coffee beans".to_owned())
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("5755469bf4b8e6b6ae7a1308d5f9bad5c82812e0855cd24fac242aa323fa820c5c551ede4faeabcb7fb6d5a464ad0e35c86f615589ee0e0c250c216a662198c1").unwrap(),
|
||||||
|
RecoveryId::from_i32(1).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
true, // Different features than set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"LNBC25M1PVJLUEZPP5QQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQQQSYQCYQ5RQWZQFQYPQDQ5VDHKVEN9V5SXYETPDEESSP5ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYG3ZYGS9Q5SQQQQQQQQQQQQQQQQSGQ2A25DXL5HRNTDTN6ZVYDT7D66HYZSYHQS4WDYNAVYS42XGL6SGX9C4G7ME86A27T07MDTFRY458RTJR0V92CNMSWPSJSCGT2VCSE3SGPZ3UAPA".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_500_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.description("coffee beans".to_owned())
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("5755469bf4b8e6b6ae7a1308d5f9bad5c82812e0855cd24fac242aa323fa820c5c551ede4faeabcb7fb6d5a464ad0e35c86f615589ee0e0c250c216a662198c1").unwrap(),
|
||||||
|
RecoveryId::from_i32(1).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
true, // Different features than set in InvoiceBuilder
|
||||||
|
false, // No unknown fields
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x".to_owned(),
|
||||||
|
InvoiceBuilder::new(Currency::Bitcoin)
|
||||||
|
.amount_milli_satoshis(2_500_000_000)
|
||||||
|
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
|
||||||
|
.payment_secret(PaymentSecret([0x11; 32]))
|
||||||
|
.payment_hash(sha256::Hash::from_hex(
|
||||||
|
"0001020304050607080900010203040506070809000102030405060708090102"
|
||||||
|
).unwrap())
|
||||||
|
.description("coffee beans".to_owned())
|
||||||
|
.build_raw()
|
||||||
|
.unwrap()
|
||||||
|
.sign(|_| {
|
||||||
|
RecoverableSignature::from_compact(
|
||||||
|
&hex::decode("150a5252308f25bc2641a186de87470189bb003774326beee33b9a2a720d1584386631c5dda6fc3195f97464bfc93d2574868eadd767d6da1078329c4349c837").unwrap(),
|
||||||
|
RecoveryId::from_i32(0).unwrap()
|
||||||
|
)
|
||||||
|
}).unwrap(),
|
||||||
|
true, // Different features than set in InvoiceBuilder
|
||||||
|
true, // Some unknown fields
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize() {
|
fn invoice_deserialize() {
|
||||||
for (serialized, deserialized, _) in get_test_tuples() {
|
for (serialized, deserialized, ignore_feature_diff, ignore_unknown_fields) in get_test_tuples() {
|
||||||
assert_eq!(deserialized.to_string(), serialized);
|
eprintln!("Testing invoice {}...", serialized);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize() {
|
|
||||||
for (serialized, deserialized, maybe_error) in get_test_tuples() {
|
|
||||||
let parsed = serialized.parse::<SignedRawInvoice>().unwrap();
|
let parsed = serialized.parse::<SignedRawInvoice>().unwrap();
|
||||||
|
|
||||||
assert_eq!(parsed, deserialized);
|
let (parsed_invoice, _, parsed_sig) = parsed.into_parts();
|
||||||
|
let (deserialized_invoice, _, deserialized_sig) = deserialized.into_parts();
|
||||||
|
|
||||||
let validated = Invoice::from_signed(parsed);
|
assert_eq!(deserialized_sig, parsed_sig);
|
||||||
|
assert_eq!(deserialized_invoice.hrp, parsed_invoice.hrp);
|
||||||
|
assert_eq!(deserialized_invoice.data.timestamp, parsed_invoice.data.timestamp);
|
||||||
|
|
||||||
if let Some(error) = maybe_error {
|
let mut deserialized_hunks: HashSet<_> = deserialized_invoice.data.tagged_fields.iter().collect();
|
||||||
assert_eq!(Err(error), validated);
|
let mut parsed_hunks: HashSet<_> = parsed_invoice.data.tagged_fields.iter().collect();
|
||||||
} else {
|
if ignore_feature_diff {
|
||||||
assert!(validated.is_ok());
|
deserialized_hunks.retain(|h|
|
||||||
|
if let RawTaggedField::KnownSemantics(TaggedField::Features(_)) = h { false } else { true });
|
||||||
|
parsed_hunks.retain(|h|
|
||||||
|
if let RawTaggedField::KnownSemantics(TaggedField::Features(_)) = h { false } else { true });
|
||||||
}
|
}
|
||||||
|
if ignore_unknown_fields {
|
||||||
|
parsed_hunks.retain(|h|
|
||||||
|
if let RawTaggedField::UnknownSemantics(_) = h { false } else { true });
|
||||||
|
}
|
||||||
|
assert_eq!(deserialized_hunks, parsed_hunks);
|
||||||
|
|
||||||
|
Invoice::from_signed(serialized.parse::<SignedRawInvoice>().unwrap()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bolt_invalid_invoices() {
|
||||||
|
// Tests the BOLT 11 invalid invoice test vectors
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372"
|
||||||
|
), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidFeatures)));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt"
|
||||||
|
), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::InvalidChecksum))));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny"
|
||||||
|
), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MissingSeparator))));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny"
|
||||||
|
), Err(ParseOrSemanticError::ParseError(ParseError::Bech32Error(bech32::Error::MixedCase))));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2"
|
||||||
|
), Err(ParseOrSemanticError::SemanticError(SemanticError::InvalidSignature)));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh"
|
||||||
|
), Err(ParseOrSemanticError::ParseError(ParseError::TooShortDataPart)));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqrrzc4cvfue4zp3hggxp47ag7xnrlr8vgcmkjxk3j5jqethnumgkpqp23z9jclu3v0a7e0aruz366e9wqdykw6dxhdzcjjhldxq0w6wgqcnu43j"
|
||||||
|
), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix)));
|
||||||
|
assert_eq!(Invoice::from_str(
|
||||||
|
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x"
|
||||||
|
), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount)));
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
use io;
|
use io;
|
||||||
use prelude::*;
|
use prelude::*;
|
||||||
use core::{cmp, fmt};
|
use core::{cmp, fmt};
|
||||||
|
use core::hash::{Hash, Hasher};
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
use bitcoin::bech32;
|
use bitcoin::bech32;
|
||||||
|
@ -362,6 +363,11 @@ impl<T: sealed::Context> Clone for Features<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl<T: sealed::Context> Hash for Features<T> {
|
||||||
|
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||||
|
self.flags.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
impl<T: sealed::Context> PartialEq for Features<T> {
|
impl<T: sealed::Context> PartialEq for Features<T> {
|
||||||
fn eq(&self, o: &Self) -> bool {
|
fn eq(&self, o: &Self) -> bool {
|
||||||
self.flags.eq(&o.flags)
|
self.flags.eq(&o.flags)
|
||||||
|
@ -548,7 +554,9 @@ impl<T: sealed::Context> Features<T> {
|
||||||
&self.flags
|
&self.flags
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn requires_unknown_bits(&self) -> bool {
|
/// Returns true if this `Features` object contains unknown feature flags which are set as
|
||||||
|
/// "required".
|
||||||
|
pub fn requires_unknown_bits(&self) -> bool {
|
||||||
// Bitwise AND-ing with all even bits set except for known features will select required
|
// Bitwise AND-ing with all even bits set except for known features will select required
|
||||||
// unknown features.
|
// unknown features.
|
||||||
let byte_count = T::KNOWN_FEATURE_MASK.len();
|
let byte_count = T::KNOWN_FEATURE_MASK.len();
|
||||||
|
|
|
@ -533,7 +533,7 @@ impl_writeable_tlv_based!(ChannelInfo, {
|
||||||
|
|
||||||
|
|
||||||
/// Fees for routing via a given channel or a node
|
/// Fees for routing via a given channel or a node
|
||||||
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
#[derive(Eq, PartialEq, Copy, Clone, Debug, Hash)]
|
||||||
pub struct RoutingFees {
|
pub struct RoutingFees {
|
||||||
/// Flat routing fee in satoshis
|
/// Flat routing fee in satoshis
|
||||||
pub base_msat: u32,
|
pub base_msat: u32,
|
||||||
|
|
|
@ -28,7 +28,7 @@ use core::cmp;
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
|
|
||||||
/// A hop in a route
|
/// A hop in a route
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct RouteHop {
|
pub struct RouteHop {
|
||||||
/// The node_id of the node at this hop.
|
/// The node_id of the node at this hop.
|
||||||
pub pubkey: PublicKey,
|
pub pubkey: PublicKey,
|
||||||
|
@ -60,7 +60,7 @@ impl_writeable_tlv_based!(RouteHop, {
|
||||||
|
|
||||||
/// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP,
|
/// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP,
|
||||||
/// it can take multiple paths. Each path is composed of one or more hops through the network.
|
/// it can take multiple paths. Each path is composed of one or more hops through the network.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct Route {
|
pub struct Route {
|
||||||
/// The list of routes taken for a single (potentially-)multi-part payment. The pubkey of the
|
/// The list of routes taken for a single (potentially-)multi-part payment. The pubkey of the
|
||||||
/// last RouteHop in each path must be the same.
|
/// last RouteHop in each path must be the same.
|
||||||
|
@ -108,11 +108,11 @@ impl Readable for Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of hops along a payment path terminating with a channel to the recipient.
|
/// A list of hops along a payment path terminating with a channel to the recipient.
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct RouteHint(pub Vec<RouteHintHop>);
|
pub struct RouteHint(pub Vec<RouteHintHop>);
|
||||||
|
|
||||||
/// A channel descriptor for a hop along a payment path.
|
/// A channel descriptor for a hop along a payment path.
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct RouteHintHop {
|
pub struct RouteHintHop {
|
||||||
/// The node_id of the non-target end of the route
|
/// The node_id of the non-target end of the route
|
||||||
pub src_node_id: PublicKey,
|
pub src_node_id: PublicKey,
|
||||||
|
|
Loading…
Add table
Reference in a new issue