Commit Graph
14827 Commits
Author SHA1 Message Date
Claude 2635bd90a5 feat(cli): zap --with <ndebit> settles the invoice via CLINK debit
amy zap printed the invoice but never paid it. With --with <ndebit> it now
settles the fetched BOLT-11 in-place through a CLINK debit pointer (kind-21002,
reusing DebitCommands.settle), mirroring how the app routes a zap through its
default payment source. Works for both single-recipient (zap user) and
split zaps (zap event) — each recipient reports paid + preimage (or pay_error).

Adds a --with validation case to the headless harness; 16/16 pass.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 21:27:36 +00:00
Claude e77658c292 feat(cli): close CLINK parity gaps — profile offer, follow, offer pay, GFY detail
Brings amy's CLINK surface closer to the app's:

- profile edit --clink-offer <noffer|"">: set/clear the kind-0 clink_offer
  (validated as a real noffer; "" clears). MetadataEvent already carried the field.
- offer request --follow: chase an 'Expired or Moved' (code 3) reply to its
  'latest' pointer (bounded hops), mirroring the app; the error output now also
  carries code/latest/range so a script can follow or correct manually.
- offer pay <noffer> --with <ndebit> [--amount]: end-to-end — fetch the invoice
  (21001) and settle it through a debit pointer (21002), reusing DebitCommands.settle.
- Structured GFY detail (code, range, retry_after, delta) in debit/offer errors,
  via a new Output.error(extra=) overload.

Adds local-validation cases to the headless harness (offer pay --with, profile
edit --clink-offer); 15/15 pass.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 21:23:51 +00:00
Claude 5dadff0315 fix(clink): label the offer chip/card 'CLINK Offer'
https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 20:58:54 +00:00
Claude 01cb45cb31 feat(clink): render profile offer as a tappable chip, not an always-on card
The profile CLINK offer showed the full ClinkOfferPreview payment card up front.
Render it instead as a compact payment-target-style chip (Bolt icon + 'Lightning
Offer' label, matching the PaymentTargetChip look); tapping it expands the
payable card, collapsed by default — same expand-on-click idiom as the lightning
address row.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 20:55:21 +00:00
Claude 1490e7a030 fix(zap): thread-safe progress accumulator for both pay rails
progressAllPayments was a non-atomic Float var incremented from the concurrent
mapNotNullAsync bodies AND the async response callbacks (NWC onResponse / the
CLINK launched coroutine), so parallel zap splits raced and could leave the
progress bar below 100%. Replace it with a shared PaymentProgress(AtomicInteger
over 2*N half-steps) used by both payViaNWC and payViaClinkDebit, which also
removes the duplicated half-step arithmetic.

Note: NWC's response half-step still won't fire if a wallet never replies within
its 60s window (sendZapPaymentRequestFor doesn't signal onResponse on timeout);
that progress-stall is pre-existing and separate from this race fix.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 19:16:59 +00:00
Claude 7e7898bf77 refactor(clink): audit follow-ups — consistent error detail, non-null priceType, budget guard
From the audit of this session's changes:

- Error surfacing: the budget (WalletScreen) and offer/invoice card
  (InvoicePaymentDispatcher) paths now use DebitResponse.failureDetail() like the
  zap path, so a GFY code-5/code-4 surfaces its range/retry_after instead of just
  the bare error string.
- NOffer.priceType is now non-null: decode already defaults an absent TLV 3 to
  SPONTANEOUS, so the nullable type was misleading and the '?: SPONTANEOUS'
  fallbacks in ClinkOfferPreview were dead. Drops them and the now-redundant
  always-emit-TLV3 test (covered by the spontaneous round-trip).
- WalletViewModel.requestDebitBudget catches the budget-validation
  IllegalArgumentException so a malformed frequency dismisses the dialog instead
  of hanging the spinner.
- Document why ClinkDebitPayer signs with the persistent account key (stable
  identity for budgets) while ClinkOfferPayer uses an ephemeral key.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 18:55:57 +00:00
Claude 33f1d20a21 fix(clink): mirror NWC's split progress on the debit zap rail
Advance half the per-payable progress on dispatch and the other half when the
async debit response arrives, exactly like payViaNWC, instead of jumping the
full share on dispatch.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 18:29:46 +00:00
Claude 15b12e7f13 refactor(clink): make the debit zap rail fire-and-forget like NWC
payViaClinkDebit blocked the zap on the debit service's res:ok/GFY reply (up to
30s) before reporting a result. Mirror the NWC rail instead: dispatch the debit
on the account scope and report each payable paid optimistically so the zap UI
completes promptly; a GFY/failure (or no reply) surfaces asynchronously through
onError rather than blocking. The programmatic App Functions debit path is
unchanged (it still awaits the real result).

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 17:54:09 +00:00
Claude 61387ba12c docs(clink): record interop review + spec-conformance pass results
https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 17:40:29 +00:00
Claude f4e0bcf73d fix(clink): spec-conformance hardening (k1 length, frequency, description, GFY detail)
Follow-ups from the line-by-line spec audit, scoped to the consume-only client:

- NDebit.parse rejects a TLV-3 session id that isn't exactly 32 bytes (64 hex),
  per clink-debits: a wrong-length k1 is a malformed session pointer.
- DebitClient.requestBudget validates frequency.unit is one of day/week/month
  (DebitFrequency.VALID_UNITS) instead of sending a unit a node service will GFY.
- OfferClient caps the invoice description at 100 chars per clink-offers.
- DebitResponse.failureDetail() composes the GFY error with its actionable extra
  (allowed range for code 5, retry_after for code 4); the debit zap path now
  surfaces that instead of the bare error string.

Adds regression tests for each (malformed-k1 rejection, invalid-unit throw,
description truncation, failureDetail range/retry_after).

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 16:44:00 +00:00
Claude c78ba27c86 test(clink): correct provenance of the default-offer interop vector
The vector is the canonical @shocknet/clink-sdk example — both its MIT README
usage snippet and clink-demo's public-domain DEFAULT_NOFFER are the same string.
Confirmed the published npm tarball ships only build output (no test vectors), so
this is the one real codec vector the ecosystem exposes.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 16:15:25 +00:00
Claude eb7b3bad07 test(clink): golden wire-shape fixtures from the public-domain specs
Adds ClinkWireShapeTest: the literal decrypted JSON payload bodies documented in
shocknet/CLINK/specs/clink-{offers,debits,manage}.md (public domain) must
deserialize into our DTOs with the right fields. Covers the encrypted-content
half the bech32 pointer vectors don't: offer request + success/error codes 1-5
(incl. code-3 latest, code-5 range) + receipts; debit direct/budget requests,
success, and GFY 1-6 (incl. delta, retry_after, range); manage nested
offer.fields requests and responses — including the single-object 'details'
coercing to a list, which exercises the Manage list/single interop fix.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 15:46:51 +00:00
Claude 5e465410b2 test(clink): add clink-demo DEFAULT_NOFFER interop vector
The clinkme.dev demo (shocknet/clink-demo, public domain) hard-codes a live
default noffer. Adds it as an 8th cross-impl vector — a real-world spontaneous,
relay-bearing, no-price offer with a 64-char-hex offer-id — decoded and
round-tripped through our parser.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 15:26:03 +00:00
Claude f9ed2e0ab7 feat(clink): close ecosystem interop gaps (manage list, TLV3, NIP-05, receipts, payer privacy)
Interop review against the shocknet/CLINK ecosystem (Lightning.Pub, clink-sdk,
ShockWallet, Zeus, Stacker News, bridgelet, clinkme.dev) surfaced five fixes:

1. Manage `details` single-object responses now parse. Lightning.Pub returns a
   bare OfferData object for create/update/get and an array only for list; enable
   Jackson ACCEPT_SINGLE_VALUE_AS_ARRAY so both shapes coerce into the list field.
2. NOffer.encode() always emits the price-type TLV (3), even for spontaneous
   offers — the reference SDK and bridgelet decoders throw on a missing TLV 3, so
   an absent field made our pointers undecodable by every JS consumer. Decode now
   defaults an absent/unknown price-type to SPONTANEOUS, per the spec.
3. Nip05Parser.parseClinkOffer accepts bridgelet's flat top-level
   `"clink_offer":"noffer1…"` string in addition to the spec's per-name map.
4. Offer payment receipts: OfferEvent.createReceipt/decryptReceipt +
   OfferClient.parseReceipt + OfferReceipt.isOk() make the post-settlement receipt
   (the SDK's onReceipt) a parseable primitive instead of a dead DTO.
5. ClinkOfferPayer signs offer requests with an ephemeral key, like the SDK / Zeus
   / Stacker News, so paying an offer no longer reveals the user's Nostr identity
   to the service. Debits keep the persistent account key (budgets need a stable
   app identity).

Adds regression tests for each: always-emit TLV3, flat-string NIP-05 discovery,
and a receipt round-trip.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 15:16:44 +00:00
Claude fdc09ad039 perf(clink): cache parsed NOffer for NIP-05 lookups + case-insensitive key
The profile-header NIP-05 clink_offer cache stored the raw noffer string,
re-running ClinkPointerParser on every cache hit; it now stores the parsed
NOffer. The cache key is lowercased since NIP-05 identifiers are
case-insensitive, so casing variants no longer trigger duplicate fetches.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 14:30:02 +00:00
Claude 33b3eb5b6f docs(clink): record final implementation state, audit results, spec-vs-SDK notes
Append a 'Final implementation state' section to the CLINK plan capturing
what shipped across Phases 0-3 + the receive side + CLI, the audit findings
and their fixes, the three-level verification matrix, and the critical
spec-vs-SDK gotchas (offer 'latest' at GFY code 3 and ndebit k1 at TLV-3 are
spec-defined and must not be removed). Flip the doc status to implemented.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 07:18:02 +00:00
Claude 545018a6a3 test(cli): clink-headless harness for amy offer/debit info
Local-only shell suite (no relay): asserts amy decodes the canonical CLINK
interop vectors (same fixtures as quartz ClinkInteropTest) to the right fields
for 'offer info' and 'debit info', plus the argument-error paths (bad pointer,
unknown budget frequency, missing --amount). The round-trip verbs need a live
service and stay out of scope.

12/12 assertions pass locally (amy init -> decode, no network).
2026-06-10 07:01:43 +00:00
Claude 904032cea8 feat(cli): amy debit command for CLINK debits (info + pay + budget)
Completes amy's CLINK coverage alongside 'amy offer', reusing the
Context.requestResponse round-trip primitive:
- debit info NDEBIT: local decode of an ndebit1… pointer (pubkey, relays,
  pointer id, session flag), no network.
- debit pay NDEBIT BOLT11 [--amount SATS] [--timeout MS]: kind-21002 round
  trip asking the wallet to pay the invoice; prints preimage or GFY error.
- debit budget NDEBIT --amount SATS [--frequency day|week|month] [--timeout MS]:
  authorize a one-time or recurring spending budget.

Thin-assembly: ClinkPointerParser + DebitClient (quartz) do the protocol; the
command shares one roundTrip helper. Verified: 'debit info' decodes an interop
vector correctly (text + --json), and budget arg validation returns exit 1.
pay/budget need a live debit service to exercise fully.
2026-06-10 06:44:45 +00:00
Claude 7d9a42a51b feat(cli): amy offer command for CLINK offers (info + request)
Adds headless CLINK Offers support to amy for interop testing against real
offer services:
- offer info NOFFER: local decode of a noffer1… pointer (pubkey, relays,
  pointer id, price type/amount), no network.
- offer request NOFFER [--amount SATS] [--timeout MS]: the kind-21001 round
  trip — publishes the request to the pointer's relays and prints the returned
  BOLT11, or the service's error.

Thin-assembly per the CLI contract: pointer decode + request/response events
live in quartz (ClinkPointerParser, OfferClient); the round-trip uses a new
Context.requestResponse primitive (publish then await the first matching live
reply — unlike drain, which returns at EOSE).

Verified: 'offer info' runs end-to-end against an interop vector (correct
pubkey/relays/price-type, text + --json modes, bad-pointer error contract +
exit 1). The request round-trip needs a live service to exercise fully.
2026-06-10 06:03:07 +00:00
Claude 2bd18eb50d test(clink): regression tests for the audit fixes
Locks in the protocol-layer fixes that were previously only compile-checked:
- offerLargePriceRoundTripIsUnsigned: a price > Int.MAX_VALUE round-trips as a
  positive Long (guards the unsigned-decode fix).
- cannotDecryptAuthoredEventMissingRecipient: an authored event with no p tag
  can't be decrypted by anyone (guards the no-self-fallback conversationPeer).
- manageCreateRequestSerializesNested + manageFailureResponseParsesField: the
  Manage request nests under offer.fields, payer_data is a string list, and the
  failure response carries field (guards the 21003 shape fix).

All CLINK tests pass.
2026-06-10 05:10:54 +00:00
Claude 488d459984 harden(clink): no self-decrypt fallback + #p on response filters
Two robustness fixes from the audit:
- OfferEvent/DebitEvent/ManageEvent: replace talkingWith() (which fell back to
  self when an authored event lacked its p tag, deriving a NIP-44 key with
  myself) with conversationPeer(), which returns null when I'm neither the
  author nor the addressed recipient; decryptContent then fails cleanly with
  UnauthorizedDecryptionException. canDecrypt() is now exactly 'a valid peer
  exists'.
- OfferClient/DebitClient/ManageClient responseFilter now also requires
  #p == my pubkey, so a service reply that e-tags my request but is addressed
  to a different payer no longer matches my subscription.

Valid request/response round-trips are unchanged (CLINK tests pass).
2026-06-10 04:32:51 +00:00
Claude 3968790db1 fix(clink): audit fixes — unsigned offer price, Manage shape, NIP-05 cache
From a spec/SDK audit (verified against the CLINK spec, not just SDK 1.5.5):
- NOffer.price: decode as UNSIGNED 4-byte big-endian (now Long) — the SDK reads
  price via parseInt(hex); reading it signed turned prices >= 2^31 sats negative
  and broke encode/decode idempotency for high-bit prices.
- Manage (21003) messages corrected to the nested spec shape: request nests offer
  data under offer{id,fields}, payer_data is a string list (not a map), and the
  response uses details + field (was offer/offers). Documented the single-object
  details limitation (Manage is consume-unused).
- DisplayClinkOffer: cache NIP-05 .well-known clink_offer lookups (incl. negative
  results) so profile visits / kind-0 refreshes don't refetch nostr.json.

Deliberately NOT changed: the offer 'latest' (code 3) field and ndebit k1 at
TLV-3 — both are SPEC-defined; the SDK 1.5.5 merely lags, as the code comments
already noted. CLINK tests pass; app compiles.
2026-06-10 04:20:47 +00:00
Claude b74d769f60 fix(clink): honor default payment source in App Functions + main-safe debit callback
Resolves two review findings:
- App Functions (Assistant) pay path gated on hasWalletConnectSetup() and so
  ignored a debit-only user's chosen default. payViaNwcOrNull is generalized to
  payViaDefaultSourceOrNull, routing through account.settings.defaultPaymentSource()
  (NWC wallet or CLINK debit), matching the rest of the app; NwcOutcome -> PayOutcome.
- AccountViewModel.payInvoiceViaClinkDebit now delivers onResult on Dispatchers.Main
  (was Dispatchers.IO via launchSigner), consistent with requestDebitBudget and safe
  for UI callbacks (toasts/dialogs).

:amethyst (play) compiles.
2026-06-10 03:43:33 +00:00
Claude a481fa5bea fix(clink): harden payer round-trips + two review findings
From a correctness review of the CLINK branch:
- ClinkOfferPayer/ClinkDebitPayer now catch decrypt/parse failures from
  parseResponse and treat an undecryptable reply as no response (return null)
  instead of throwing. An uncaught SerializationException/NIP-44 failure
  escaped launchSigner (which only catches signer exceptions), hanging the UI:
  the offer card stuck on 'Requesting…', the DVM status stuck, the budget toast
  never shown, and a split zap silently cancelling sibling payments.
- ClinkOfferPreview now renders the active (possibly moved) offer's price/type,
  not the original pointer's, after an Expired-or-Moved redirect.
- WalletViewModel.setDefaultWallet only updates local state when the persist
  actually succeeds, so the default star can't diverge from the stored value.

:amethyst compiles.
2026-06-10 03:13:54 +00:00
Claude 084c7be23a feat(clink): debit spending budgets (one-time + recurring)
Exposes the CLINK Debits budget capability (requestBudget) the spec describes:
- ClinkDebitPayer.requestBudget publishes the kind-21002 budget request and
  awaits the reply; the publish/await machinery is factored out of payInvoice
  into a shared sendAndAwait helper.
- DebitFrequency gains UNIT_DAY/WEEK/MONTH constants.
- WalletViewModel.requestDebitBudget resolves the debit pointer and runs it.
- A 'Budget' action on CLINK debit rows opens ClinkBudgetDialog (amount +
  one-time/daily/weekly/monthly cadence); the result is surfaced as a toast.

:amethyst compiles. The 21002 budget round-trip is untested end-to-end.
2026-06-10 02:37:46 +00:00
Claude 7bac8cfd46 refactor(clink): clink_version as a shared ClinkVersionTag class
Models the protocol-version tag the way other tags are modeled, instead of a
loose helper on the Clink object:
- New ClinkVersionTag (TAG_NAME/CURRENT/assemble/parse) under clink/tags, with
  a clinkVersion() TagArrayBuilder DSL extension, reused by all three events.
- OfferEvent/DebitEvent/ManageEvent read version() via ClinkVersionTag::parse
  and build via clinkVersion() in their templates.
- Retires the now-empty Clink object (its KDoc moved to the tag class).

Behavior-preserving: assemble() emits the identical ["clink_version", "1"]
tag in the same position. All CLINK tests pass.
2026-06-10 01:41:04 +00:00
Claude 03b091d3ec refactor(clink): model event tags via PTag/ETag classes + builder DSL
Brings OfferEvent/DebitEvent/ManageEvent (21001-3) in line with the codebase
tag conventions, replacing raw inline tags:
- Build via eventTemplate(KIND, content) { pTag(...); eTag/add; alt(...) } and
  signer.sign(template), instead of hand-rolled arrayOf("p"/"e", ...) + sign().
- Accessors use PTag.parseKey / ETag.parseId instead of matching "p"/"e" literals.

Behavior-preserving: PTag.assemble(x, null) yields the identical ["p", x] bytes
and tag order is unchanged, so signed events are byte-identical. All CLINK tests
pass (ClinkEventTest, ClinkClientServerTest, pointer/interop).

Note: these are NIP-44-encrypted request/response events, so create*() stays a
suspend factory that encrypts then signs the template — matching NIP-47; a pure
pre-signing template isn't possible without the signer.
2026-06-10 01:26:06 +00:00
Claude c1a0e707a0 refactor(clink): clink_offer metadata via ClinkOfferTag + DSL builder
Brings the kind-0 clink_offer field in line with the sibling fields' structure
instead of a raw string constant written to content only:
- New ClinkOfferTag (TAG_NAME/assemble/parse) under nip01Core/metadata/tags.
- clinkOffer() TagArrayBuilder DSL extension in TagArrayBuilderExt.
- MetadataEvent uses ClinkOfferTag.TAG_NAME and dual-writes it as a kind-0 tag
  in updateOrDeleteTagNames (NIP-1770 pattern), like lud16/nip05; drops the
  ad-hoc CLINK_OFFER_PROPERTY constant.

UpdateMetadataTest now also asserts the tag is emitted. quartz tests pass.
2026-06-10 01:10:49 +00:00
Claude 57850c3bcf revert(clink): drop zap requests from the offer card
Paying a CLINK offer is already a direct payment to the recipient; wrapping it
in a NIP-57 zap request conflated two different things. Removes the zappable-
offers and post-level-zap behavior:
- ClinkOfferPreview no longer builds a zap request or takes authorPubKey/zapEvent;
  it just pays the fetched invoice via the default source.
- ClinkOfferPayer.requestInvoice drops the zap param.
- Unwinds the zapEvent threading through RichTextViewer / ExpandableRichTextViewer
  / TranslatableRichTextViewer (both flavors) and reverts Text.kt.

Keeps the rest of the offer card (variable amount, moved-offer follow, default
payment-source dispatch) and the profile receive side intact. Both flavors compile.
2026-06-10 00:43:51 +00:00
Claude 46225bf5b4 feat(clink): post-level zaps for offers + enable offer zaps in the feed
Threads the note's Event through the rich-text render chain
(TranslatableRichTextViewer [play+fdroid] -> ExpandableRichTextViewer ->
RichTextViewer -> word renderers -> ClinkOfferPreview) as a default-null
zapEvent. The offer card now prefers an e-tag zap on the post when the event
is present, falling back to the author (p-tag) zap, then a plain invoice.

Also fixes coverage: the main note body (Text.kt) didn't pass author context
at all, so offer zaps previously only fired in chat. It now passes both
authorPubKey and the note event, so paying a noffer in a feed post lands as a
zap on that post.

Both flavors compile. Zap round-trip/receipt untested end-to-end.
2026-06-10 00:37:19 +00:00
Claude c24215c836 feat(clink): follow a moved offer (Expired or Moved, code 3)
When the offer service replies EXPIRED_OR_MOVED with a replacement noffer in
'latest', the card now parses it, swaps to the new pointer, and retries the
request once (paying the relocated offer) instead of dead-ending on an error.
Request handling is refactored into a single helper so the amount field and
zap-request attachment apply to the retry too.

:amethyst compiles; offer round-trip remains untested end-to-end.
2026-06-10 00:18:58 +00:00
Claude 6b9184bf50 feat(clink): read + surface a profile's noffer (kind-0 + NIP-05)
Completes the receive side: a payable CLINK Offer card now appears on a
profile that advertises one, preferring the kind-0 clink_offer and falling
back to the NIP-05 .well-known clink_offer.
- Nip05Parser.parseClinkOffer + INip05Client.loadClinkOffer fetch/parse the
  well-known clink_offer (keyed by local name, mirroring the names map; exact
  shape isn't a finalized spec so a mismatch yields null). JVM-tested.
- DrawAdditionalInfo.DisplayClinkOffer resolves kind-0 first, else fetches
  NIP-05 on IO, parses the noffer, and renders ClinkOfferPreview zapping the
  profile.

quartz tests pass; :amethyst compiles. Network fetch + card render untested
end-to-end.
2026-06-09 23:39:36 +00:00
Claude def5cbc4e9 feat(clink): let users set a noffer on their profile
Adds a 'CLINK Offer (noffer)' field to the profile editor so users can advertise
a payment offer in their kind-0 metadata:
- UserMetadataState.sendNewUserMetadata threads clinkOffer into the kind-0 build.
- NewUserMetadataViewModel loads/saves/clears the clinkOffer field.
- NewUserMetadataScreen renders the input (placeholder noffer1…).

:amethyst compiles. The read side (NIP-05 clink_offer discovery + preferring it)
is the next step.
2026-06-09 23:28:26 +00:00
Claude 50b4ed8c1e feat(clink): kind-0 clink_offer metadata field
Adds the CLINK Offers discovery pointer to profile metadata, mirroring the
NIP-05 `clink_offer` key:
- UserMetadata.clinkOffer (@SerialName clink_offer) + clinkOffer() accessor,
  with trim/blank cleanup alongside the other fields.
- MetadataEvent.createNew/updateFromPast gain a clinkOffer param written into
  kind-0 content via the new CLINK_OFFER_PROPERTY key.

Covered by UpdateMetadataTest (write + parse round-trip) on JVM.
2026-06-09 23:22:36 +00:00
Claude db2f65d300 feat(clink): make offer payments zappable
When a noffer is rendered in someone's note, paying it now attaches a NIP-57
zap request so the offer service issues a zappable invoice and publishes a zap
receipt — turning the payment into a real zap on the author instead of a silent
invoice:
- ClinkOfferPayer.requestInvoice forwards a serialized zap request via
  OfferClient's existing zap field.
- ClinkOfferPreview builds the 9734 from the threaded authorPubKey using the
  account's default zap type (skipped for NONZAP), at the resolved amount.
- RichTextViewer threads authorPubKey into the offer-segment renderers; the
  markdown/secret paths default to null (plain invoice, no target).

Falls back to a plain invoice when there's no author or the user opted out of
zaps. :amethyst compiles; the round-trip and receipt remain untested end-to-end.
2026-06-09 23:13:26 +00:00
Claude 94ee192641 feat(clink): support variable-amount offers in the offer card
The offer card no longer assumes a fixed price:
- FIXED offers show their preset price (unchanged).
- SPONTANEOUS offers (and the spec default when the pointer omits a price
  type) now render an amount field; Pay is disabled until a positive amount
  is entered, and that amount is sent as amount_sats.
- An INVALID_AMOUNT (code 5) response reveals/refines the amount field and
  shows the service's allowed range, so variable offers recover gracefully
  even when the price type was ambiguous.

:amethyst compiles; the offer round-trip remains untested end-to-end.
2026-06-09 23:00:12 +00:00
Claude 503f33eb2e feat(clink): pay offer/invoice cards via default source, confirmed in-app
In-post payment cards now route their 'Pay' button through the selected
default payment source instead of always opening an external wallet:
- New shared InvoicePaymentDispatcher: resolves defaultPaymentSource() for a
  bolt11. External-wallet path fires the intent (the wallet app confirms);
  NWC and CLINK-debit paths show a ConfirmPaymentDialog first, because a card
  pay (unlike a deliberate small zap tap) can be a larger/variable amount.
- ClinkOfferPreview and InvoicePreview both drive it via a pending-invoice
  state; InvoicePreview now takes accountViewModel.

NWC-only and intent-only users are unaffected. :amethyst compiles; the in-app
pay paths remain untested end-to-end.
2026-06-09 22:49:32 +00:00
Claude 11891ace8c feat(clink): route profile + DVM single-invoice pays through the default source
The two remaining 'pay this bolt11' sites now honor the selected default
payment source instead of the binary NWC-or-intent check:
- AccountViewModel gains payInvoiceViaClinkDebit, the single-invoice debit-rail
  counterpart of sendZapPaymentRequestFor.
- DisplayLNAddress (profile LN-address pay) and DvmContentDiscoveryScreen (DVM
  invoice pay) dispatch on defaultPaymentSource(): CLINK debit -> 21002 round
  trip, NWC -> existing pay_invoice, none -> external wallet intent.

NWC-only users are unaffected. :amethyst compiles; the debit payout path stays
untested end-to-end.
2026-06-09 22:31:26 +00:00
Claude 002cd3cceb feat(clink): add CLINK debit as a payment source in the Wallet screen
Surfaces debits in the existing wallet list and add flow:
- WalletViewModel reads clinkDebitWallets and emits them as spend-only rows
  (canShowBalance=false) in the unified walletInfoList; the default radio
  spans both types via setDefaultPaymentSource; remove/rename route by type.
- WalletScreen renders debit rows with a Pay only badge instead of a balance
  and disables the NWC-only detail navigation for them.
- AddWalletScreen offers a CLINK Debit type; AddClinkDebitWalletScreen pastes
  or scans an ndebit1 pointer (no secret) and saves it as a payment source.
- New Route.WalletAddClinkDebit + AppNavigation registration.

:amethyst compiles. UI rendering and the end-to-end debit payout remain
untested on device.
2026-06-09 22:24:11 +00:00
Claude 7533c9f34f feat(clink): route zap payments through the selected default source
ZapPaymentHandler now dispatches on account.settings.defaultPaymentSource():
a CLINK debit default pays each zap invoice via the new payViaClinkDebit
(kind-21002 round-trip through ClinkDebitPayer, surfacing the service's GFY
error text); an NWC default keeps the existing payViaNWC path; no configured
source falls back to the external wallet intent. NWC-only users are unaffected.

The two secondary single-invoice sites (profile LN-address pay, DVM pay) still
use NWC/intent and are left as follow-ups.

:amethyst compiles. The debit payout path is untested end-to-end — it needs a
live debit service to verify a real payment.
2026-06-09 22:16:52 +00:00
Claude 71dc036889 feat(clink): persist debit wallets + unify zap default payment source
Wires the payment-source model into AccountSettings + LocalPreferences:
- AccountSettings gains clinkDebitWallets and add/remove/rename methods
  mirroring the NWC ones, plus defaultPaymentSource() resolving the unified
  default via PaymentSourceResolver.
- Renames defaultNwcWalletId -> defaultPaymentSourceId: one default id spans
  both NWC wallets and CLINK debits. First configured source of any kind
  auto-defaults; adding more never silently changes it; removing the default
  falls back to the first remaining source.
- LocalPreferences persists clinkDebitWallets and defaultPaymentSourceId,
  migrating the legacy defaultNwcWalletId key on load.
- NwcSignerState/WalletViewModel read the unified default; NWC zap routing is
  unchanged for NWC-only users (falls back to first NWC wallet).

:amethyst compiles. The Wallet-screen rows/confirm dialog and routing the zap
button through ClinkDebitPayer are the next (compile-only) step.
2026-06-09 22:06:22 +00:00
Claude f0276f1e04 feat(clink): debit payment-source model + unified default resolver
Adds the verifiable core for using a CLINK debit pointer as a spend rail
alongside NWC:
- ClinkDebitWalletEntry (commons): a saved ndebit pointer, the spend-only
  counterpart of NwcWalletEntry (no secret, no balance/history)
- PaymentSource + PaymentSourceResolver (commons): unifies NWC wallets and
  CLINK debits into one list with a single default id spanning both types;
  no explicit default falls back to first (NWC before debits), preserving
  today's behavior. canShowBalance marks NWC vs debit honestly.
- ClinkDebitPayer (amethyst): publishes the kind-21002 pay request and awaits
  the preimage via a one-shot subscription, mirroring ClinkOfferPayer.

Resolver logic covered by PaymentSourceResolverTest on JVM (7 cases incl.
cross-type default + stale-id fallback); amethyst compiles. Persisting the
new fields in AccountSettings and the Wallet-screen rows/confirm dialog are
the next (compile-only) step.
2026-06-09 21:52:05 +00:00
Claude a7066784d4 feat(clink): render noffer offers as a payable card in notes
Wires the ClinkOfferSegment into RichTextViewer with a ClinkOfferPreview
card (modeled on InvoicePreview): shows the offer + price and a Pay
button. Pay runs ClinkOfferPayer, which publishes the kind-21001 request
to the offer's relays and awaits the encrypted reply via a one-shot
subscription, then hands the returned bolt11 to the existing
payViaIntent wallet flow. Consume-only; Amethyst never answers offers.

Compiles (:amethyst:compilePlayDebugKotlin); visual rendering and a live
offer round-trip still need on-device verification.
2026-06-09 21:19:45 +00:00
Claude 62e522ccc4 feat(clink): detect noffer pointers in rich-text as ClinkOfferSegment
Teaches the commons RichTextParser to recognize an inline noffer1...
token and emit a ClinkOfferSegment carrying the decoded NOffer, so a
GUI front end can render a 'Pay' card in the note body (the feed-offer
feature). Bare tokens only for now; nostr:/lightning: prefixed forms
fall through. Covered by ClinkOfferSegmentTest on JVM.
2026-06-09 20:58:58 +00:00
Claude ef7658ae09 test(clink): add cross-impl interop vectors from @shocknet/clink-sdk
Adds ClinkInteropTest with bech32 pointer strings generated by the
reference TypeScript SDK (clink-sdk 1.5.5) for noffer/ndebit/nmanage.
Asserts our parser decodes the SDK's bytes into the expected fields and
that re-encoding round-trips. TLV is order-independent on decode, so
interop is functional (not byte-identical: we emit fields ascending,
the SDK descending); the reverse direction (SDK decoding our output)
was verified out-of-band against decodeBech32.
2026-06-09 20:39:45 +00:00
Claude c619338204 feat(clink): add CLINK client and server facades
Adds the high-level request/response orchestration over the CLINK
pointers and event kinds (experimental/clink):
- OfferClient / DebitClient / ManageClient: build the kind-21001/2/3
  request from a decoded pointer, expose the relays to publish on, the
  response filter (kind + author + #e=requestId), and the response parser
- ClinkServer: per-kind request filters (#p=service), 30s freshness
  check, plus K1Tracker for single-use debit session enforcement

Filter construction, freshness window and k1 single-use covered by
ClinkClientServerTest on JVM; request-building encryption round-trips
will be added under androidDeviceTest (lazysodium constraint).
2026-06-09 20:24:48 +00:00
Claude 8fa06f525f feat(clink): add CLINK request/response event kinds and DTOs
Adds the three CLINK message kinds to quartz (experimental/clink):
- OfferEvent (21001), DebitEvent (21002), ManageEvent (21003), each
  carrying both request and response over one kind, NIP-44 encrypted,
  with p + clink_version tags and an e tag on responses
- Request/response DTOs per spec (offers, debits, manage) plus shared
  SatRange/GfyDelta and GFY/offer error-code constants
- Registers all three kinds in EventFactory

Pure-logic + JSON (de)serialization covered by ClinkEventTest on JVM;
the NIP-44 encrypt/decrypt round-trip will live in androidDeviceTest
(lazysodium is unavailable in JVM unit tests).
2026-06-09 20:19:17 +00:00
Claude 41b1b63482 feat(clink): add CLINK bech32 pointer types and parser
Implements the noffer/ndebit/nmanage pointers (CLINK Offers/Debits/Manage)
as standard-bech32 TLV codes, with a dedicated ClinkPointerParser kept
separate from NIP-19. Wire format (HRPs, TLV indices, single-byte priceType,
4-byte big-endian price) verified against @shocknet/clink-sdk 1.5.5.

Adds round-trip + dispatch + reject tests in commonTest.
2026-06-09 20:04:00 +00:00
Claude 5f41907149 docs(clink): add CLINK protocol implementation plan
Plan to implement CLINK (Offers 21001 / Debits 21002 / Manage 21003) on
Quartz (client + server) and Amethyst (consume-only), reusing NIP-44,
bech32/TLV, the NWC encrypted-event pattern, and ZapPaymentHandler.
Pointers parsed by a dedicated ClinkPointerParser (separate from NIP-19).
2026-06-09 19:44:21 +00:00
Vitor PamplonaandGitHub 3dfe418150 Merge pull request #3151 from vitorpamplona/claude/relay-message-pagination-LMqSQ
DM history: per-relay backward paging, live-tail split + prune-aware window realignment
2026-06-09 15:05:58 -04:00