RGB-Tools / RGB-Tools/rgb-lightning-node
Feature request: BOLT12 offer support (create + decode)
Nobody has claimed this yet.
- 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
/lnofferand/decodeofferhandlers + routes. - Register
/decodeofferas a read-only op (auth). - Track an explicit
payment_hashfor outbound payments (offer payments do not
expose the hash up front:payment_id != payment_hash), so/getpaymentand
/listpaymentsresolve the RGB payment-info file and report status correctly. - OpenAPI docs + an
offer_roundtripintegration 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:
/lnofferon node A →/decodeoffer+/sendpaymenton node B →
assertSucceededon both sides.
Open questions
- Should amount-less offers be allowed by default, or require an explicit opt-in?
- Default offer expiry when
expiry_secis omitted — none (never expires) or a
sane default? - 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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