feat(wallet): pay CLINK noffer offer strings from the Send field#429
Conversation
The wallet Send field only handled bolt11 invoices, Lightning addresses, and on-chain Bitcoin addresses. A pasted (or QR-scanned) noffer1… offer string fell through to a failed bolt11 payment. Detect noffer strings in the Send input, decode them, and route through a new handleSendNoffer() path that runs the CLINK kind-21001 RPC to fetch a bolt11 and pays it via the active wallet — reusing the same decodeNoffer/requestInvoice machinery as NofferPayModal. Fixed offers pay their TLV-4 price; Variable/Spontaneous offers prompt for an amount. Friendly NofferError mapping mirrors the modal.
There was a problem hiding this comment.
Pull request overview
Adds first-class support for paying CLINK noffer1… offer strings from the wallet “Send” input by detecting offers, requesting a bolt11 via kind-21001 RPC, and then paying via the active wallet.
Changes:
- Detect
noffer1…strings in the Send field, decode them for UI state, and show an “offer detected” hint (including fixed price when present). - Add a dedicated
handleSendNoffer()path that requests an invoice from the offer’s relay (kind-21001) and pays it viawalletManager.sendPayment, with friendly error mapping. - Update Send field label/placeholder copy to mention CLINK offers; bump package version.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/components/wallet/WalletPanel.svelte | Adds noffer detection + decoding, a new offer payment flow, UI hints, and updated Send button/labels. |
| package.json | Bumps app version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch (e.code) { | ||
| case 1: | ||
| sendError = 'This offer is no longer valid.'; | ||
| break; | ||
| case 2: | ||
| sendError = "The service couldn't complete the request. Try again in a moment."; | ||
| break; | ||
| case 3: | ||
| sendError = e.latest | ||
| ? 'This offer has a newer version. Ask the recipient for an updated offer.' | ||
| : 'This offer has been retired by the recipient.'; | ||
| break; | ||
| case 5: | ||
| sendError = e.range | ||
| ? `Pick an amount between ${e.range.min.toLocaleString()} and ${e.range.max.toLocaleString()} sats.` | ||
| : "The amount is outside the offer's allowed range."; | ||
| break; | ||
| default: | ||
| sendError = e.message; | ||
| } |
There was a problem hiding this comment.
Fixed in 341f329 — code 4 now maps to the same copy as NofferPayModal: "The service doesn't support this kind of payment request."
| <label class="block text-sm font-medium mb-2 text-caption"> | ||
| {#if $activeWallet?.kind === 4} | ||
| Invoice, Lightning Address, or Bitcoin Address | ||
| Invoice, Lightning or Bitcoin Address, or CLINK Offer |
There was a problem hiding this comment.
Fixed in 341f329 — the Spark label now reads "Invoice, Lightning Address, Bitcoin Address, or CLINK Offer".
| CLINK offer detected{nofferData.pricingType === 'fixed' && nofferData.price | ||
| ? ` · ${nofferData.price.toLocaleString()} sats` | ||
| : ''} |
There was a problem hiding this comment.
Fixed in 341f329 — the price hint now checks nofferData.price != null so a 0-sat price isn't suppressed.
| const result = await sendPayment(bolt11, { | ||
| amount: effectiveAmount, | ||
| description: `CLINK offer: ${data.offerId}`, | ||
| pubkey: data.pubkey | ||
| }); |
There was a problem hiding this comment.
Good catch — fixed in 341f329. The amount passed to sendPayment is now decoded from the service's bolt11 (same extraction pattern as zapAmount.ts), so the Spark override always matches the invoice exactly. The entered/TLV-4 amount is only a fallback when the invoice is amountless, where Spark requires an explicit amount anyway. History metadata also gets the accurate invoice amount as a side benefit.
…bel/price polish - Trust the service's bolt11 for the payment amount instead of the offer's TLV-4 price: Spark passes metadata.amount as an explicit override when paying a bolt11, so a stale Fixed-offer price could mismatch the invoice. The entered/TLV amount is now only a fallback for amountless invoices (which Spark needs an explicit amount for). - Map NofferError code 4 (unsupported feature) to the same friendly copy as NofferPayModal. - Spark label reads "Invoice, Lightning Address, Bitcoin Address, or CLINK Offer" — no shared-noun ellipsis. - Fixed-offer price hint uses a null check so a 0-sat price isn't suppressed.
The wallet's Send field only handled bolt11 invoices, Lightning addresses, and on-chain Bitcoin addresses — a pasted (or QR-scanned)
noffer1…CLINK offer string fell through to a failed bolt11 payment.What's new
noffer1…strings (pasted or QR-scanned; the scanner already stripslightning:URIs to the bare token) and shows a "⚡ CLINK offer detected" hint — including the price when the offer is Fixed (TLV-4).handleSendNoffer()runs the CLINK kind-21001 RPC (decodeNoffer+requestInvoice— the same machineryNofferPayModaluses) against the offer's relay to fetch a bolt11, then pays it through the active wallet. Fixed offers pay their embedded price; Variable/Spontaneous offers require an amount in the existing Amount field.NofferErrorcodes map to the same friendly copy as the modal (expired offer, retired offer with/without replacement, amount out of range with the allowed min/max, service failure, timeout).noffer1....The wallet history row carries the offer id in its description and the service pubkey in metadata.
Test plan
noffer1…string into Send → "CLINK offer detected" hint appears