feat: add BIP353 DNS payment instructions#236
Conversation
ba3c713 to
a0f3e9d
Compare
|
@sdmg15 Hi! Could you make it compile? |
49154f3 to
baf349b
Compare
Pull Request Test Coverage Report for Build 21818410493Details
💛 - Coveralls |
baf349b to
0e9756e
Compare
|
@va-an Thanks for taking a look. I think it should be resolved now. |
0e9756e to
6724be5
Compare
6724be5 to
20ef094
Compare
977534a to
4852673
Compare
4852673 to
58798de
Compare
|
@sdmg15 thanks for updates! |
58798de to
72656ec
Compare
va-an
left a comment
There was a problem hiding this comment.
Thanks for addressing the previous round of feedback. Before tackling more changes - branch has merge conflicts with master, would you mind rebasing first? A few more comments below.
Payment struct design
Payment is shared between two flows with different needs.
In the send flow (process_instructions -> call site in handlers.rs) only receiving_addr is read, everything else is built and discarded. .unwrap() is needed only because the field is Option<Address> despite always being Some here.
In the resolve flow (resolve_dns_recipient) the same struct is filled with the opposite subset: receiving_addr/expected_amount always None, metadata fields populated for display. So every Option<T> here is "always Some" in one flow and "always None" in the other - the type system encodes nullability that doesn't actually exist in either path.
Suggested split:
// resolve flow - display only
pub struct ResolvedPaymentInfo {
pub hrn: String,
pub payment_methods: Vec<PaymentMethod>,
pub description: Option<String>,
pub min_amount: Option<Amount>,
pub max_amount: Option<Amount>,
pub notes: String,
}
// `display(pretty)` moves here
// send flow - return what's actually used
pub async fn process_instructions(...) -> Result<(Address, Amount), Error>This resolves several inline comments:
- unwrap on
receiving_addr - the
FixedAmountamount 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.
| 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))) | ||
| } | ||
| }); |
There was a problem hiding this comment.
Consider map + collect here - drops the mutable accumulator and lets match return the String directly. Would need PaymentMethod::* in the use block.
| let mut methods: Vec<String> = Vec::new(); | |
| self.payment_methods.iter().for_each(|pm| match pm { | |
| bitcoin_payment_instructions::PaymentMethod::LightningBolt11(bolt11) => { | |
| methods.push(format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15))) | |
| } | |
| bitcoin_payment_instructions::PaymentMethod::LightningBolt12(offer) => { | |
| methods.push(format!("Bolt 12 invoice ({})", shorten(offer, 20, 15))) | |
| } | |
| bitcoin_payment_instructions::PaymentMethod::OnChain(address) => { | |
| methods.push(format!("On chain ({})", address)) | |
| } | |
| bitcoin_payment_instructions::PaymentMethod::Cashu(csh) => { | |
| methods.push(format!("Cashu payment ({})", shorten(csh, 20, 15))) | |
| } | |
| }); | |
| let methods: Vec<String> = self | |
| .payment_methods | |
| .iter() | |
| .map(|pm| match pm { | |
| LightningBolt11(bolt11) => { | |
| format!("Bolt 11 invoice ({})", shorten(bolt11, 20, 15)) | |
| } | |
| LightningBolt12(offer) => format!("Bolt 12 invoice ({})", shorten(offer, 20, 15)), | |
| OnChain(address) => format!("On chain ({})", address), | |
| Cashu(csh) => format!("Cashu payment ({})", shorten(csh, 20, 15)), | |
| }) | |
| .collect(); |
| 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))) | ||
| } |
There was a problem hiding this comment.
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.
| 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")] |
There was a problem hiding this comment.
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->resolverhandlers.rs::handle_resolve_dns_recipient_command(... resolver: String ...)dns_payment_instructions.rs::resolve_dns_recipient(... ip: String)dns_payment_instructions.rs::parse_dns_instructions(... resolver_ip: String)
Three different names for the same value makes the flow harder to follow. Use dns_resolver everywhere - both as the CLI flag (--dns_resolver) and as the field/parameter name across all functions.
| pub(crate) async fn parse_dns_instructions( | ||
| hrn: &str, | ||
| network: Network, | ||
| resolver_ip: String, |
There was a problem hiding this comment.
Take &str instead of String - format! already allocates, and it removes the need for dns_resolver.clone() at the call site.
| use crate::dns_payment_instructions::{ | ||
| parse_dns_instructions, process_instructions, | ||
| }; |
There was a problem hiding this comment.
Please move this use to the top of the file, next to the existing dns_payment_instructions import on line 16.
| .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)); |
There was a problem hiding this comment.
Two issues here:
-
amountis the user-supplied value, notpayment.expected_amount. ForFixedAmountinstructions the recipient sets an exact amount, and the tx silently goes out with whatever the user typed instead. -
.unwrap()relies onprocess_instructionsalways settingreceiving_addr: Some(...)- an implicit invariant across two files.
Both resolved by the structural change in the top-level comment.
|
@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. |
Description
This PR adds support for BIP353 payment instructions.
The following notable changes are done:
resolve_dns_recipientwhich receives a Human Readable Name (HRN) and returns the associated addresscreate_txfor it to support user specifying recipients asHRN.Notes to the reviewers
Here is a list of HRN that could be used to test the implementation:
bdk-cli -n bitcoin resolve_dns_recipient send.some@satsto.mebdk-cli -n bitcoin wallet -w regtest_default_wallet create_tx --to "bcrt1qe497k549sgw9trzym50tdlegq3xx4apjqynjqm:1000" --to_dns "send.some@satsto.me:5000"Changelog notice
resolve_dns_recipientto resolve the DNS payment instructions--to_dnstocreate_txcommand to allow sending to a Human Readable Name (HRN)Checklists
All Submissions:
cargo fmtandcargo clippybefore committingNew Features:
CHANGELOG.md