Files
amethyst/quartz/consumer-rules.pro
T
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

40 lines
1.9 KiB
Prolog

# =============================================================================
# Keep rules Quartz contributes to every app that consumes it (wired through
# `optimization.consumerKeepRules` in build.gradle.kts, so these end up merged
# into the consumer's own R8 configuration).
#
# Scope them tightly. A rule here applies to the CONSUMER's whole program, so
# the `-keepnames class ** { *; }` that used to sit in this file pinned every
# name in every app that depends on Quartz — ours included — and blocked R8
# from optimizing any member anywhere (`-keepnames` is `-keep,allowshrinking`).
# Only list what breaks at runtime if the name changes.
# =============================================================================
-keepdirectories libs
# secp256k1's JNI layer resolves these from native code.
-keep class fr.acinq.secp256k1.** { *; }
# libscrypt
-keep class com.lambdaworks.codec.** { *; }
-keep class com.lambdaworks.crypto.** { *; }
-keep class com.lambdaworks.jni.** { *; }
# No Jackson keeps. Every wire format Quartz speaks is now handled by a
# hand-written serializer that names its fields as string literals: the
# StdSerializer/StdDeserializer pairs registered on JacksonMapper (Event, Filter,
# Message, Command, Rumor, EventTemplate, TagArray, the NIP-46 Bunker messages),
# JsonMapperNip55 (IntentResult, Permission), and the kotlinx serializers that
# NIP-47 and CLINK route through on every target. None of it reads a Kotlin
# constructor parameter name at runtime, so none of it has to survive R8.
# Quartz serialises enums by name (Jackson writes/reads Enum.name, and
# Enum.valueOf resolves that string against the static field name), so the
# constants of its own enums have to keep their names. Consumers that persist
# their OWN enums by name need the equivalent rule in their own configuration.
-keepclassmembers enum com.vitorpamplona.quartz.** {
<fields>;
public static **[] values();
public static ** valueOf(java.lang.String);
}