Skip to content

Commit 3302977

Browse files
benthecarmanTheBlueMatt
authored andcommitted
Add some initial tests
Mostly tests copied from https://github.com/MutinyWallet/bitcoin-waila/blob/master/waila/src/lib.rs#L507 Tests modified somewhat by: Matt Corallo <[email protected]>
1 parent c608553 commit 3302977

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed

src/lib.rs

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ pub const MAX_AMOUNT_DIFFERENCE: Amount = Amount::from_sats(100);
122122

123123
/// An error when parsing payment instructions into [`PaymentInstructions`].
124124
#[derive(Debug)]
125+
#[cfg_attr(test, derive(PartialEq))]
125126
pub enum ParseError {
126127
/// An invalid lightning BOLT 11 invoice was encountered
127128
InvalidBolt11(ParseOrSemanticError),
@@ -637,6 +638,16 @@ pub trait HrnResolver {
637638
fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a>;
638639
}
639640

641+
/// An HRN "resolver" that never succeeds at resolving.
642+
#[derive(Clone, Copy)]
643+
pub struct DummyHrnResolver;
644+
645+
impl HrnResolver for DummyHrnResolver {
646+
fn resolve_hrn<'a>(&'a self, _hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> {
647+
Box::pin(async { Err("Human Readable Name resolution not supported") })
648+
}
649+
}
650+
640651
impl PaymentInstructions {
641652
/// Resolves a string into [`PaymentInstructions`].
642653
pub async fn parse<H: HrnResolver>(
@@ -654,3 +665,275 @@ impl PaymentInstructions {
654665
}
655666
}
656667
}
668+
669+
#[cfg(test)]
670+
mod tests {
671+
use alloc::format;
672+
use alloc::str::FromStr;
673+
use alloc::string::ToString;
674+
675+
use super::*;
676+
677+
const SAMPLE_INVOICE_WITH_FALLBACK: &str = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0";
678+
const SAMPLE_INVOICE: &str = "lnbc20m1pn7qa2ndqqnp4q0d3p2sfluzdx45tqcsh2pu5qc7lgq0xs578ngs6s0s68ua4h7cvspp5kwzshmne5zw3lnfqdk8cv26mg9ndjapqzhcxn2wtn9d6ew5e2jfqsp5h3u5f0l522vs488h6n8zm5ca2lkpva532fnl2kp4wnvsuq445erq9qyysgqcqpcxqppz4395v2sjh3t5pzckgeelk9qf0z3fm9jzxtjqpqygayt4xyy7tpjvq5pe7f6727du2mg3t2tfe0cd53de2027ff7es7smtew8xx5x2spwuvkdz";
679+
const SAMPLE_OFFER: &str = "lno1qgs0v8hw8d368q9yw7sx8tejk2aujlyll8cp7tzzyh5h8xyppqqqqqqgqvqcdgq2qenxzatrv46pvggrv64u366d5c0rr2xjc3fq6vw2hh6ce3f9p7z4v4ee0u7avfynjw9q";
680+
const SAMPLE_BIP21: &str = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz";
681+
682+
const SAMPLE_BIP21_WITH_INVOICE: &str = "bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=LNBC10U1P3PJ257PP5YZTKWJCZ5FTL5LAXKAV23ZMZEKAW37ZK6KMV80PK4XAEV5QHTZ7QDPDWD3XGER9WD5KWM36YPRX7U3QD36KUCMGYP282ETNV3SHJCQZPGXQYZ5VQSP5USYC4LK9CHSFP53KVCNVQ456GANH60D89REYKDNGSMTJ6YW3NHVQ9QYYSSQJCEWM5CJWZ4A6RFJX77C490YCED6PEMK0UPKXHY89CMM7SCT66K8GNEANWYKZGDRWRFJE69H9U5U0W57RRCSYSAS7GADWMZXC8C6T0SPJAZUP6";
683+
#[cfg(not(feature = "std"))]
684+
const SAMPLE_BIP21_WITH_INVOICE_ADDR: &str = "bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u";
685+
#[cfg(not(feature = "std"))]
686+
const SAMPLE_BIP21_WITH_INVOICE_INVOICE: &str = "lnbc10u1p3pj257pp5yztkwjcz5ftl5laxkav23zmzekaw37zk6kmv80pk4xaev5qhtz7qdpdwd3xger9wd5kwm36yprx7u3qd36kucmgyp282etnv3shjcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqjcewm5cjwz4a6rfjx77c490yced6pemk0upkxhy89cmm7sct66k8gneanwykzgdrwrfje69h9u5u0w57rrcsysas7gadwmzxc8c6t0spjazup6";
687+
688+
const SAMPLE_BIP21_WITH_INVOICE_AND_LABEL: &str = "bitcoin:tb1p0vztr8q25czuka5u4ta5pqu0h8dxkf72mam89cpg4tg40fm8wgmqp3gv99?amount=0.000001&label=yooo&lightning=lntbs1u1pjrww6fdq809hk7mcnp4qvwggxr0fsueyrcer4x075walsv93vqvn3vlg9etesx287x6ddy4xpp5a3drwdx2fmkkgmuenpvmynnl7uf09jmgvtlg86ckkvgn99ajqgtssp5gr3aghgjxlwshnqwqn39c2cz5hw4cnsnzxdjn7kywl40rru4mjdq9qyysgqcqpcxqrpwurzjqfgtsj42x8an5zujpxvfhp9ngwm7u5lu8lvzfucjhex4pq8ysj5q2qqqqyqqv9cqqsqqqqlgqqqqqqqqfqzgl9zq04nzpxyvdr8vj3h98gvnj3luanj2cxcra0q2th4xjsxmtj8k3582l67xq9ffz5586f3nm5ax58xaqjg6rjcj2vzvx2q39v9eqpn0wx54";
689+
690+
#[tokio::test]
691+
async fn parse_address() {
692+
let addr_str = "1andreas3batLhQa2FawWjeyjCqyBzypd";
693+
let parsed =
694+
PaymentInstructions::parse(&addr_str, Network::Bitcoin, DummyHrnResolver, false)
695+
.await
696+
.unwrap();
697+
698+
assert_eq!(parsed.methods.len(), 1);
699+
assert_eq!(parsed.methods[0].amount(), None);
700+
assert_eq!(parsed.recipient_description, None);
701+
if let PaymentMethod::OnChain { amount, address } = &parsed.methods[0] {
702+
assert_eq!(*amount, None);
703+
assert_eq!(*address, Address::from_str(addr_str).unwrap().assume_checked());
704+
} else {
705+
panic!("Wrong method");
706+
}
707+
}
708+
709+
// Test a handful of ways a lightning invoice might be communicated
710+
async fn check_ln_invoice(inv: &str) -> Result<PaymentInstructions, ParseError> {
711+
assert!(inv.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10)), "{}", inv);
712+
let resolver = DummyHrnResolver;
713+
let raw = PaymentInstructions::parse(inv, Network::Bitcoin, resolver, false).await;
714+
715+
let ln_uri = format!("lightning:{}", inv);
716+
let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
717+
assert_eq!(raw, uri);
718+
719+
let ln_uri = format!("LIGHTNING:{}", inv);
720+
let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
721+
assert_eq!(raw, uri);
722+
723+
let ln_uri = ln_uri.to_uppercase();
724+
let uri = PaymentInstructions::parse(&ln_uri, Network::Bitcoin, resolver, false).await;
725+
assert_eq!(raw, uri);
726+
727+
let btc_uri = format!("bitcoin:?lightning={}", inv);
728+
let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
729+
assert_eq!(raw, uri);
730+
731+
let btc_uri = btc_uri.to_uppercase();
732+
let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
733+
assert_eq!(raw, uri);
734+
735+
let btc_uri = format!("bitcoin:?req-lightning={}", inv);
736+
let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
737+
assert_eq!(raw, uri);
738+
739+
let btc_uri = btc_uri.to_uppercase();
740+
let uri = PaymentInstructions::parse(&btc_uri, Network::Bitcoin, resolver, false).await;
741+
assert_eq!(raw, uri);
742+
743+
raw
744+
}
745+
746+
#[cfg(not(feature = "std"))]
747+
#[tokio::test]
748+
async fn parse_invoice() {
749+
let invoice = Bolt11Invoice::from_str(SAMPLE_INVOICE).unwrap();
750+
let parsed = check_ln_invoice(SAMPLE_INVOICE).await.unwrap();
751+
752+
assert_eq!(parsed.methods.len(), 1);
753+
assert_eq!(
754+
parsed.methods[0].amount(),
755+
invoice.amount_milli_satoshis().map(Amount::from_milli_sats),
756+
);
757+
assert_eq!(parsed.recipient_description, Some(String::new()));
758+
assert!(matches!(parsed.methods[0].clone(), PaymentMethod::LightningBolt11(_)));
759+
}
760+
761+
#[cfg(feature = "std")]
762+
#[tokio::test]
763+
async fn parse_invoice() {
764+
assert_eq!(check_ln_invoice(SAMPLE_INVOICE).await, Err(ParseError::InstructionsExpired));
765+
}
766+
767+
#[cfg(not(feature = "std"))]
768+
#[tokio::test]
769+
async fn parse_invoice_with_fallback() {
770+
let invoice = Bolt11Invoice::from_str(SAMPLE_INVOICE_WITH_FALLBACK).unwrap();
771+
let parsed = check_ln_invoice(SAMPLE_INVOICE_WITH_FALLBACK).await.unwrap();
772+
773+
assert_eq!(parsed.methods.len(), 2);
774+
assert_eq!(
775+
parsed.methods[0].amount(),
776+
invoice.amount_milli_satoshis().map(Amount::from_milli_sats),
777+
);
778+
assert_eq!(
779+
parsed.methods[1].amount(),
780+
invoice.amount_milli_satoshis().map(Amount::from_milli_sats),
781+
);
782+
783+
assert_eq!(parsed.recipient_description, None); // no description for a description hash
784+
let is_bolt11 = |meth: &&PaymentMethod| matches!(meth, &&PaymentMethod::LightningBolt11(_));
785+
assert_eq!(parsed.methods.iter().filter(is_bolt11).count(), 1);
786+
let is_onchain = |meth: &&PaymentMethod| matches!(meth, &&PaymentMethod::OnChain { .. });
787+
assert_eq!(parsed.methods.iter().filter(is_onchain).count(), 1);
788+
}
789+
790+
#[cfg(feature = "std")]
791+
#[tokio::test]
792+
async fn parse_invoice_with_fallback() {
793+
assert_eq!(
794+
check_ln_invoice(SAMPLE_INVOICE_WITH_FALLBACK).await,
795+
Err(ParseError::InstructionsExpired),
796+
);
797+
}
798+
799+
// Test a handful of ways a lightning offer might be communicated
800+
async fn check_ln_offer(offer: &str) -> Result<PaymentInstructions, ParseError> {
801+
assert!(offer.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10)), "{}", offer);
802+
let resolver = DummyHrnResolver;
803+
let raw = PaymentInstructions::parse(offer, Network::Signet, resolver, false).await;
804+
805+
let btc_uri = format!("bitcoin:?lno={}", offer);
806+
let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
807+
assert_eq!(raw, uri);
808+
809+
let btc_uri = btc_uri.to_uppercase();
810+
let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
811+
assert_eq!(raw, uri);
812+
813+
let btc_uri = format!("bitcoin:?req-lno={}", offer);
814+
let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
815+
assert_eq!(raw, uri);
816+
817+
let btc_uri = btc_uri.to_uppercase();
818+
let uri = PaymentInstructions::parse(&btc_uri, Network::Signet, resolver, false).await;
819+
assert_eq!(raw, uri);
820+
821+
raw
822+
}
823+
824+
#[tokio::test]
825+
async fn parse_offer() {
826+
let offer = Offer::from_str(SAMPLE_OFFER).unwrap();
827+
let amt_msats = match offer.amount() {
828+
None => None,
829+
Some(offer::Amount::Bitcoin { amount_msats }) => Some(amount_msats),
830+
Some(offer::Amount::Currency { .. }) => panic!(),
831+
};
832+
let parsed = check_ln_offer(SAMPLE_OFFER).await.unwrap();
833+
834+
assert_eq!(parsed.methods.len(), 1);
835+
assert_eq!(parsed.methods[0].amount(), amt_msats.map(Amount::from_milli_sats));
836+
assert_eq!(parsed.recipient_description, Some("faucet".to_string()));
837+
assert!(matches!(parsed.methods[0].clone(), PaymentMethod::LightningBolt12(_)));
838+
}
839+
840+
#[tokio::test]
841+
async fn parse_bip_21() {
842+
let parsed =
843+
PaymentInstructions::parse(SAMPLE_BIP21, Network::Bitcoin, DummyHrnResolver, false)
844+
.await
845+
.unwrap();
846+
847+
assert_eq!(parsed.methods.len(), 1);
848+
assert_eq!(parsed.methods[0].amount(), Some(Amount::from_sats(5_000_000_000)));
849+
assert_eq!(parsed.recipient_description, None);
850+
assert!(matches!(
851+
parsed.methods[0].clone(),
852+
PaymentMethod::OnChain { amount: Some(_), .. }
853+
));
854+
}
855+
856+
#[cfg(not(feature = "std"))]
857+
#[tokio::test]
858+
async fn parse_bip_21_with_invoice() {
859+
let parsed = PaymentInstructions::parse(
860+
SAMPLE_BIP21_WITH_INVOICE,
861+
Network::Bitcoin,
862+
DummyHrnResolver,
863+
false,
864+
)
865+
.await
866+
.unwrap();
867+
868+
assert_eq!(parsed.methods.len(), 2);
869+
assert_eq!(parsed.onchain_payment_amount(), Some(Amount::from_milli_sats(1_000_000)));
870+
assert_eq!(parsed.ln_payment_amount(), Some(Amount::from_milli_sats(1_000_000)));
871+
assert_eq!(parsed.methods[0].amount(), Some(Amount::from_milli_sats(1_000_000)));
872+
assert_eq!(parsed.recipient_description, Some("sbddesign: For lunch Tuesday".to_string()));
873+
if let PaymentMethod::OnChain { amount, address } = &parsed.methods[0] {
874+
assert_eq!(*amount, Some(Amount::from_milli_sats(1_000_000)));
875+
assert_eq!(address.to_string(), SAMPLE_BIP21_WITH_INVOICE_ADDR);
876+
} else {
877+
panic!("Missing on-chain (or order changed)");
878+
}
879+
if let PaymentMethod::LightningBolt11(inv) = &parsed.methods[1] {
880+
assert_eq!(inv.to_string(), SAMPLE_BIP21_WITH_INVOICE_INVOICE);
881+
} else {
882+
panic!("Missing invoice (or order changed)");
883+
}
884+
}
885+
886+
#[cfg(feature = "std")]
887+
#[tokio::test]
888+
async fn parse_bip_21_with_invoice() {
889+
assert_eq!(
890+
PaymentInstructions::parse(
891+
SAMPLE_BIP21_WITH_INVOICE,
892+
Network::Bitcoin,
893+
DummyHrnResolver,
894+
false,
895+
)
896+
.await,
897+
Err(ParseError::InstructionsExpired),
898+
);
899+
}
900+
901+
#[cfg(not(feature = "std"))]
902+
#[tokio::test]
903+
async fn parse_bip_21_with_invoice_with_label() {
904+
let parsed = PaymentInstructions::parse(
905+
SAMPLE_BIP21_WITH_INVOICE_AND_LABEL,
906+
Network::Signet,
907+
DummyHrnResolver,
908+
false,
909+
)
910+
.await
911+
.unwrap();
912+
913+
assert_eq!(parsed.methods.len(), 2);
914+
assert_eq!(parsed.onchain_payment_amount(), Some(Amount::from_milli_sats(100_000)));
915+
assert_eq!(parsed.ln_payment_amount(), Some(Amount::from_milli_sats(100_000)));
916+
assert_eq!(parsed.methods[0].amount(), Some(Amount::from_milli_sats(100_000)));
917+
assert_eq!(parsed.recipient_description, Some("yooo".to_string()));
918+
assert!(matches!(
919+
parsed.methods[0].clone(),
920+
PaymentMethod::OnChain { amount: Some(_), .. }
921+
));
922+
assert!(matches!(parsed.methods[1].clone(), PaymentMethod::LightningBolt11(_)));
923+
}
924+
925+
#[cfg(feature = "std")]
926+
#[tokio::test]
927+
async fn parse_bip_21_with_invoice_with_label() {
928+
assert_eq!(
929+
PaymentInstructions::parse(
930+
SAMPLE_BIP21_WITH_INVOICE_AND_LABEL,
931+
Network::Signet,
932+
DummyHrnResolver,
933+
false,
934+
)
935+
.await,
936+
Err(ParseError::InstructionsExpired),
937+
);
938+
}
939+
}

0 commit comments

Comments
 (0)