Skip to content

Commit a9e159e

Browse files
committed
feat: uppercase bip321 string
1 parent 73500ef commit a9e159e

2 files changed

Lines changed: 87 additions & 44 deletions

File tree

crates/cdk-ffi/src/bip321.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ mod tests {
106106
const TEST_BOLT11: &str = "lnbc100n1ptest";
107107

108108
fn assert_uri_prefix(uri: &str) {
109-
assert!(uri.starts_with("bitcoin:?"));
109+
assert!(uri.starts_with("BITCOIN:?"));
110110
}
111111

112112
#[test]
@@ -118,27 +118,27 @@ mod tests {
118118
);
119119

120120
assert_uri_prefix(&uri);
121-
assert!(uri.contains(&format!("creq={TEST_CREQ}")));
122-
assert!(uri.contains(&format!("lightning={TEST_BOLT11}")));
121+
assert!(uri.contains(&format!("CREQ={TEST_CREQ}")));
122+
assert!(uri.contains(&format!("LIGHTNING={}", TEST_BOLT11.to_ascii_uppercase())));
123123
}
124124

125125
#[test]
126126
fn test_create_bip321_uri_cashu_only() {
127127
let uri = create_bip321_uri(Some(TEST_CREQ.to_string()), None, None);
128128

129129
assert_uri_prefix(&uri);
130-
assert!(uri.contains("creq="));
131-
assert!(!uri.contains("lightning="));
132-
assert!(!uri.contains("lno="));
130+
assert!(uri.contains("CREQ="));
131+
assert!(!uri.contains("LIGHTNING="));
132+
assert!(!uri.contains("LNO="));
133133
}
134134

135135
#[test]
136136
fn test_create_bip321_uri_bolt11_only() {
137137
let uri = create_bip321_uri(None, Some(TEST_BOLT11.to_string()), None);
138138

139139
assert_uri_prefix(&uri);
140-
assert!(uri.contains(&format!("lightning={TEST_BOLT11}")));
141-
assert!(!uri.contains("creq="));
140+
assert!(uri.contains(&format!("LIGHTNING={}", TEST_BOLT11.to_ascii_uppercase())));
141+
assert!(!uri.contains("CREQ="));
142142
}
143143

144144
#[test]
@@ -152,14 +152,14 @@ mod tests {
152152
);
153153

154154
assert_uri_prefix(&uri);
155-
assert!(uri.contains("creq="));
156-
assert!(uri.contains("lightning="));
157-
assert!(uri.contains("lno="));
155+
assert!(uri.contains("CREQ="));
156+
assert!(uri.contains("LIGHTNING="));
157+
assert!(uri.contains("LNO="));
158158
}
159159

160160
#[test]
161161
fn test_create_bip321_uri_empty() {
162162
let uri = create_bip321_uri(None, None, None);
163-
assert_eq!(uri, "bitcoin:");
163+
assert_eq!(uri, "BITCOIN:");
164164
}
165165
}

crates/cdk/src/wallet/bip321.rs

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -308,50 +308,63 @@ fn extract_payment_methods<'a>(
308308

309309
impl fmt::Display for Bip321UriBuilder {
310310
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311-
f.write_str("bitcoin:")?;
311+
let mut uri = String::from("BITCOIN:");
312312

313-
// On-chain address goes in the path
314313
if let Some(ref addr) = self.onchain_address {
315-
f.write_str(addr)?;
314+
uri.push_str(&uppercase_qr_address(addr));
316315
}
317316

318317
let mut query_params = form_urlencoded::Serializer::new(String::new());
319318

320319
if let Some(amount_sats) = self.amount_sats {
321320
let amount_btc = format_btc_amount_from_sats(amount_sats);
322-
query_params.append_pair("amount", &amount_btc);
321+
query_params.append_pair("AMOUNT", &amount_btc);
323322
}
324323

325324
if let Some(ref label) = self.label {
326-
query_params.append_pair("label", label);
325+
query_params.append_pair("LABEL", &label.to_ascii_uppercase());
327326
}
328327

329328
if let Some(ref message) = self.message {
330-
query_params.append_pair("message", message);
329+
query_params.append_pair("MESSAGE", &message.to_ascii_uppercase());
331330
}
332331

333332
if let Some(ref bolt11) = self.bolt11_invoice {
334-
query_params.append_pair("lightning", bolt11);
333+
query_params.append_pair("LIGHTNING", &bolt11.to_ascii_uppercase());
335334
}
336335

337336
if let Some(ref bolt12) = self.bolt12_offer {
338-
query_params.append_pair("lno", bolt12);
337+
query_params.append_pair("LNO", &bolt12.to_ascii_uppercase());
339338
}
340339

341340
if let Some(ref creq) = self.cashu_request_str {
342-
query_params.append_pair("creq", creq);
341+
query_params.append_pair("CREQ", &creq.to_ascii_uppercase());
343342
}
344343

345344
let query_string = query_params.finish();
346345
if !query_string.is_empty() {
347-
f.write_str("?")?;
348-
f.write_str(&query_string)?;
346+
uri.push('?');
347+
uri.push_str(&query_string);
349348
}
350349

351-
Ok(())
350+
f.write_str(&uri)
352351
}
353352
}
354353

354+
fn uppercase_qr_address(address: &str) -> String {
355+
if is_qr_friendly_segwit_address(address) {
356+
return address.to_ascii_uppercase();
357+
}
358+
359+
address.to_string()
360+
}
361+
362+
fn is_qr_friendly_segwit_address(address: &str) -> bool {
363+
let lowercase = address.to_ascii_lowercase();
364+
365+
lowercase.starts_with("bc1") || lowercase.starts_with("tb1") || lowercase.starts_with("bcrt1")
366+
}
367+
355368
/// Format satoshis to BTC string without trailing zeros, per BIP 21.
356369
fn format_btc_amount_from_sats(sats: u64) -> String {
357370
let whole = sats / 100_000_000;
@@ -377,8 +390,8 @@ mod tests {
377390
}
378391

379392
fn assert_has_creq(uri: &str) {
380-
assert!(uri.starts_with("bitcoin:?") || uri.starts_with("bitcoin:bc1"));
381-
assert!(uri.contains("creq="));
393+
assert!(uri.starts_with("BITCOIN:?") || uri.starts_with("BITCOIN:BC1"));
394+
assert!(uri.contains("CREQ="));
382395
}
383396

384397
fn assert_demo_cashu(parsed: &ParsedPaymentInstruction) {
@@ -410,16 +423,16 @@ mod tests {
410423
.with_message("Coffee payment".to_string())
411424
.to_string();
412425

413-
assert!(uri.starts_with(&format!("bitcoin:{addr}?")));
414-
assert!(uri.contains("creq="));
415-
assert!(uri.contains("amount=0.001"));
416-
assert!(uri.contains("message=Coffee+payment"));
426+
assert!(uri.starts_with(&format!("BITCOIN:{}?", addr.to_ascii_uppercase())));
427+
assert!(uri.contains("CREQ="));
428+
assert!(uri.contains("AMOUNT=0.001"));
429+
assert!(uri.contains("MESSAGE=COFFEE+PAYMENT"));
417430
}
418431

419432
#[test]
420433
fn test_bip321_uri_empty_params() {
421434
let uri = Bip321UriBuilder::new().to_string();
422-
assert_eq!(uri, "bitcoin:");
435+
assert_eq!(uri, "BITCOIN:");
423436
}
424437

425438
#[test]
@@ -436,7 +449,7 @@ mod tests {
436449
.with_label("a=b&c=d".to_string())
437450
.with_message("hello world".to_string())
438451
.to_string();
439-
assert_eq!(uri, "bitcoin:?label=a%3Db%26c%3Dd&message=hello+world");
452+
assert_eq!(uri, "BITCOIN:?LABEL=A%3DB%26C%3DD&MESSAGE=HELLO+WORLD");
440453
}
441454

442455
#[tokio::test]
@@ -449,19 +462,19 @@ mod tests {
449462

450463
#[tokio::test]
451464
async fn test_parse_bip321_uri_with_cashu() {
452-
let uri = format!("bitcoin:?creq={TEST_CREQ}");
465+
let uri = format!("BITCOIN:?CREQ={TEST_CREQ}");
453466
let parsed = parse_payment_instruction(&uri, bitcoin::Network::Bitcoin)
454467
.await
455-
.expect("should parse bitcoin: URI with creq");
468+
.expect("should parse BITCOIN: URI with CREQ");
456469
assert_demo_cashu(&parsed);
457470
}
458471

459472
#[tokio::test]
460473
async fn test_parse_bip321_uri_with_cashu_and_amount() {
461-
let uri = format!("bitcoin:?creq={TEST_CREQ}&amount=0.00001");
474+
let uri = format!("BITCOIN:?CREQ={TEST_CREQ}&AMOUNT=0.00001");
462475
let parsed = parse_payment_instruction(&uri, bitcoin::Network::Bitcoin)
463476
.await
464-
.expect("should parse bitcoin: URI with creq and amount");
477+
.expect("should parse BITCOIN: URI with CREQ and AMOUNT");
465478

466479
assert_eq!(parsed.cashu_requests.len(), 1);
467480
assert_eq!(parsed.amount_msats, Some(1_000_000));
@@ -521,9 +534,9 @@ mod tests {
521534
.with_onchain_address("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string())
522535
.with_amount_sats(100_000)
523536
.to_string();
524-
assert!(uri.contains("creq="));
525-
assert!(uri.contains("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"));
526-
assert!(uri.contains("amount=0.001"));
537+
assert!(uri.contains("CREQ="));
538+
assert!(uri.contains("BC1QAR0SRRR7XFKVY5L643LYDNW9RE59GTZZWF5MDQ"));
539+
assert!(uri.contains("AMOUNT=0.001"));
527540
}
528541

529542
#[tokio::test]
@@ -544,18 +557,48 @@ mod tests {
544557
let uri = Bip321UriBuilder::new()
545558
.with_bolt12_offer(offer.to_string())
546559
.to_string();
547-
assert!(uri.starts_with("bitcoin:?"));
548-
assert!(uri.contains(&format!("lno={offer}")));
549-
assert!(!uri.contains("creq="));
550-
assert!(!uri.contains("lightning="));
560+
assert!(uri.starts_with("BITCOIN:?"));
561+
assert!(uri.contains(&format!("LNO={}", offer.to_ascii_uppercase())));
562+
assert!(!uri.contains("CREQ="));
563+
assert!(!uri.contains("LIGHTNING="));
551564
}
552565

553566
#[test]
554567
fn test_bip321_uri_with_label() {
555568
let uri = Bip321UriBuilder::new()
556569
.with_label("Donation".to_string())
557570
.to_string();
558-
assert!(uri.contains("label=Donation"));
571+
assert!(uri.contains("LABEL=DONATION"));
572+
}
573+
574+
#[test]
575+
fn test_base58_address_preserved() {
576+
let addr = "175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W";
577+
let uri = Bip321UriBuilder::new()
578+
.with_onchain_address(addr.to_string())
579+
.to_string();
580+
581+
assert_eq!(uri, format!("BITCOIN:{addr}"));
582+
}
583+
584+
#[test]
585+
fn test_testnet_segwit_address_is_uppercased() {
586+
let addr = "tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg";
587+
let uri = Bip321UriBuilder::new()
588+
.with_onchain_address(addr.to_string())
589+
.to_string();
590+
591+
assert_eq!(uri, format!("BITCOIN:{}", addr.to_ascii_uppercase()));
592+
}
593+
594+
#[test]
595+
fn test_regtest_segwit_address_is_uppercased() {
596+
let addr = "bcrt1qxlvaw9k0v4m4u6sz5s4z7qjv3f4x0n8xk5m9z2";
597+
let uri = Bip321UriBuilder::new()
598+
.with_onchain_address(addr.to_string())
599+
.to_string();
600+
601+
assert_eq!(uri, format!("BITCOIN:{}", addr.to_ascii_uppercase()));
559602
}
560603

561604
#[tokio::test]

0 commit comments

Comments
 (0)