Skip to content

feat: add BIP353 DNS payment instructions#236

Open
sdmg15 wants to merge 1 commit into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Open

feat: add BIP353 DNS payment instructions#236
sdmg15 wants to merge 1 commit into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15
Copy link
Copy Markdown

@sdmg15 sdmg15 commented Jan 7, 2026

Description

This PR adds support for BIP353 payment instructions.
The following notable changes are done:

  • Adding of an option resolve_dns_recipient which receives a Human Readable Name (HRN) and returns the associated address
  • Modify the create_tx for it to support user specifying recipients as HRN.

Notes to the reviewers

Here is a list of HRN that could be used to test the implementation:

  • pay@dunxen.dev: Supports BOLT 12 Payment method only
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain
  • Testing resolving: bdk-cli -n bitcoin resolve_dns_recipient send.some@satsto.me
  • Testing create tx: bdk-cli -n bitcoin wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "send.some@satsto.me:5000"

Changelog notice

  • Add top level command resolve_dns_recipient to resolve the DNS payment instructions
  • Add option --to_dns to create_tx command to allow sending to a Human Readable Name (HRN)

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9d Compare January 8, 2026 18:13
@sdmg15 sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@va-an
Copy link
Copy Markdown
Contributor

va-an commented Feb 7, 2026

@sdmg15 Hi! Could you make it compile?

@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349b Compare February 9, 2026 08:51
@coveralls
Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 21818410493

Details

  • 0 of 120 (0.0%) changed or added relevant lines in 3 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage decreased (-0.5%) to 10.237%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/utils.rs 0 15 0.0%
src/handlers.rs 0 36 0.0%
src/dns_payment_instructions.rs 0 69 0.0%
Files with Coverage Reduction New Missed Lines %
src/handlers.rs 1 12.57%
Totals Coverage Status
Change from base Build 21153868360: -0.5%
Covered Lines: 268
Relevant Lines: 2618

💛 - Coveralls

@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756e Compare February 11, 2026 04:52
@sdmg15
Copy link
Copy Markdown
Author

sdmg15 commented Feb 11, 2026

@va-an Thanks for taking a look. I think it should be resolved now.

Copy link
Copy Markdown
Contributor

@va-an va-an left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @sdmg15!
I have left a few suggestions, please have a look.

Comment thread Cargo.toml Outdated
Comment thread src/dns_payment_instructions.rs
Comment thread src/dns_payment_instructions.rs Outdated
Comment thread src/commands.rs Outdated
Comment thread src/handlers.rs Outdated
Comment thread src/dns_payment_instructions.rs
Comment thread src/dns_payment_instructions.rs
@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5 Compare February 13, 2026 09:21
@sdmg15
Copy link
Copy Markdown
Author

sdmg15 commented Feb 13, 2026

@va-an Applied some reviews and recommendation on 6724be5

I've also updated the PR description.

@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094 Compare February 13, 2026 11:46
Copy link
Copy Markdown
Contributor

@va-an va-an left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdmg15 thanks for update and for the detailed description!
Please take a look at the comments.

Comment thread src/dns_payment_instructions.rs Outdated
Comment thread src/handlers.rs Outdated
Comment thread src/handlers.rs Outdated
Comment thread src/handlers.rs Outdated
Comment thread src/handlers.rs Outdated
@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673 Compare February 17, 2026 20:12
Copy link
Copy Markdown
Contributor

@va-an va-an left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdmg15 thanks for update!
I've added some suggestions, pls have a look.

Comment thread src/utils.rs Outdated
Comment thread src/dns_payment_instructions.rs Outdated
Comment thread src/commands.rs Outdated
Comment thread src/dns_payment_instructions.rs Outdated
@tvpeter tvpeter moved this to In Progress in BDK-CLI Feb 23, 2026
@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798de Compare March 16, 2026 12:44
@va-an
Copy link
Copy Markdown
Contributor

va-an commented Mar 28, 2026

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15 sdmg15 force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ec Compare March 31, 2026 08:46
Copy link
Copy Markdown
Contributor

@va-an va-an left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the previous round of feedback. Before tackling more changes - branch has merge conflicts with master, would you mind rebasing first? A few more comments below.


Payment struct design

Payment is shared between two flows with different needs.

In the send flow (process_instructions -> call site in handlers.rs) only receiving_addr is read, everything else is built and discarded. .unwrap() is needed only because the field is Option<Address> despite always being Some here.

In the resolve flow (resolve_dns_recipient) the same struct is filled with the opposite subset: receiving_addr/expected_amount always None, metadata fields populated for display. So every Option<T> here is "always Some" in one flow and "always None" in the other - the type system encodes nullability that doesn't actually exist in either path.

Suggested split:

// resolve flow - display only
pub struct ResolvedPaymentInfo {
    pub hrn: String,
    pub payment_methods: Vec<PaymentMethod>,
    pub description: Option<String>,
    pub min_amount: Option<Amount>,
    pub max_amount: Option<Amount>,
    pub notes: String,
}
// `display(pretty)` moves here

// send flow - return what's actually used
pub async fn process_instructions(...) -> Result<(Address, Amount), Error>

This resolves several inline comments:

  • unwrap on receiving_addr
  • the FixedAmount amount mismatch
  • both unwraps on human_readable_name().

One small process note: please leave review threads open after addressing them - the convention in this repo is for the reviewer to resolve them once they confirm the fix.

Comment on lines +25 to +39
let mut methods: Vec<String> = Vec::new();
self.payment_methods.iter().for_each(|pm| match pm {
bitcoin_payment_instructions::PaymentMethod::LightningBolt11(bolt11) => {
methods.push(format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15)))
}
bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
methods.push(format!("Bolt 12 invoice ({})", shorten(offer, 20, 15)))
}
bitcoin_payment_instructions::PaymentMethod::OnChain(address) => {
methods.push(format!("On chain ({})", address))
}
bitcoin_payment_instructions::PaymentMethod::Cashu(csh) => {
methods.push(format!("Cashu payment ({})", shorten(csh, 20, 15)))
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider map + collect here - drops the mutable accumulator and lets match return the String directly. Would need PaymentMethod::* in the use block.

Suggested change
let mut methods: Vec<String> = Vec::new();
self.payment_methods.iter().for_each(|pm| match pm {
bitcoin_payment_instructions::PaymentMethod::LightningBolt11(bolt11) => {
methods.push(format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15)))
}
bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
methods.push(format!("Bolt 12 invoice ({})", shorten(offer, 20, 15)))
}
bitcoin_payment_instructions::PaymentMethod::OnChain(address) => {
methods.push(format!("On chain ({})", address))
}
bitcoin_payment_instructions::PaymentMethod::Cashu(csh) => {
methods.push(format!("Cashu payment ({})", shorten(csh, 20, 15)))
}
});
let methods: Vec<String> = self
.payment_methods
.iter()
.map(|pm| match pm {
LightningBolt11(bolt11) => {
format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15))
}
LightningBolt12(offer) => format!("Bolt 12 invoice ({})", shorten(offer, 20, 15)),
OnChain(address) => format!("On chain ({})", address),
Cashu(csh) => format!("Cashu payment ({})", shorten(csh, 20, 15)),
})
.collect();

Comment on lines +26 to +38
self.payment_methods.iter().for_each(|pm| match pm {
bitcoin_payment_instructions::PaymentMethod::LightningBolt11(bolt11) => {
methods.push(format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15)))
}
bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => {
methods.push(format!("Bolt 12 invoice ({})", shorten(offer, 20, 15)))
}
bitcoin_payment_instructions::PaymentMethod::OnChain(address) => {
methods.push(format!("On chain ({})", address))
}
bitcoin_payment_instructions::PaymentMethod::Cashu(csh) => {
methods.push(format!("Cashu payment ({})", shorten(csh, 20, 15)))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shorten runs before the if pretty check, so JSON output contains truncated Bolt 11 / Bolt 12 invoices and Cashu tokens - downstream tools can't use them.

Non-pretty output:

-> % cargo run --all-features -- -n bitcoin resolve_dns_recipient send.some@satsto.me
{
  "description": null,
  "expected_amount_to_send": null,
  "hrn": "send.some@satsto.me",
  "max_amount": null,
  "min_amount": null,
  "notes": "This is configurable payment instructions. You must send an amount between min_amount and max_amount if set.",
  "payment_methods": [
    "On chain (bc1qztwy6xen3zdtt7z0vrgapmjtfz8acjkfp5fp7l)",
    "Bolt 12 invoice (lno1zr5qyugqgskrk70k...ph4xrxtk2xc3lpq)"
  ]
}

The shorten calls must be moved into the if pretty branch only.

Comment thread src/commands.rs
dns_recipients: Vec<(String, u64)>,
#[cfg(feature = "dns_payment")]
/// Custom resolver DNS IP to be used for resolution.
#[arg(long = "dns_resolver", default_value = "8.8.8.8:53")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default DNS resolver value is inconsistent between the two commands:

  • ResolveDnsRecipient::resolver -> "8.8.8.8"
  • CreateTx::dns_resolver -> "8.8.8.8:53"

parse_dns_instructions already appends :53 when no port is present, so the :53 suffix in CreateTx is redundant. Drop it to keep the defaults aligned.

Also worth aligning the flag names themselves - --resolver in one command and --dns_resolver in the other forces users to remember two names for the same thing.

Same value also keeps getting renamed as it travels through the call stack. For the resolve command:

  • commands.rs -> resolver
  • handlers.rs::handle_resolve_dns_recipient_command(... resolver: String ...)
  • dns_payment_instructions.rs::resolve_dns_recipient(... ip: String)
  • dns_payment_instructions.rs::parse_dns_instructions(... resolver_ip: String)

Three different names for the same value makes the flow harder to follow. Use dns_resolver everywhere - both as the CLI flag (--dns_resolver) and as the field/parameter name across all functions.

pub(crate) async fn parse_dns_instructions(
hrn: &str,
network: Network,
resolver_ip: String,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take &str instead of String - format! already allocates, and it removes the need for dns_resolver.clone() at the call site.

Comment thread src/handlers.rs
Comment on lines +368 to +370
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this use to the top of the file, next to the existing dns_payment_instructions import on line 16.

Comment thread src/handlers.rs
.map_err(|e| Error::Generic(format!("Parsing error occured {e:#?}")))?;
let payment = process_instructions(amount, &instructions, resolver).await?;

recipients.push((payment.receiving_addr.unwrap().into(), amount));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

  1. amount is the user-supplied value, not payment.expected_amount. For FixedAmount instructions the recipient sets an exact amount, and the tx silently goes out with whatever the user typed instead.

  2. .unwrap() relies on process_instructions always setting receiving_addr: Some(...) - an implicit invariant across two files.

Both resolved by the structural change in the top-level comment.

@va-an
Copy link
Copy Markdown
Contributor

va-an commented May 15, 2026

@sdmg15 when addressing the comments, could you split the changes into separate commits per independent fix instead of squashing everything into one? It makes the review much easier to follow (each commit maps to one review thread), and helps with bisect/revert later if needed. They can always be squashed at merge time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants