RGB-Tools / RGB-Tools/rgb-lightning-node

Feature request: BOLT12 offer support (create + decode)

Open
#130 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Rust
Stars
50
Forks
48
Avg merge
8h 52m
Merged PRs (30d)
2

Description

Summary

rgb-lightning-node can already pay a BOLT12 offer (/sendpayment accepts an
lno1… string and routes it through LDK's pay_for_offer), but it cannot
create or inspect one. This proposal adds the two missing halves so a node
can act as a BOLT12 payee:

  • POST /lnoffer — create a reusable BOLT12 offer.
  • POST /decodeoffer — decode/inspect an offer (amount, description, expiry, chains, …).

Together with the existing pay path this closes the BOLT12 loop (create → decode →
pay → receive) for plain BTC payments.

Motivation

BOLT12 offers are static, reusable payment instructions: one lno1… string can be
published once (QR, website, BIP-353 DNS record) and paid many times, with payer
privacy via blinded paths and no LNURL/HTTP callback server. For an RGB Lightning
node this unlocks:

  • Reusable receive — today receiving requires minting a fresh single-use BOLT11
    per payment. An offer is mint-once, pay-many.
  • BIP-353 human-readable addresses — a DNSSEC bitcoin:?lno=… record needs a
    static offer to point at; that is only possible once the node can emit one.
  • Ecosystem parity — LDK, CLN, LND (experimental) and most modern wallets now
    speak BOLT12; an RLN node should be a first-class BOLT12 payee.

Proposed API

POST /lnoffer

Request:

field type notes
amt_msat u64? omit for an amount-less ("any amount") offer
description string? defaults to empty
issuer string? optional issuer metadata
expiry_sec u32? relative expiry; absolute timestamp stored
supported_quantity_max u64? None=1, 0=unbounded, n=bounded
asset_id string? reserved — rejected for now (see follow-up)
asset_amount u64? reserved — rejected for now (see follow-up)

Response: { "offer": "lno1…" }.

asset_id / asset_amount are present in the request schema but currently return
400 "RGB assets are not yet supported in BOLT12 offers", so the wire format is
forward-compatible with the follow-up without a breaking change.

POST /decodeoffer

Request: { "offer": "lno1…" }. Response: amount, description, issuer, expiry,
is_expired, supported chains, supported_quantity_max, issuer signing pubkey,
blinded-path count, and offer_id.

Paying

Unchanged — /sendpayment already accepts an offer string and pays it via
pay_for_offer.

Scope of the first PR (BTC only)

  • Add /lnoffer and /decodeoffer handlers + routes.
  • Register /decodeoffer as a read-only op (auth).
  • Track an explicit payment_hash for outbound payments (offer payments do not
    expose the hash up front: payment_id != payment_hash), so /getpayment and
    /listpayments resolve the RGB payment-info file and report status correctly.
  • OpenAPI docs + an offer_roundtrip integration test (create → decode → pay →
    receive over a regtest channel).
Backward compatibility

The new payment_hash field is appended to the PaymentInfo TLV stream at the
next free type id (16, payment_hash, option); existing type ids are unchanged, so
already-persisted outbound payments continue to deserialize. (An earlier draft
renumbered the existing fields — that would have corrupted on-disk payment state on
upgrade and is avoided here.)

Out of scope → follow-up PR: RGB assets in BOLT12

Carrying an RGB asset id + amount inside a BOLT12 offer (and honoring it on the
receive/HTLC path) is deliberately not in this PR. It needs its own design:

  • offer-level encoding of (contract_id, amount) (custom TLV vs. a convention),
  • RGB payment-info plumbing for offer-initiated inbound HTLCs,
  • quantity × asset-amount semantics,
  • interop expectations with non-RGB BOLT12 wallets (graceful ignore vs. reject).

The request schema already reserves asset_id / asset_amount so that follow-up is
additive. Tracked separately.

Testing

  • cargo test offer_roundtrip (regtest services via ./regtest.sh start).
  • Manual: /lnoffer on node A → /decodeoffer + /sendpayment on node B →
    assert Succeeded on both sides.

Open questions

  1. Should amount-less offers be allowed by default, or require an explicit opt-in?
  2. Default offer expiry when expiry_sec is omitted — none (never expires) or a
    sane default?
  3. Do we want a /listoffers / offer-persistence story, or are offers fire-and-forget
    (caller stores the string)?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the existing /sendpayment path and the /getpayment and /listpayments handling, then trace the PaymentInfo TLV stream and the proposed /lnoffer and /decodeoffer entry points. Run ./regtest.sh start followed by cargo test offer_roundtrip; done means the BTC-only create → decode → pay → receive flow works, OpenAPI docs are updated, and existing PaymentInfo data remains compatible.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend, payments
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.