Bip321-Rust: Composable Bitcoin URI Parser
Features
- Full BIP-321 Support: Handles the standard
bitcoin: scheme.
- Composable Architecture: Use the Bip321ExtraHandle trait to easily add support for custom query parameters (Payjoin, SP, LN, etc.) without modifying the core parser.
- Strict Validation:
- Prevents duplicate parameters for standard keys (label, message, amount, pop).
- Correctly handles req- (required) parameters for forward compatibility.
- Validates Bitcoin amounts with 8-decimal precision.
- Network Validation: Effortlessly verify if an address matches your wallet's current network (Mainnet, Testnet, etc.).
📦 Installation
[dependencies]
bitcoin = "0.32.8"
bitcoin-uri-composer = "0.1.0"
🛠 Usage
Basic Parsing & Network Checking
use bitcoin::Network;
use bitcoin_uri_composer::{Bip321, ExtraExample};
fn main() {
let uri = "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?amount=1.5";
let unchecked = Bip321::<ExtraExample>::parse_url(uri).expect("Invalid URI");
match unchecked.into_checked(Network::Bitcoin) {
Ok(payment) => {
println!("Address is valid for Mainnet: {:?}", payment.address);
println!("Amount to send: {:?}", payment.amount);
},
Err(e) => eprintln!("Validation error: {:?}", e),
}
}
Advanced: Handling Extras (Lightning, Payjoin, Secret Payment)
let uri = "bitcoin:?sp=sp1qsilentpayment&pj=https://payjoin.example.com";
let result = Bip321::<ExtraExample>::parse_url(uri).unwrap();
if let Some(extras) = result.extras {
println!("Silent Payment: {:?}", extras.sp);
println!("Payjoin Endpoints: {:?}", extras.pj);
}
🧩 The Extension System
To support new payment protocols, simply implement the Bip321ExtraHandle trait for your own struct:
pub trait Bip321ExtraHandle<'a>
where
Self: Default,
{
fn handle_param(
&mut self,
key: &'a str,
value: Vec<Cow<'a, str>>,
) -> Result<(), Bip321Errors<'a>>;
fn validate(&self, _network: Network) -> Result<(), Bip321Errors<'a>> {
Ok(())
}
fn is_empty(&self) -> bool;
fn is_supported_key(&self, key: &str) -> bool;
}
🧩 How to Implement Your Own Handler
1. Define your data structure
#[derive(Debug, Default)]
pub struct MyWalletExtras {
pub payjoin_endpoints: Vec<String>,
pub lightning_invoice: Vec<String>,
}
2. Implement the Bip321ExtraHandle trait
impl<'a> Bip321ExtraHandle<'a> for MyWalletExtras {
fn handle_param(&mut self, key: &'a str, values: Vec<Cow<'a, str>>) -> Result<(), Bip321Errors<'a>> {
match key {
"pj" => {
for val in values {
self.payjoin_endpoints.push(val.to_string());
};
Ok(())
}
"lightning" => {
for val in values {
self.lightning_invoice.push(val.to_string());
};
Ok(())
}
_ => Ok(()), }
}
fn validate(&self, _network: Network) -> Result<(), Bip321Errors> {
if let Some(ref url) = self.payjoin_endpoints[0] {
if !url.starts_with("https://") && !url.contains(".onion") {
return Err(Bip321Errors::InvalidRequiredPayment);
}
}
Ok(())
}
fn is_supported_key(&self, key: &str) -> bool {
matches!(key, "pj" | "lightning")
}
fn is_empty(&self) -> bool {
self.payjoin_endpoints.is_none() && self.lightning_invoice.is_none()
}
}
3. Use it with the parser
let uri = "bitcoin:address?pj=https://...&req-unknown=123";
let result = Bip321::<MyWalletExtras>::parse_url(uri).unwrap();