Implementing BIP21 URIs in LDK-Node with the bip21 Crate

Ian Slane
4 min readJul 7, 2024

--

doc.rust-lang.org/cargo/

In my previous post, we explored the world of LDK Node and the concepts of BIP21 QR code support into LDK Node, which will allow users to request bitcoin payments using a single QR code that can contain both on-chain transactions and lightning payments. If you haven’t read my last post in this series, check it out first here to understand the context better.

But what does this mean in practice? Let’s look closer behind the bip21 crate and how it will be integrated into the unified_qr.rs payment handler.

The bip21 Rust Crate

The bip21 crate is a Rust library for BIP21 bitcoin payment URIs that prioritized performance, flexibility, and compliance with the BIP21 standard, making it an ideal choice for developers looking to integrate BIP21 into their applications. This streamlines the process of creating and parsing BIP32 URIs, leveraging Rust’s strong type systems and standard traits. By using key-value pairs, I could easily build and deserialize URIs, while also using the crate’s flexible parsing and serialization features.

Adding Lightning Support for BIP21 URIs with the bip21 Crate

To integrate the bip21 crate into my project, I defined a custom type for the URI, I called it LnURI, which includes extra parameters needed for the URI. Currently, the Extras struct can hold optional BOLT11 invoices and BOLT12 offers. In the future, it could be extended to include Keysend or Silent Payment parameters based on user interest. The NetworkChecked type marker ensures that the address's network is correctly validated. This step is important because, during address parsing, we have to confirm that the address is meant for the correct network (mainnet, testnet, signet, or regtest).

Here’s where I started:

use bip21::de::ParamKind;
use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams};

type LnUri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;

#[derive(Debug, Clone)]
struct Extras {
bolt11_invoice: Option<Bolt11Invoice>,
bolt12_offer: Option<Offer>,
}

Next, I added an implementation of the SerializeParams trait for the Extras struct to help serialize the additional parameters.

The SerializeParams trait is used for converting the Extras struct into a format that can be included in the URI. It defines an iterator over key-value pairs that represent the parameters. The Key type represents the parameter name, the Value type represents the parameter value, and the Iterator type defines an iterator over key-value pairs!

In this example, lightning is the key and the invoice is the value:

&lightning=LNBC1000M1PN8GSKYDQA2PSHJMT9DE6ZQ6TWYP3XJARRDA5KUNP4QVMG8RY0YLRXECA7YE5V50MM9JTSXWW4FJNNLCU678KU0LH2SVYJQPP5DGVG4Y909VMUUJ9J92KMYG55AVZQ8P6P4CQ4NP0RF0MG67HQ4MZQSP5WQM2JFS0H43575DT9J9WQM4SW82XGKLE6LQGFX6744JZQ0RXA9QQ9QYYSGQCQPCXQRRAQ3T0D5UHM570YGP6DSPG37VG9FNSQ200DHQZU4L256WMSYK3L4H2YXV23P9SE3ZQLJD5FYCENC57L8SN3PQKW3AMLCSNGWNETL8ZH2WGQT80HPV

Currently, the project uses a single lightning key for both the BOLT11 invoices and BOLT12 offers. But, I just found out that the BOLT12 offer should be identified by the lno key which I’ll be adding in my next commit!

impl<'a> SerializeParams for &'a Extras {
type Key = &'static str;
type Value = String;
type Iterator = IntoIter<(Self::Key, Self::Value)>;

fn serialize_params(self) -> Self::Iterator {
let mut params = Vec::new();

if let Some(bolt11_invoice) = &self.bolt11_invoice {
params.push(("lightning", bolt11_invoice.to_string()));
}
if let Some(bolt12_offer) = &self.bolt12_offer {
params.push(("lightning", bolt12_offer.to_string()));
}

params.into_iter()
}
}

For deserializing the extra parameters, I needed to implement the DeserializeParams trait. It’s used to convert the serialized data back into the Extras struct. It requires defining a deserialization state and methods to process each key-value pair during deserialization. The DeserializationState trait represents the state of deserialization and includes methods to handle each parameter and finalize the deserialization process.
The DeserializeParams trait lets the Extras type parameter be deserialized from the URI. The DeserializationState tracks the deserialization process, checking if each parameter is known and processing it accordingly. The finalize method completes the deserialization, ensuring all parameters are correctly parsed and validated! Here’s what the implementation looks like:

impl<'a> DeserializeParams<'a> for Extras {
type DeserializationState = DeserializationState;
}

#[derive(Default)]
struct DeserializationState {
bolt11_invoice: Option<Bolt11Invoice>,
bolt12_offer: Option<Offer>,
}

impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
type Value = Extras;

fn is_param_known(&self, key: &str) -> bool {
key == "lightning"
}

fn deserialize_temp(
&mut self, key: &str, value: Param<'_>,
) -> Result<ParamKind, <Self::Value as DeserializationError>::Error> {
if key == "lightning" {
let lighting_str =
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;

for param in lighting_str.split('&') {
if let Ok(offer) = param.parse::<Offer>() {
self.bolt12_offer = Some(offer);
} else if let Ok(invoice) = param.parse::<Bolt11Invoice>() {
self.bolt11_invoice = Some(invoice);
}
}
Ok(bip21::de::ParamKind::Known)
} else {
Ok(bip21::de::ParamKind::Unknown)
}
}

fn finalize(self) -> Result<Self::Value, <Self::Value as DeserializationError>::Error> {
Ok(Extras { bolt11_invoice: self.bolt11_invoice, bolt12_offer: self.bolt12_offer })
}
}

impl DeserializationError for Extras {
type Error = Error;
}

Like the current serialization problem above, the project uses a single lightning key for both BOLT11 invoices and BOLT12 offers. And as you know, BOLT12 offer should be identified by the lno key. In my next commit, I’ll add this update to the deserialization part of the code.

By implementing these traits, I was easily able to serialize and deserialize the additional lightning parameters, enabling the BIP21 URIs to include both on-chain and lightning payment details. A complete URI with a BOLT11 invoice and BOLT12 offer could look something like this (although large and not ideal):

BITCOIN:BC1QUM5RL5AAMKC5YEHL9H9SRXTR4NCDEGEUVSFUGS?amount=1&message=LDK%20Rocks&lightning=LNBC1000M1PN8GSKYDQA2PSHJMT9DE6ZQ6TWYP3XJARRDA5KUNP4QVMG8RY0YLRXECA7YE5V50MM9JTSXWW4FJNNLCU678KU0LH2SVYJQPP5DGVG4Y909VMUUJ9J92KMYG55AVZQ8P6P4CQ4NP0RF0MG67HQ4MZQSP5WQM2JFS0H43575DT9J9WQM4SW82XGKLE6LQGFX6744JZQ0RXA9QQ9QYYSGQCQPCXQRRAQ3T0D5UHM570YGP6DSPG37VG9FNSQ200DHQZU4L256WMSYK3L4H2YXV23P9SE3ZQLJD5FYCENC57L8SN3PQKW3AMLCSNGWNETL8ZH2WGQT80HPV&lno=LNO1PGXXVET9VSSX6EFQWDSHGUCKYYP68KS6WHN8SSETRPXPDC3PWLR7H0JK07H5Z0Z8079JYXS0DGKQCLG

And as a QR code:

This is all integrated into the unified_qr.rs payment handler, enabling the generation and parsing of URIs. That’s all for now, but next post I’ll get into the core of the project with the receive and send methods and talk about building the URIs and the logic behind making a payment given a BIP21 URI!

As always, thanks for joining me and if you found this article insightful or have any questions or critiques, I’d love to hear from you. Feel free to reach out to me on Twitter or LinkedIn. If you want to review my PR, check it out here.

--

--

Ian Slane

Writing about stuff I learn in the world of Rust, bitcoin, and lightning