Files
amethyst/tools
Claude 475da3ff3c refactor: move NIP-47 and CLINK to kotlinx only, and make their parsers forgiving
These were the last two reflective JSON bindings in the app. Jackson's hand-written
NWC deserializers dispatched to ~35 concrete classes with treeToValue(), and CLINK
went through readValue<T>() with no registered deserializer at all — so the field
names of 106 classes were load-bearing under R8 and cost two package keeps.

OptimizedJsonMapper.jvmAndroid now routes Request/Response/Notification and the
seven CLINK types at the hand-written kotlinx serializers that already existed in
commonMain and already were the production path on iOS. Nothing new had to be
written to replace Jackson; the Jackson (de)serializers for these types, and
OmitNullsMixin, are deleted. Everything else stays on Jackson, which is faster on
the event hot path and non-reflective there.

    seeds under nip47WalletConnect.rpc   851 -> 19
    seeds under experimental.clink       793 ->  9
    DEX                              31.11 MB -> 30.96 MB
    reflection contract              31 checks -> 24

PayInvoiceParams is now gone from the DEX entirely: R8 merged it away, which the
keep rule had been forbidding. It is not deleted (usage.txt lists only members),
the request serializer survives, and the wire-shape tests pass.

**Forgiving parsing.** The serializers read every field through new helpers in
nip01Core.kotlinSerialization.LenientJson, replacing 176 raw `.jsonPrimitive` /
`.jsonObject` accesses that threw on the wrong shape. The rule now: a field that is
missing, null, or the wrong type reads as null and the rest of the message still
parses. That is deliberately more forgiving than Jackson was — it ignored unknown
properties but still raised MismatchedInputException when a declared String arrived
as an object, losing a settled payment's preimage over one bad neighbouring field.

Covered by 22 new tests across NWC and CLINK: unknown result_types, unknown extra
fields, integers quoted as strings, booleans as 1/0, explicit nulls, a wrong-shaped
field costing only itself, a broken entry in a list leaving the good ones, and a
lone value where a list belongs (Jackson's ACCEPT_SINGLE_VALUE_AS_ARRAY, preserved).

An unrecognised `result_type` no longer throws. NIP-47 grows and the wallet is
somebody else's software, so those arrive as the new NwcUnknownResponse carrying
their result intact. It is deliberately not an IErrorResponseLike: an unknown
answer is not a refusal. Response is abstract, not sealed, so no consumer breaks.
An unknown *request* method still throws — Request is sealed, we are a client, and
that one is left for a separate decision.

Two things found on the way:

- toAnyValue() tried Double before Long, so a metadata integer round-tripped 42 as
  42.0 and changed the bytes of a field we only carry. Fixed the precedence.
- toAnyValue/toAnyMap lived under nip47 while CLINK imported them across packages;
  moved beside their inverse anyToJsonElement in nip01Core.

Verified: full ./gradlew test green, R8 release build green, 24/24 contract checks,
and the NWC/CLINK classes confirmed renamed in the shipped DEX. The NWC payment and
CLINK offer paths are the ones to exercise on a device — R8 can optimize these
classes for the first time, and no unit test runs against the minified APK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEoxktEZyTrAS33vVZBiwm
2026-09-19 03:40:05 +00:00
..