Implementing BIP21 QR Support: Project Overview

Ian Slane
5 min readJun 21, 2024

--

https://bitcoinqr.dev/

Before I go into how I got started and the initial setup of the project. Let’s talk about the project, but first, if you haven’t read my first post in this series, check it out here to understand the context better.

The plan is to implement BIP21 QR code support for LDK Node. But what is LDK Node and what’s BIP21?

LDK Node

LDK Node is a ready-to-go lightning node library built in Rust with the Lightning Development Kit (LDK) and Bitcoin Development Kit (BDK). Its main objective is to offer a compact, user-friendly interface that makes it easy for users to set up and manage a lightning node with an integrated on-chain wallet. Despite its minimal approach, LDK Node strives to be modular and configurable enough to accommodate diverse use cases.

Getting started with LDK Node

The core component of the library is the Node, which the user can set up by configuring a Builder to your needs and then calling one of the build methods. Once configured you can control Node with commands like start, connect_open_channel, send, and stop.

Here’s a simple example from the ldk-node repo:

use ldk_node::Builder;
use ldk_node::lightning_invoice::Bolt11Invoice;
use ldk_node::lightning::ln::msgs::SocketAddress;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::bitcoin::Network;
use std::str::FromStr;

fn main() {
let mut builder = Builder::new();
builder.set_network(Network::Testnet);
builder.set_esplora_server("https://blockstream.info/testnet/api".to_string());
builder.set_gossip_source_rgs("https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string());

let node = builder.build().unwrap();

node.start().unwrap();

let funding_address = node.onchain_payment().new_address();

// .. fund address ..

let node_id = PublicKey::from_str("NODE_ID").unwrap();
let node_addr = SocketAddress::from_str("IP_ADDR:PORT").unwrap();
node.connect_open_channel(node_id, node_addr, 10000, None, None, false).unwrap();

let event = node.wait_next_event();
println!("EVENT: {:?}", event);
node.event_handled();

let invoice = Bolt11Invoice::from_str("INVOICE_STR").unwrap();
node.bolt11_payment().send(&invoice).unwrap();

node.stop().unwrap();
}

LDK Nodes Modularity

LDK Node is built with several opinionated design choices in mind

  • On-chain data is managed by a BDK wallet
  • Chain data can currently be sourced from an Esplora server, with upcoming support for Electrum and bitcoind RPC.
  • Wallet and channel state can be persisted in a SQLite database, the file system, or a custom backend
  • Gossip data can be sourced from a lightning peer-to-peer network or the Rapid Gossip Sync protocol.
  • Entropy for the lightning and on-chain wallets come from raw bytes or BIP39 mnemonics. LDK Node also provides methods to generate and store these entropy bytes.

Language Support

LDK Node is written in Rust, making it a native library dependency for any standard Rust program. In addition to its Rust API, it offers language bindings for Swift, Kotlin, and Python via UniFFI. Flutter bindings are also available.

Understanding BIP21

BIP21 defines a URI scheme for creating bitcoin payment links or QR codes. BIP21 standardizes the way bitcoin payments can be requested using a simple mutable format.

Why BIP21 is Useful

Traditionally users have had to choose between on-chain and lightning payments, which can be a bit confusing and has led to interoperability issues between wallets. Most wallets either support on-chain transactions or lightning payments, and those who support both typically use a tab between the two formats. BIP21 addresses this problem by allowing a single unified QR code to contain both an on-chain bitcoin address and a lightninglightning invoice. This way, users don’t have to decide between payment methods; instead, the wallet can handle it automatically. This simplification is useful for maintaining interoperability between different wallets.

How BIP21 Works

The main parts of a BIP21 URI are:

  1. The Scheme: The URI starts with bitcoin: (uppercase or lowercase).
  2. The Address: following bitcoin: is the bitcoin address.
  3. Parameters: There are additional, optional parameters that can be appended to the URI to provide a more detailed payment. Such as amount, label, and lightning! For my project, the lightning parameter is the most important. The lightning scheme is (you guessed it), lightning= followed by an invoice, payable offer, or keysend payment.

An example of a BIP21 URI created from my project:

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

The Projects Initial Set Up

The initial setup for the project was already prepared because of my project proposal. I started planning how I would structure and write the code in hopes that it would give me a better chance of getting the internship, so I ended up writing most of the boilerplate code. The project was well on its way by the time I turned in my proposal. But, of course, things don’t always go as planned and I had to make some changes.

I cloned the ldk-node repo and began making the unified QR code builder module I proposed with helper methods where you can call a UnifiedQrBuilder object and just add parameters as you need. The URI generator method I thought about looked like this:

pub fn generate_unified_qr(
onchain_payment: &OnchainPayment,
bolt11_invoice: &Bolt11Payment,
amount: u64,
label: &str,
expiry_secs: u32,
) -> Result<Uri, Error> {
let onchain_address = onchain_payment.new_address().unwrap();

let msats = amount * 1_000;
let bolt11_invoice = bolt11_invoice.receive(msats, label, expiry_secs)?;

let builder = UnifiedQrBuilder::new(onchain_address)
.amount(Amount::from_sat(amount))
.label(label.to_string())
.set_bolt11_invoice(bolt11_invoice);

builder.build()
}

So in my head, a user could say:
let uri = UnifiedQrBuilder::new(address).generate_unified_qr(address, invoice, amount, label, expiry_seconds;
Then it would spit out a QR code, but I soon found out that this idea couldn’t work because we want our BDK wallet and lightning node to take care of most of this behind the scenes. So after talking with my project mentor, we came up with a better idea. Create a UnifiedQrPayment, payment handler with simple send and receive methods. The receive method, in short, would have the user’s wallet create and pass in their address and their node creates and passes in the invoice. Then with that information, an amount, an optional message, and an expiry time the method would generate a URI. Once the URI is made, the user could use the Swift or Kotlin bindings to generate the actual QR code (most likely in a native library). Next, the idea for the send method is to pass in a URI string, parse the address, invoice, or offer, and pay the offer or invoice, and if those fail fallback to pay to the on-chain address! Of course, it’s a bit more complex than that but that’s the idea. By project completion, users will be able to effortlessly generate and transmit a QR code using the following process (or something similar):

use ldk_node::Builder;
use ldk_node::lightning_invoice::Bolt11Invoice;
use ldk_node::lightning::ln::msgs::SocketAddress;
use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::bitcoin::Network;
use std::str::FromStr;

fn main() {
let mut builder = Builder::new();
builder.set_network(Network::Testnet);
builder.set_esplora_server("https://blockstream.info/testnet/api".to_string());
builder.set_gossip_source_rgs("https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string());

let node = builder.build().unwrap();

node.start().unwrap();

let funding_address = node.onchain_payment().new_address();

// .. fund address ..

let node_id = PublicKey::from_str("NODE_ID").unwrap();
let node_addr = SocketAddress::from_str("IP_ADDR:PORT").unwrap();
node.connect_open_channel(node_id, node_addr, 10000, None, None, false).unwrap();

let event = node.wait_next_event();
println!("EVENT: {:?}", event);
node.event_handled();

// .. generate URI with the receive method ..
let sats = 100_000;
let expiry_sec = 6_000;
let unified_qr_code = node.unified_qr_payment().receive(sats, "Message", expiry_sec);

// .. send payment given a URI ..
node.unified_qr_payment().send(some_uri_string);


node.stop().unwrap();
}

Looking Ahead

The bip21 Rust crate will do a lot of the heavy lifting, especially serializing and deserializing the URI. In the next post in this project series, I’ll be going into how I’m using the bip21 crate to help with the project. Then, I’ll talk about my first draft of the project and the code I’m writing. Stay tuned!

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