Skip to content

feat(wallet): pay CLINK noffer offer strings from the Send field#429

Merged
spe1020 merged 3 commits into
zapcooking:mainfrom
dmnyc:feat/wallet-noffer-send
Jun 11, 2026
Merged

feat(wallet): pay CLINK noffer offer strings from the Send field#429
spe1020 merged 3 commits into
zapcooking:mainfrom
dmnyc:feat/wallet-noffer-send

Conversation

@dmnyc

@dmnyc dmnyc commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

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

  • Detection: the Send input recognizes noffer1… strings (pasted or QR-scanned; the scanner already strips lightning: URIs to the bare token) and shows a "⚡ CLINK offer detected" hint — including the price when the offer is Fixed (TLV-4).
  • Payment path: a dedicated handleSendNoffer() runs the CLINK kind-21001 RPC (decodeNoffer + requestInvoice — the same machinery NofferPayModal uses) 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.
  • Errors: typed NofferError codes 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).
  • Help text: field label now reads "Invoice, Lightning Address, or CLINK Offer" (plus Bitcoin Address on Spark), and the placeholder includes noffer1....

The wallet history row carries the offer id in its description and the service pubkey in metadata.

Test plan

  • Paste a noffer1… string into Send → "CLINK offer detected" hint appears
  • Pay a Spontaneous offer with a chosen amount → invoice fetched via kind-21001 RPC and paid (verified live: 10-sat and 1,000-sat payments to a Zeus offer, fees 4/7 sats, correct history rows)
  • Amount required for non-Fixed offers (blocks send at 0)
  • bolt11 / Lightning address / on-chain send paths unchanged

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 via walletManager.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.

Comment on lines +1783 to +1802
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;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 341f329 — the Spark label now reads "Invoice, Lightning Address, Bitcoin Address, or CLINK Offer".

Comment on lines +5317 to +5319
CLINK offer detected{nofferData.pricingType === 'fixed' && nofferData.price
? ` · ${nofferData.price.toLocaleString()} sats`
: ''}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 341f329 — the price hint now checks nofferData.price != null so a 0-sat price isn't suppressed.

Comment on lines +1762 to +1766
const result = await sendPayment(bolt11, {
amount: effectiveAmount,
description: `CLINK offer: ${data.offerId}`,
pubkey: data.pubkey
});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

dmnyc and others added 2 commits June 11, 2026 12:04
…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.
@spe1020 spe1020 merged commit f8d1fa6 into zapcooking:main Jun 11, 2026
4 checks passed
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.

3 participants