mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
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
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
# CLINK on Quartz + Amethyst
|
||||
|
||||
Status: proposed
|
||||
Status: implemented (Phases 0–3 + receive side + CLI) — see "Final
|
||||
implementation state" at the bottom for what actually shipped, the audit
|
||||
results, and the spec-vs-SDK gotchas. The body below is the original
|
||||
(proposed) design and is kept for context; where it disagrees with the final
|
||||
state, the final state wins.
|
||||
Date: 2026-06-09
|
||||
Owner: TBD
|
||||
|
||||
@@ -129,3 +133,114 @@ if users want to mint offers from inside the app later.
|
||||
pin against `@shocknet/clink-sdk` source, not the client-rendered docs.
|
||||
- Relay selection for ephemeral req/resp (pointer relay vs account relays).
|
||||
- Response-timeout + retry UX (30s freshness window, GFY code 3 deltas).
|
||||
|
||||
---
|
||||
|
||||
## Final implementation state
|
||||
|
||||
Everything below reflects what is on the branch now, not the proposal above.
|
||||
Read this section first if you're touching CLINK.
|
||||
|
||||
### What shipped
|
||||
|
||||
**Phase 0 — Quartz foundation** (`quartz/.../experimental/clink/`)
|
||||
- Pointers (`pointers/NOffer.kt`, `NDebit.kt`, `NManage.kt`) decoded/encoded
|
||||
via a dedicated `ClinkPointerParser` (bech32 + TLV), **not** wired into
|
||||
`Nip19Parser`. Round-trip tested against the canonical interop vectors
|
||||
(`ClinkPointerTest`, `ClinkInteropTest`).
|
||||
- Events `OfferEvent(21001)`, `DebitEvent(21002)`, `ManageEvent(21003)`,
|
||||
refactored to the codebase convention: `eventTemplate(KIND, content, …){…}`
|
||||
+ tag-class DSL (`pTag`, `eTag`, `alt`, `clinkVersion`) and typed accessors
|
||||
(`PTag::parseKey`, `ETag::parseId`). Registered in `EventFactory`.
|
||||
- High-level clients `OfferClient` / `DebitClient` / `ManageClient` build the
|
||||
request event, expose a `responseFilter` (filtered by **both** `e=reqId`
|
||||
and `p=self`), and parse the NIP-44-decrypted response DTO.
|
||||
- Shared `clink_version` is its own tag class (`tags/ClinkVersionTag.kt`,
|
||||
`CURRENT="1"`) reused by all three events, with a `clinkVersion()` builder
|
||||
extension. The old `Clink.kt` constants object was retired.
|
||||
|
||||
**Phase 1 — Offers consume** (Amethyst): inline feed card
|
||||
(`ClinkOfferPreview.kt`) renders a payable "⚡" card for a `noffer1…` token,
|
||||
with a variable-amount field for SPONTANEOUS offers, moved-offer follow
|
||||
(GFY code 3 `latest`), and the resolved `activeOffer` price. Payment routes
|
||||
through the shared `InvoicePaymentDispatcher` (confirm-then-pay) into
|
||||
`ZapPaymentHandler`. **Zappable offers were explicitly reverted** — do not
|
||||
re-add NIP-57 zaps to offer payment.
|
||||
|
||||
**Phase 2 — Debits as a wallet** (Amethyst, consume-only): a stored `ndebit`
|
||||
pointer is a first-class payment source alongside NWC. `PaymentSource`
|
||||
(sealed: `Nwc` / `ClinkDebit`), `PaymentSourceResolver`, and
|
||||
`AccountSettings.defaultPaymentSource()` unify default selection;
|
||||
`defaultNwcWalletId` was migrated to `defaultPaymentSourceId`.
|
||||
`ZapPaymentHandler` dispatches on the resolved source. Budgets/recurring via
|
||||
`DebitClient.requestBudget` + `DebitFrequency` units (day/week/month).
|
||||
|
||||
**Phase 3 — Manage** (Quartz only, no Amethyst UI): nested request/response
|
||||
shape — `ManageRequest(resource, action, pointer, offer: ManageOffer?)`,
|
||||
`ManageOffer(id, fields: OfferFields)`, `OfferData(...)`, `ManageResponse(...)`.
|
||||
|
||||
**Receive side** (advertise your own `noffer`): kind-0 `clink_offer` field
|
||||
(`UserMetadata.clinkOffer` + dual-written `clink_offer` tag via
|
||||
`ClinkOfferTag`, NIP-1770 pattern) **and** NIP-05 `.well-known` discovery
|
||||
(`Nip05Parser.parseClinkOffer`, `INip05Client.loadClinkOffer`). The profile
|
||||
header (`DisplayClinkOffer`) prefers whichever is present, with a
|
||||
256-entry `LruCache` over the NIP-05 lookups.
|
||||
|
||||
**CLI** (`amy`): `offer info|request`, `debit info|pay|budget` (thin assembly
|
||||
only). New `Context.requestResponse(...)` does subscribe→publish→await-first-
|
||||
matching-live-reply (vs `drain`, which returns at EOSE).
|
||||
|
||||
### Audit findings & resolutions
|
||||
|
||||
- **Offer price is an UNSIGNED 4-byte BE integer.** `NOffer.price` is `Long?`;
|
||||
decode reads the 4 bytes as unsigned (SDK does `parseInt(hex)`), encode
|
||||
writes the low 32 bits. The earlier `Int` typing produced a negative price
|
||||
for any amount ≥ 2^31. Regression:
|
||||
`ClinkPointerTest.offerLargePriceRoundTripIsUnsigned` (3_000_000_000L).
|
||||
- **Decrypt guard.** `OfferEvent`/`DebitEvent`/`ManageEvent` replaced the old
|
||||
self-fallback `talkingWith()` with `conversationPeer(myPubKey)` that returns
|
||||
`null` when the signer is neither author nor recipient; `decryptContent`
|
||||
then throws `UnauthorizedDecryptionException`. Regression:
|
||||
`ClinkEventTest.cannotDecryptAuthoredEventMissingRecipient`.
|
||||
- **Payer hang fix.** `ClinkOfferPayer` / `ClinkDebitPayer` wrap `parseResponse`
|
||||
in try/catch and return `null` on a decode failure — an uncaught
|
||||
`SerializationException` previously hung the UI waiting on a coroutine that
|
||||
never completed. `payInvoiceViaClinkDebit` now delivers `onResult` on
|
||||
`Dispatchers.Main`.
|
||||
- **NIP-05 cache correctness.** The offer cache distinguishes a cache-miss
|
||||
from a cached-`null` (explicit presence check), so a profile with no offer
|
||||
isn't re-fetched on every recomposition.
|
||||
|
||||
### CRITICAL — spec vs SDK (do NOT "fix" these)
|
||||
|
||||
The `@shocknet/clink-sdk` (1.5.5) **lags the published spec**. Two things look
|
||||
like bugs against the SDK but are correct against the spec
|
||||
(`raw.githubusercontent.com/shocknet/CLINK/main/specs/clink-*.md`) and were
|
||||
verified there directly:
|
||||
|
||||
1. **Offer moved → GFY `code 3` carries `latest`** (a fresh pointer). The
|
||||
client follows it. The SDK omits this; the spec defines it. Keep the
|
||||
follow logic in `ClinkOfferPreview` / `OfferClient`.
|
||||
2. **ndebit session `k1` lives at TLV index 3.** The SDK doesn't read it; the
|
||||
spec defines it as the optional single-use session id. Keep decoding it.
|
||||
|
||||
Other shape notes for future maintainers:
|
||||
- **Manage uses the nested `offer.fields` shape** (above), not a flat object.
|
||||
- **`ManageResponse.details` is parsed as a single object**, not an array —
|
||||
Jackson's `ACCEPT_SINGLE_VALUE_AS_ARRAY` is OFF in this repo, and the
|
||||
reference service returns one object. Documented as a known limitation in
|
||||
`ManageMessages.kt`; revisit if a service returns a list.
|
||||
|
||||
### Verification matrix
|
||||
|
||||
| Level | What it covers | Status |
|
||||
|-------|----------------|--------|
|
||||
| JVM unit tests | pointer codecs (incl. unsigned price), decrypt guard, Manage nested shape, DTOs, metadata `clink_offer`, NIP-05 discovery, `PaymentSourceResolver` | green |
|
||||
| `amy` CLI | `offer info`/`request`, `debit info`/`pay`/`budget` — local decode verified end-to-end | green |
|
||||
| Shell harness | `cli/tests/clink/clink-headless.sh` — decode of canonical vectors + arg-error paths | 12/12 |
|
||||
|
||||
**Remaining gap:** the live NIP-44 request→response round-trip over a relay
|
||||
against a real CLINK service is not automated (needs a device or a
|
||||
mock/live service). Feasible later via quartz's in-process relay server
|
||||
(`nip01Core/relay/server/`) plus a mock CLINK responder; flagged but not
|
||||
built.
|
||||
|
||||
Reference in New Issue
Block a user