mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
Merge pull request #2801 from mstrofnone/feat/namecoin-malformed-json-diagnostic
feat(namecoin): distinguish malformed-JSON values from missing-field
This commit is contained in:
@@ -201,6 +201,13 @@ fun SearchScreen(
|
||||
NamecoinResolveState.Error("Name exists but has no Nostr pubkey")
|
||||
}
|
||||
|
||||
is NamecoinResolveOutcome.MalformedRecord -> {
|
||||
// Surface the parser detail verbatim so the publisher
|
||||
// of the broken record can locate the bad byte
|
||||
// (kotlinx.serialization includes a column number).
|
||||
NamecoinResolveState.Error("Namecoin record JSON is malformed: ${outcome.error}")
|
||||
}
|
||||
|
||||
is NamecoinResolveOutcome.ServersUnreachable -> {
|
||||
NamecoinResolveState.Error("ElectrumX servers unreachable — check your connection or try again")
|
||||
}
|
||||
|
||||
+50
-2
@@ -43,6 +43,20 @@ sealed class NamecoinResolveOutcome {
|
||||
val name: String,
|
||||
) : NamecoinResolveOutcome()
|
||||
|
||||
/**
|
||||
* The name exists but its value is not parseable as a Namecoin Domain
|
||||
* Name Object (i.e. valid JSON of the right shape). Distinct from
|
||||
* [NoNostrField], which means valid JSON but no `nostr` data.
|
||||
*
|
||||
* `error` is the underlying parser message (kotlinx.serialization);
|
||||
* useful for surfacing back to the publisher of the broken record
|
||||
* (e.g. "Unfinished JSON term at EOF at line 1, column 474").
|
||||
*/
|
||||
data class MalformedRecord(
|
||||
val name: String,
|
||||
val error: String,
|
||||
) : NamecoinResolveOutcome()
|
||||
|
||||
/** All ElectrumX servers were unreachable. */
|
||||
data class ServersUnreachable(
|
||||
val message: String,
|
||||
@@ -234,9 +248,23 @@ class NamecoinNameResolver(
|
||||
)
|
||||
}
|
||||
|
||||
// Distinguish "valid JSON, no `nostr` field" from "value is
|
||||
// unparseable JSON". Both used to collapse into NoNostrField, which
|
||||
// sent operators chasing a missing field that wasn't actually the
|
||||
// problem (the Namecoin record itself was malformed). Surfacing
|
||||
// the parser's column number lets a publisher see WHERE the value
|
||||
// is bad without spelunking through the whole serialised form.
|
||||
val valueJson =
|
||||
tryParseJson(nameResult.value)
|
||||
?: return NamecoinResolveOutcome.NoNostrField(parsed.namecoinName)
|
||||
parseValueOrError(nameResult.value)
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { err ->
|
||||
return NamecoinResolveOutcome.MalformedRecord(
|
||||
name = parsed.namecoinName,
|
||||
error = err.message ?: "unparseable JSON value",
|
||||
)
|
||||
},
|
||||
)
|
||||
val merged = expandImportsIfPresent(valueJson)
|
||||
|
||||
val nostrResult =
|
||||
@@ -473,5 +501,25 @@ class NamecoinNameResolver(
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse [raw] into a [JsonObject], surfacing the underlying parser
|
||||
* error message instead of swallowing it. The result is used by
|
||||
* [performLookupDetailed] to distinguish [NamecoinResolveOutcome.MalformedRecord]
|
||||
* (the value itself is broken) from [NamecoinResolveOutcome.NoNostrField]
|
||||
* (valid JSON, just missing the `nostr` block).
|
||||
*
|
||||
* Returns [Result.failure] with the parser's diagnostic message; the
|
||||
* caller is expected to forward that text into a UI / log surface so
|
||||
* the publisher of the broken record can fix it.
|
||||
*/
|
||||
private fun parseValueOrError(raw: String): Result<JsonObject> =
|
||||
runCatching {
|
||||
val element = json.parseToJsonElement(raw)
|
||||
element as? JsonObject
|
||||
?: throw IllegalArgumentException(
|
||||
"top-level value is ${element::class.simpleName}, expected JSON object",
|
||||
)
|
||||
}
|
||||
|
||||
private fun isValidPubkey(s: String): Boolean = HEX_PUBKEY_REGEX.matches(s)
|
||||
}
|
||||
|
||||
+56
@@ -502,6 +502,62 @@ class NamecoinImportTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NIP-05 lookup surfaces MalformedRecord with parser detail when value is broken JSON`() =
|
||||
runTest {
|
||||
// Real-world failure mode: a hand-built `name_update` with one
|
||||
// closing brace short of balanced ends up published as broken
|
||||
// JSON. The publisher then sees "name not found / no nostr
|
||||
// field" and chases a phantom bug. Surface the parser's
|
||||
// diagnostic so they know the value itself is the problem.
|
||||
val truncated =
|
||||
"""{"ip":"1.2.3.4","nostr":{"names":{"_":"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"}}""".trimIndent()
|
||||
val client =
|
||||
FakeElectrumXClient().apply {
|
||||
register("d/broken", truncated)
|
||||
}
|
||||
val resolver = NamecoinNameResolver(client, lookupTimeoutMs = 1_000L)
|
||||
val outcome = resolver.resolveDetailed("_@broken.bit")
|
||||
assertTrue(
|
||||
"expected MalformedRecord, got $outcome",
|
||||
outcome is NamecoinResolveOutcome.MalformedRecord,
|
||||
)
|
||||
outcome as NamecoinResolveOutcome.MalformedRecord
|
||||
assertEquals("d/broken", outcome.name)
|
||||
// Don't pin the exact parser text — different kotlinx.serialization
|
||||
// versions phrase it differently. Just require it's a non-empty
|
||||
// diagnostic.
|
||||
assertTrue(
|
||||
"error must be a non-empty diagnostic: ${outcome.error}",
|
||||
outcome.error.isNotBlank(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NIP-05 lookup surfaces MalformedRecord when top-level value is a JSON array`() =
|
||||
runTest {
|
||||
// Top-level non-object values (arrays, primitives, null) are
|
||||
// not valid Domain Name Objects per ifa-0001. They should be
|
||||
// rejected with a useful diagnostic, not collapsed into
|
||||
// NoNostrField (which implies the JSON parsed fine).
|
||||
val client =
|
||||
FakeElectrumXClient().apply {
|
||||
register("d/arr", """["not","an","object"]""")
|
||||
}
|
||||
val resolver = NamecoinNameResolver(client, lookupTimeoutMs = 1_000L)
|
||||
val outcome = resolver.resolveDetailed("_@arr.bit")
|
||||
assertTrue(
|
||||
"expected MalformedRecord, got $outcome",
|
||||
outcome is NamecoinResolveOutcome.MalformedRecord,
|
||||
)
|
||||
outcome as NamecoinResolveOutcome.MalformedRecord
|
||||
assertEquals("d/arr", outcome.name)
|
||||
assertTrue(
|
||||
"error must mention the unexpected top-level shape: ${outcome.error}",
|
||||
outcome.error.contains("expected JSON object", ignoreCase = true),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun parse(s: String): JsonObject = json.parseToJsonElement(s).jsonObject
|
||||
|
||||
Reference in New Issue
Block a user