Skip to content

Support BOLT12 offers for submarine swaps - #224

Closed
luckysori wants to merge 8 commits into
masterfrom
feat/bolt12
Closed

Support BOLT12 offers for submarine swaps#224
luckysori wants to merge 8 commits into
masterfrom
feat/bolt12

Conversation

@luckysori

@luckysori luckysori commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Based on #197.

Now provides an e2e test. I left out the proposed BOLT12 helper for reverse submarine swaps because they were unused.

Summary by CodeRabbit

  • New Features

    • Added BOLT12 Lightning invoice support for submarine swaps and unified handling of BOLT11/BOLT12 invoices.
    • Configurable Boltz BOLT12 API endpoint for clients and new public result types for BOLT12 payments.
  • Bug Fixes

    • Validates Boltz-provided VHTLC/address before persisting or funding swaps.
  • Tests

    • Added unit tests for invoice parsing/serde and an ignored end-to-end BOLT12 submarine swap test.
  • Chores

    • Bumped Lightning invoice dependency to 0.34.0 and declared updated Lightning dependency.

Review Change Stack

@luckysori luckysori self-assigned this May 19, 2026
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Walkthrough

This PR adds BOLT12 invoice parsing and Boltz offer swap flows, introduces a unified LnInvoice (BOLT11|BOLT12), validates derived VHTLC addresses against Boltz responses, exposes BOLT12 types via the public API, and provides an ignored E2E test plus related test updates.

Changes

BOLT12 Invoice and Swap Support

Layer / File(s) Summary
LnInvoice enum and deps
ark-client/Cargo.toml, ark-client/src/boltz.rs
Adds LnInvoice (Bolt11
BOLT11 submarine swap validation
ark-client/src/boltz.rs
Refactors BOLT11 submarine-swap creation/payment to use a shared path, builds reqwest client with timeout, constructs and validates expected VHTLC script/address against Boltz, switches internal request invoice to string, and stores invoices as LnInvoice.
BOLT12 parsing & types
ark-client/src/boltz/bolt12.rs
Implements ParsedBolt12Invoice (bech32 lni HRP parse, Bolt12Invoice conversion, payment-hash), serde as string, Display, and Bolt12SubmarineSwapResult. Adds unit tests for parsing, HRP validation, and serde round-trip.
BOLT12 submarine swap flows
ark-client/src/boltz/bolt12.rs
Adds prepare_bolt12_offer_payment and pay_bolt12_offer; implements create_bolt12_submarine_swap, fetch_bolt12_invoice, and verify_bolt12_invoice_against_offer with Boltz HTTP interactions, preimage/refund derivation, and VHTLC funding returning Bolt12SubmarineSwapResult.
Client API, config, tests, and e2e
ark-client/src/lib.rs, ark-client/src/swap_storage/sqlite.rs, e2e-tests/tests/*
Re-exports BOLT12 types (Bolt12SubmarineSwapResult, LnInvoice, ParsedBolt12Invoice); adds OfflineClient::boltz_bolt12_url override with builder and accessor; updates swap-storage test data to use LnInvoice::Bolt11(...); configures e2e client bolt12 URL and adds an ignored E2E BOLT12 submarine-swap test.
Transport and error formatting tweaks
ark-grpc/src/client.rs, ark-grpc/src/error.rs
Uses Endpoint::new(...) when building tonic endpoint and changes Error::Display to append source errors with ": {source}" formatting.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • arkade-os/rust-sdk#121: Introduces foundational Boltz submarine-swap structures that relate to the unified invoice handling and swap creation changes in this PR.

Suggested reviewers

  • bonomat
  • vincenzopalazzo
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the changeset, which is adding BOLT12 offer support for submarine swaps across multiple modules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bolt12

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Concept ACK — BOLT12 submarine flow looks solid, nothing hard-blocking. CI green (25/25).

Correct:

  • VHTLC address check before funding — and applied to the BOLT11 paths too, good call.
  • LnInvoice stays a bare string on the wire, so old SubmarineSwapData rows still deserialize. No migration needed.
  • Trust model documented honestly.

Issues:

  1. Medium — fetched BOLT12 invoice is never verified against the offer (bolt12.rs:201). Boltz picks the invoice, so it could return one payable to its own node. The one new trust surface here — details in my follow-up comments.
  2. Cleanup — the swap HTTP + VHTLC-check block is triplicated across boltz.rs and bolt12.rs. One shared helper; the address check shouldn't live in 3 places.
  3. CleanupCreateStringInvoiceSubmarineSwapRequest (bolt12.rs:366) duplicates CreateSubmarineSwapRequest. Just make the existing one take invoice: String.
  4. CleanupLnInvoice::payment_hash() (boltz.rs:118) is unused and its Result never fails. Drop it, or return sha256::Hash.
  5. Suggestion — full LDK lightning crate for just Bolt12Invoice is heavy for a wasm target. Probably unavoidable — fine if the team's OK with it.

nit: SubmarineSwapData.preimage_hash doc still says "BOLT11 invoice" (boltz.rs:3924).

Only #1 needs a real decision before mainnet funds — the rest are cleanups.

Comment thread ark-client/src/boltz/bolt12.rs
Comment thread ark-client/src/boltz/bolt12.rs Outdated
Comment thread ark-client/src/boltz/bolt12.rs Outdated
Comment thread ark-client/src/boltz.rs Outdated

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

Follow-up on point 1.

The use case: a user pays a BOLT12 offer, but Boltz is the one resolving that offer into the invoice — so the user pays whatever invoice Boltz returns.

The inline comment below anchors the gap to the code. Verdict is unchanged — point 1 is the call to make before real funds.

Comment thread ark-client/src/boltz/bolt12.rs
pi-chat and others added 8 commits May 26, 2026 15:39
Co-authored-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Parse the offer locally with LDK's Offer::from_str and run three checks
before using the invoice Boltz returns:

1. If the offer has an explicit issuer_signing_pubkey, the invoice's
   signing_pubkey must match it.
2. Otherwise, if the offer has blinded message paths, the invoice's
   signing_pubkey must match the blinded_node_id of the final hop in
   one of those paths.
3. If the invoice carries an offer_id, it must match the offer's own id.

This closes the central trust gap in the Boltz-resolves-offer flow:
Boltz can no longer return an invoice payable to a different node
without detection.
Otherwise HTTPS connections fail!?
Extract the common Boltz submarine swap creation path into a shared helper
used by BOLT11 prepare, BOLT11 pay, and BOLT12 offer flows.

The helper now owns the HTTP request, response parsing, VHTLC script
construction, VHTLC address verification, swap data construction, and
persistence. Callers only provide invoice-specific data and perform their
own post-creation actions.

Also consolidates the submarine swap request to use a string invoice,
which is wire-compatible with the previous BOLT11 request because
Bolt11Invoice serializes as a string.

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

LGTM :)

@ghost ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana Code Review — #224

Verdict: Request changes. This PR touches submarine swap creation and VHTLC construction — protocol-critical paths. The VHTLC address validation is an excellent security addition. However, I found issues in the BOLT12 invoice verification logic that need attention before merge.


🔴 P0 — Blinded-path verification is broken

ark-client/src/boltz/bolt12.rsverify_bolt12_invoice_against_offer, lines ~340–348 (new file)

When the offer has no issuer_signing_pubkey but has blinded message paths, the code checks:

let matches = paths.iter()
    .filter_map(|path| path.blinded_hops().last())
    .any(|last_hop| invoice_signing_pk == last_hop.blinded_node_id);

This comparison is incorrect. In BOLT12, blinded_node_id of the last hop is the blinded version of the recipient's pubkey (computed as b_n * G + real_node_id). The invoice's signing_pubkey() returns the recipient's real node key. These will never match for blinded-path offers.

Impact: All BOLT12 payments to offers that use blinded paths (which is the privacy-preserving default in many LN implementations) will be rejected with a false-positive verification error. The feature is non-functional for this class of offers.

Fix options:

  1. Use LDK's Bolt12Invoice::verify_using_metadata or verify_using_payer_data for proper cryptographic verification (preferred).
  2. If LDK doesn't expose what's needed, log a warning for blinded-path offers instead of rejecting, and document the trust assumption.
  3. At minimum, skip the blinded-path check (it provides a false sense of security since the comparison is wrong) and rely on the offer_id check (Check 3) which IS correct.

🟡 P1 — No invoice amount validation after Boltz fetch

ark-client/src/boltz/bolt12.rsfetch_bolt12_invoice / create_bolt12_submarine_swap

After fetching a BOLT12 invoice from Boltz via bolt12_fetch, the code verifies the signing key and offer_id — but never checks that the invoice amount matches what the user requested.

If a user calls pay_bolt12_offer(offer, Some(Amount::from_sat(2_000))) and Boltz returns an invoice for 200,000 sats, the code will proceed to create a submarine swap for the inflated amount. The VHTLC expected_amount comes from Boltz's response, not from the user's intent.

Suggested fix: After parsing the invoice, verify invoice.invoice().amount_msats() matches the requested amount (or the offer's fixed amount if no user amount was provided). Reject if they diverge beyond a reasonable fee tolerance.


🟡 P1 — Silent accept-all when offer has no signing key and no paths

ark-client/src/boltz/bolt12.rsverify_bolt12_invoice_against_offer, lines ~350–353

// If neither issuer_signing_pubkey nor paths are set, we cannot verify the
// invoice's signing key — the offer is unusually permissive. Accept it.

This means a malformed or stripped offer (no node_id, no paths) bypasses ALL verification. Boltz could return an invoice for any recipient. Consider at minimum logging a warning, or requiring the offer_id check to pass in this case.


🟢 Positive findings

  1. VHTLC address validation (boltz.rs, create_submarine_swap): Locally rebuilding the VHTLC script and verifying the address matches Boltz's response is a significant security improvement. Previously the code trusted Boltz's address blindly. Well done.

  2. Refactoring create_submarine_swap as shared code between BOLT11 and BOLT12 paths eliminates duplication and ensures both paths get the VHTLC validation. Clean.

  3. LnInvoice serde: Custom deserializer that tries BOLT11 first then falls back to BOLT12 is backward-compatible with existing SQLite-stored swap data. No migration needed.

  4. Timeout on HTTP client: Adding self.inner.timeout to reqwest::Client is a good hardening — previously it was unbounded.

  5. ark-grpc error display fix (error.rs:103): Adding the : separator between description and source was a real bug fix.


📝 Minor / Nits

  • No unit tests for verify_bolt12_invoice_against_offer: This is the most security-critical function in the PR. It deserves test coverage for each branch: matching issuer_signing_pubkey, matching blinded path, mismatching keys, offer_id mismatch, and the no-key-no-paths fallthrough.

  • e2e-tests/tests/boltz_bolt12_submarine.rs:43: wait_until_balance!(&alice, confirmed: Amount::ZERO, pre_confirmed: alice_fund_amount - res.amount) — this doesn't account for Ark round fees. May be flaky. Consider using a tolerance or >= check.


⚠️ Protocol-critical flag

This PR modifies submarine swap creation, VHTLC script construction, and adds a new payment flow. Requires human review and sign-off before merge, per protocol-critical code policy.


🤖 Reviewed by Arkana (#224)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ark-client/src/boltz/bolt12.rs`:
- Around line 323-341: The code currently accepts invoices when neither
issuer_signing_pubkey nor offer.paths() are present, allowing unauthenticated
invoice signers; change this to fail closed by returning an Err instead of
permissive success: in the branch where issuer_signing_pubkey is None and
paths.is_empty(), return Error::ad_hoc with a descriptive message (mentioning
invoice_signing_pk and that neither issuer_signing_pubkey nor blinded paths are
present) so verification cannot proceed without an authenticated signer; update
the logic around offer.paths(), invoice_signing_pk, and Error::ad_hoc to enforce
this rejection.
- Around line 240-291: The fetched/parsed BOLT12 invoice must be checked to
ensure its amount equals the caller-supplied amount or, when amount was None,
equals the fixed amount encoded by the offer; update fetch_bolt12_invoice to
read the resolved invoice amount from ParsedBolt12Invoice (e.g., invoice.amount
or equivalent) and compare it to the requested Amount (amount parameter) or to
the offer's fixed amount (derive from the offer if amount.is_none()), and if
they differ return an error (use Error::ad_hoc with a clear message) instead of
proceeding to verify_bolt12_invoice_against_offer and creating the swap.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9408be03-173e-4b39-82e4-673cfaf41cd2

📥 Commits

Reviewing files that changed from the base of the PR and between e7d0f1d and 1d33d77.

📒 Files selected for processing (9)
  • ark-client/Cargo.toml
  • ark-client/src/boltz.rs
  • ark-client/src/boltz/bolt12.rs
  • ark-client/src/lib.rs
  • ark-client/src/swap_storage/sqlite.rs
  • ark-grpc/src/client.rs
  • ark-grpc/src/error.rs
  • e2e-tests/tests/boltz_bolt12_submarine.rs
  • e2e-tests/tests/common.rs
✅ Files skipped from review due to trivial changes (2)
  • ark-client/Cargo.toml
  • ark-grpc/src/error.rs

Comment on lines +240 to +291
async fn fetch_bolt12_invoice(
&self,
offer: &str,
amount: Option<Amount>,
) -> Result<ParsedBolt12Invoice, Error> {
let request = Bolt12FetchInvoiceRequest {
offer: offer.to_string(),
amount: amount.map(|amount| amount.to_sat()),
};

let url = format!(
"{}/v2/lightning/BTC/bolt12/fetch",
self.inner.boltz_bolt12_url().trim_end_matches('/')
);
let client = reqwest::Client::builder()
.timeout(self.inner.timeout)
.build()
.map_err(|e| Error::ad_hoc(e.to_string()))?;
let response = client
.post(&url)
.json(&request)
.send()
.await
.map_err(|e| Error::ad_hoc(e.to_string()))
.context("failed to fetch bolt12 invoice from offer")?;

if !response.status().is_success() {
let error_text = response
.text()
.await
.map_err(|e| Error::ad_hoc(e.to_string()))
.context("failed to read bolt12_fetch error text")?;

return Err(Error::ad_hoc(format!(
"failed to fetch bolt12 invoice from offer: {error_text}"
)));
}

let response: Bolt12FetchInvoiceResponse = response
.json()
.await
.map_err(|e| Error::ad_hoc(e.to_string()))
.context("failed to deserialize bolt12_fetch response")?;
let invoice = ParsedBolt12Invoice::parse(response.invoice)
.context("bolt12_fetch returned invalid BOLT12 invoice")?;

// Verify the invoice matches the offer before using it.
verify_bolt12_invoice_against_offer(offer, &invoice)?;

tracing::info!("Fetched and verified BOLT12 invoice from offer");

Ok(invoice)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bind the resolved invoice amount to the requested amount.

This only authenticates who signed the invoice and which offer it references. A buggy or malicious bolt12_fetch endpoint can still return a valid invoice for a higher amount from the same offer, and this flow will create/fund the swap for that value. Please reject any fetched invoice whose amount does not exactly match the caller-supplied amount, or the fixed amount encoded by the offer when amount is None.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ark-client/src/boltz/bolt12.rs` around lines 240 - 291, The fetched/parsed
BOLT12 invoice must be checked to ensure its amount equals the caller-supplied
amount or, when amount was None, equals the fixed amount encoded by the offer;
update fetch_bolt12_invoice to read the resolved invoice amount from
ParsedBolt12Invoice (e.g., invoice.amount or equivalent) and compare it to the
requested Amount (amount parameter) or to the offer's fixed amount (derive from
the offer if amount.is_none()), and if they differ return an error (use
Error::ad_hoc with a clear message) instead of proceeding to
verify_bolt12_invoice_against_offer and creating the swap.

Comment on lines +323 to +341
} else {
// Check 2: offer has blinded paths — verify invoice signing key is the
// final hop in one of them.
let paths = offer.paths();
if !paths.is_empty() {
let matches = paths
.iter()
.filter_map(|path| path.blinded_hops().last())
.any(|last_hop| invoice_signing_pk == last_hop.blinded_node_id);
if !matches {
return Err(Error::ad_hoc(format!(
"BOLT12 invoice signing pubkey ({invoice_signing_pk}) does not match \
the final hop of any blinded message path in the offer",
)));
}
}
// If neither issuer_signing_pubkey nor paths are set, we cannot verify the
// invoice's signing key — the offer is unusually permissive. Accept it.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when the offer cannot authenticate the invoice signer.

If the offer has neither issuer_signing_pubkey nor blinded paths, this branch accepts the invoice without any authenticated signer binding. If the returned invoice also omits offer_id, Boltz can substitute an arbitrary invoice and still pass verification. That should be rejected rather than treated as a permissive success case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ark-client/src/boltz/bolt12.rs` around lines 323 - 341, The code currently
accepts invoices when neither issuer_signing_pubkey nor offer.paths() are
present, allowing unauthenticated invoice signers; change this to fail closed by
returning an Err instead of permissive success: in the branch where
issuer_signing_pubkey is None and paths.is_empty(), return Error::ad_hoc with a
descriptive message (mentioning invoice_signing_pk and that neither
issuer_signing_pubkey nor blinded paths are present) so verification cannot
proceed without an authenticated signer; update the logic around offer.paths(),
invoice_signing_pk, and Error::ad_hoc to enforce this rejection.

@vincenzopalazzo

Copy link
Copy Markdown
Collaborator

🔴 P0 — Blinded-path verification is broken

AI is right here but probably not the case of boltz issuer_signing_pubkey is always specified. On the other hand if we try to pay the bolt12 we may need some signature verification more strong, I should have a snipped of the code if you want

🟡 P1 — Silent accept-all when offer has no signing key and no paths

This sounds wrong

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PROTOCOL-CRITICAL: human review required.

Reviewed at head 1d33d77. The BOLT12 flow closes one important trust gap (invoice signing key ↔ offer) but leaves another wide open: the amount is entirely Boltz-controlled and never cross-checked, and pay_bolt12_offer funds the VHTLC without user confirmation. Details below.

Blocking — fund-loss vector: no amount validation on the fetched invoice

ark-client/src/boltz/bolt12.rs:200-219 fetches an invoice from Boltz, then hands it straight to create_submarine_swap. create_submarine_swap (ark-client/src/boltz.rs:305-340) trusts swap_response.expected_amount verbatim and writes it into data.amount. pay_bolt12_offer at bolt12.rs:170-192 then calls self.send(vec![SendReceiver::bitcoin(vhtlc_address, amount)]) with zero user interaction.

Consequence: a compromised or malicious Boltz bolt12/fetch endpoint can return an invoice for any amount it likes for the user's offer. When user calls pay_bolt12_offer(offer, Some(2_000)), Boltz can reply with an invoice for, say, 500_000 sats; expected_amount will reflect that (plus fees); the user's wallet drains up to whatever it can spend. Even a partially-compromised Boltz can inflate the delta between invoice.amount_msats() and expected_amount and pocket the difference on top of a legitimate invoice. verify_bolt12_invoice_against_offer at bolt12.rs:307-355 only covers signing-key/offer_id — an invoice for the right node with the wrong amount still passes.

Minimum required checks in fetch_bolt12_invoice (or right after it):

  • If amount was supplied: assert invoice.invoice().amount_msats() == amount * 1000 (or explicit tolerance).
  • If offer fixes an amount: assert invoice_amount_msats == offer.amount().
  • After create_submarine_swap: bound swap_response.expected_amount - invoice_amount_sats against an explicit fee ceiling (either configurable or a hard % cap), reject otherwise.

The BOLT11 flow gets away with skipping the second/third of these because the user supplied the invoice, so they know its amount before calling. BOLT12 has no such anchor.

High — verify function accepts offers with neither issuer key nor paths

bolt12.rs:339-340:

// If neither issuer_signing_pubkey nor paths are set, we cannot verify the
// invoice's signing key — the offer is unusually permissive. Accept it.

Silently accepting means the entire binding between offer and invoice collapses; Boltz can return any invoice. BOLT12 requires at least one of the two, so rejecting is spec-conformant. Please turn this branch into an Err.

High — no invoice expiry check

Nothing consults invoice.invoice().relative_expiry() / created_at. Boltz can return a long-expired invoice; the VHTLC gets funded and then just waits until the refund timeout because Boltz cannot route the payment. The refund path exists so funds aren't lost, but this is easy to catch at fetch time and should be.

Medium — verify_bolt12_invoice_against_offer has zero adversarial test coverage

The three tests in bolt12.rs:369-400 only cover happy-path parse, wrong-HRP parse, and the LnInvoice untagged roundtrip. The function that stops a substituted invoice has no tests for:

  • mismatched signing_pubkey vs issuer_signing_pubkey
  • signing_pubkey not matching any blinded-path last hop
  • offer_id mismatch
  • the permissive no-issuer/no-paths branch (which should be a failure once the item above is addressed)

This is the security check on a fund-flow path — please add unit tests before merge.

Medium — confirm the units on Bolt12FetchInvoiceRequest.amount

bolt12.rs:361: amount: Option<u64> populated with amount.to_sat(). BOLT12 uses msats everywhere natively; the units on Boltz's endpoint aren't obvious from the field name. If it turns out Boltz expects msat here, the request will 1000×-underpay and the invoice will come back for a rounding-error amount. Please confirm against Boltz's OpenAPI and add a comment locking in the units.

Low / nits

  • SubmarineSwapData.invoice at boltz.rs:3923 changes from Bolt11Invoice to LnInvoice. This is a semver-major break for any external Rust consumer that matches/destructures the field. SQLite backward-compat is fine (untagged serde tries Bolt11 first; prefixes are disjoint). Just call the break out in the changelog.
  • boltz_bolt12_url is only consulted for bolt12/fetch (bolt12.rs:252). The subsequent POST /v2/swap/submarine still uses boltz_url at boltz.rs:268. In the e2e-tests setup you point at :9005 for BOLT12 and :9001 for the rest, which implies two Boltz deployments in play. If that split is intentional, please document it on with_boltz_bolt12_url; if not, route the swap creation through the same URL when BOLT12 is in use.
  • prepare_bolt12_offer_payment docstring at bolt12.rs:108-132 tells users to send amount to vhtlc_address without warning that data.amount was chosen by Boltz on the basis of a Boltz-fetched invoice. Callers building UIs on top of this deserve an explicit "verify before funding" note (or, better, the amount-validation checks above so they don't have to).
  • ark-grpc/src/client.rs:120 swaps Endpoint::from_shared(self.url.clone()) for Endpoint::new(self.url.clone()). Unrelated to the BOLT12 work, uncommented, and touches connect-time transport setup. from_shared(String) is stable; new goes through TryInto<Endpoint> (URI parsing). Behaviorally likely identical for valid URIs but the commit message doesn't say why. A one-line rationale (or reverting and doing it separately) would help.
  • ark-grpc/src/error.rs:103 — the ": " separator fix is correct; before, Display was jamming description and source together as one word.

@luckysori

Copy link
Copy Markdown
Collaborator Author

No longer relevant.

@luckysori luckysori closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants