feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add BIP353 DNS payment instructions - #236

Merged
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions
Jul 8, 2026
Merged

feat: add BIP353 DNS payment instructions#236
tvpeter merged 5 commits into
bitcoindevkit:masterfrom
sdmg15:feat/bip353-payment-instructions

Conversation

@sdmg15

@sdmg15sdmg15 commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

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 (CAUTION: mainnet)
  • send.some@satsto.me: Supports BOLT 12 payment and On Chain (CAUTION: mainnet)
  • Testing resolving: bdk-cli -n testnet resolve_dns_recipient testnet@bitdevsyde.org
  • Testing create tx: bdk-cli -n testnet wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "testnet@bitdevsyde.org: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
sdmg15force-pushed the feat/bip353-payment-instructions branch from ba3c713 to a0f3e9dCompareJanuary 8, 2026 18:13
@sdmg15
sdmg15 marked this pull request as ready for review January 9, 2026 08:39
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 Hi! Could you make it compile?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 49154f3 to baf349bCompareFebruary 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 CoverageCovered LinesChanged/Added Lines%
src/utils.rs0150.0%
src/handlers.rs0360.0%
src/dns_payment_instructions.rs0690.0%
Files with Coverage ReductionNew Missed Lines%
src/handlers.rs112.57%
TotalsCoverage Status
Change from base Build 21153868360:-0.5%
Covered Lines:268
Relevant Lines:2618

💛 - Coveralls

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from baf349b to 0e9756eCompareFebruary 11, 2026 04:52
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadCargo.toml Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs
Comment threadsrc/dns_payment_instructions.rs
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0e9756e to 6724be5CompareFebruary 13, 2026 09:21
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

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

I've also updated the PR description.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 6724be5 to 20ef094CompareFebruary 13, 2026 11:46

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch 2 times, most recently from 977534a to 4852673CompareFebruary 17, 2026 20:12

@vadim-anfvvadim-anfv left a comment

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.

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

Comment threadsrc/utils.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@tvpetertvpeter moved this to In Progress in BDK-CLIFeb 23, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 4852673 to 58798deCompareMarch 16, 2026 12:44
@vadim-anfv

Copy link
Copy Markdown
Contributor

@sdmg15 thanks for updates!
Could you rebase?

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 58798de to 72656ecCompareMarch 31, 2026 08:46

@vadim-anfvvadim-anfv left a comment

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.

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 onlypubstructResolvedPaymentInfo{pubhrn:String,pubpayment_methods:Vec<PaymentMethod>,pubdescription:Option<String>,pubmin_amount:Option<Amount>,pubmax_amount:Option<Amount>,pubnotes:String,}// `display(pretty)` moves here// send flow - return what's actually usedpubasyncfnprocess_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 threadsrc/dns_payment_instructions.rs Outdated
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
letmut 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 threadsrc/dns_payment_instructions.rs Outdated
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 threadsrc/commands.rs Outdated
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.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/handlers.rs Outdated
.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.

@vadim-anfv

Copy link
Copy Markdown
Contributor

@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.

@vadim-anfvvadim-anfv left a comment

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.

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

@tvpeter

Copy link
Copy Markdown
Collaborator

I just found out that we're definitely dropping the pretty flag in #278, so please remove all the code related to this flag. This will also address some of my review comments.

Upd: although removing pretty output for this feature may create inconsistency with the current codebase, I expect we won't have any releases before #278 is merged, so this shouldn't become an issue for users.

@tvpeter please correct me if my assumption is wrong.

Yes, that's correct. The --pretty flag is been removed. But you don't have to worry about removing it at your end. Once it is merged, I will clean it out.

@tvpetertvpeter added this to the CLI 4.0.0 milestone May 22, 2026

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sdmg15 thank you for working on this.

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

Thank you

Comment threadsrc/handlers.rs Outdated
Comment on lines +367 to +376
for recipient in dns_recipients {
use crate::dns_payment_instructions::{
parse_dns_instructions, process_instructions,
};
let amount = Amount::from_sat(recipient.1);
let (resolver, instructions) = parse_dns_instructions(
&recipient.0,
cli_opts.network,
dns_resolver.clone(),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it is possible, can you handle this DNS resolution concurrently? Else, print the status on the terminal so that users are aware that it is resolving the DNS.

let PaymentMethod::OnChain(addr) = instructions
.methods()
.iter()
.find(|ix| matches!(ix, PaymentMethod::OnChain(_)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you will have to reconsider prioritizing SP code for privacy concerns.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SP is still of type OnChain it's just the address derived that will change.

@sdmg15

sdmg15 commented May 25, 2026

Copy link
Copy Markdown
ContributorAuthor

The comment I left regarding prioritizing silent payment code, I understand that this PR was worked on at a time when the silent payment feature was not yet merged, so you may have prioritized on-chain addresses. But since SP has been merged, and BIP353 (I believe) is designed to be paired with Silent Payments to prevent privacy leaks through permanent address reuse. So you might have to reconsider that implementation aspect.

Yes I think this will be great but the issue here is this will require some upstream changes to the Address or PaymentMethod type in order to be able to support the sp codes formats which I'm not sure they want to support experimental features/crates.

Also, this feature has indicated using mainnet, and we have heavily discouraged users from using this app on mainnet. So you may also consider sharing another functional test network that users can use to play with.

I've updated the examples in the description with testnet DNS address

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 72656ec to 5b8d1b1CompareMay 26, 2026 11:14

@vadim-anfvvadim-anfv left a comment

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.

@sdmg15 thanks for the work on this, and for applying the previous round - the Payment split into ResolvedPaymentInfo and process_instructions -> (Address, Amount) came out clean.

Left a few inline comments below, plus one bigger-picture suggestion: unifying the recipients before the send logic. Right now --to and --to_dns are handled in separate places - DNS recipients are only resolved inside the non-send_all branch, so the building logic sees two different sources at two different points. If instead all --to_dns entries were resolved to (address, amount) and merged into the single recipients list up front - before any tx-building or send_all branching - the send path would just work off one unified list and wouldn't need to know where a recipient came from.

This would naturally fix a couple of the inline issues:

  • --to no longer has to be required (validate "at least one recipient" on the merged list instead),
  • --send_all would stop silently dropping --to_dns recipients (it currently only looks at recipients[0]).

Separating recipient parsing/resolution from the payment logic keeps the tx-building code simpler and removes these edge cases by construction.

Thanks again!

Comment threadsrc/commands.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/handlers.rs Outdated
Comment threadsrc/dns_payment_instructions.rs Outdated
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from b2ec8d9 to 2ca719bCompareJune 17, 2026 17:15

@vadim-anfvvadim-anfv left a comment

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.

tACK 2ca719b

Tested create_tx --to_dns end-to-end on regtest (with a local patch to get past the network check). Works as expected - requesting 5000 sats and 250000 sats each produced an output of exactly that amount.

Right now just pre-push does not pass:

error: variable does not need to be mutable
--> src/handlers.rs:532:13
|
532 | mut recipients,
| ----^^^^^^^^^^
| |
| help: remove this `mut`
|
= note: `-D unused-mut` implied by `-D warnings`

Other than that everything looks great.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 2ca719b to f56f72cCompareJune 23, 2026 18:01
@sdmg15

Copy link
Copy Markdown
ContributorAuthor

@va-an Thanks for the reviews. I've addressed that. We should be good to go.

@sdmg15
sdmg15 requested a review from tvpeterJune 23, 2026 18:04

@vadim-anfvvadim-anfv left a comment

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.

Thanks a lot for all the work on this @sdmg15, great job - this has come together really nicely!

Just one small thing left: Cargo.lock contains changes unrelated to this PR - a stray cargo update bumped a lot of unrelated crates, including bdk_wallet 2.1.0 -> 2.4.0. Could you regenerate the lock from master so only the new dependency's entries land?

git checkout master -- Cargo.lock
cargo build --all-features

The lock also bumps bdk_wallet to 2.4.0 - this might not pass review from @tvpeter, see #225 (review).

Thanks again!

@codecov

codecovBot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.97%. Comparing base (2f14a36) to head (943b4b6).
⚠️ Report is 1 commits behind head on master.

Files with missing linesPatch %Lines
src/dns_payment_instructions.rs0.00%145 Missing ⚠️
src/handlers.rs0.00%106 Missing ⚠️
src/utils.rs0.00%8 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## master #236 +/- ##
==========================================
- Coverage 28.87% 26.97% -1.91% 
==========================================
Files 9 10 +1 Lines 3626 3882 +256 ==========================================
Hits 1047 1047 - Misses 2579 2835 +256 
FlagCoverage Δ
rust26.97% <0.00%> (-1.91%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f56f72c to f3f78f3CompareJune 28, 2026 13:25
@tvpetertvpeter moved this from In Progress to Ready to Review in BDK-CLIJun 28, 2026
@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from f3f78f3 to 0680744CompareJune 29, 2026 06:56

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method. So I think it is either you consider using another library that supports SP or provides mappings to non-reusable addresses or you handling that with the available toolset. But I don't think this PR as it is fully accomplishes the purpose of BIP353.

Comment threadsrc/dns_payment_instructions.rs Outdated
Comment threadsrc/commands.rs
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Making this optional here removes the validation that clap offers

@sdmg15sdmg15Jun 30, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The validation is still done but at the command implementation level

Comment threadsrc/handlers.rs Outdated
Comment on lines +540 to +547
#[cfg(feature = "dns_payment")]
mut recipients,
#[cfg(not(feature = "dns_payment"))]
recipients,
#[cfg(feature = "dns_payment")]
dns_recipients,
#[cfg(feature = "dns_payment")]
dns_resolver,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel this introduces too many variables to combine with the existing CreateTx, so it will be better to add a new wallet command that handles DNS payment separately.

@sdmg15sdmg15Jul 2, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I don't know your final take on this but I tried doing it by adding CreateDNSTx or something similar which happens to just be a duplicate of CreateTx and introduce more codes. So let me know what you think should be best approach but I think they way it's done right isn't that much harmful

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

@tvpeter

Copy link
Copy Markdown
Collaborator

Following up on on my previous review comment about prioritizing SP and your response about the library, I have checked the library and confirmed that it does not include SP payment method. This is understandable as the author is lightning first and may not consider adding SP payment method.

The library isn't "lightning first". You can check this PR rust-bitcoin/bitcoin-payment-instructions#19, there has been previous effort into adding that to the library but given that some upstream components aren't available they decided to postpone it.

But I don't think this PR as it is fully accomplishes the purpose of BIP353.

I think you're confusing the purpose of BIP353 and the one of BIP352. BIP353 for dns goal has never been about avoiding address reuse. BIP352 just came and gave more sense to that proposal as combined together you have Human Readable Name that resolves to different addresses anytime when requested.

So the whole point of my comment is that, if you can find a way around address reuse, it will serve the purpose of the BIP better. See the BIP section on it here.

Thank you.

@sdmg15

Copy link
Copy Markdown
ContributorAuthor

Yeah I get the point.
I think that section is more a caution targeting users setting their DNS payment wallet.

It's not meant for the application/resolver as you don't have control over that. Only the user will responsibly go and set a different wallet address in the DNS records.

That's why BIP353 combined now with BIP352 effectively solves that as the user won't need to update the dns record any more but will get a new fresh Bitcoin address at any request.

@sdmg15
sdmg15force-pushed the feat/bip353-payment-instructions branch from 0680744 to 5c496b1CompareJuly 2, 2026 05:28

@tvpetertvpeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utACK d279097

@tvpeter
tvpeterforce-pushed the feat/bip353-payment-instructions branch from cb5424d to 943b4b6CompareJuly 8, 2026 08:19
@tvpeter
tvpeter merged commit d8e1801 into bitcoindevkit:masterJul 8, 2026
7 of 9 checks passed
@github-project-automationgithub-project-automationBot moved this from Ready to Review to Done in BDK-CLIJul 8, 2026
@tvpetertvpeter mentioned this pull request Jul 20, 2026
14 tasks
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants

@sdmg15@vadim-anfv@coveralls@tvpeter