Merge pull request #1914 from mstrofnone/desktop-namecoin-port
Port Namecoin NIP-05 resolution to Desktop app
This commit is contained in:
+68
-35
@@ -281,8 +281,16 @@ class NamecoinNameResolver(
|
||||
* Extract Nostr data from a `d/` domain value.
|
||||
*
|
||||
* Supports:
|
||||
* { "nostr": "hex-pubkey" } → simple form
|
||||
* { "nostr": "hex-pubkey" } → simple-string form
|
||||
* { "nostr": { "names": { "alice": "hex" }, ... } } → extended NIP-05-like form
|
||||
* { "nostr": { "pubkey": "hex", "relays": [...] } } → single-identity form
|
||||
*
|
||||
* The single-identity form is the same shape used by `id/` records and is
|
||||
* the natural way to express "this one name = this one pubkey". It only
|
||||
* resolves the root local-part (`_`) — there are no sub-identities in
|
||||
* this shape. If `nostr.names` is also present, it wins for any
|
||||
* sub-identity; root-lookups fall back to the bare `pubkey` when
|
||||
* `names["_"]` is missing.
|
||||
*/
|
||||
private fun extractFromDomainValue(
|
||||
value: JsonObject,
|
||||
@@ -290,7 +298,7 @@ class NamecoinNameResolver(
|
||||
): NamecoinNostrResult? {
|
||||
val nostrField = value["nostr"] ?: return null
|
||||
|
||||
// Simple form: "nostr": "hex-pubkey"
|
||||
// Simple-string form: "nostr": "hex-pubkey"
|
||||
if (nostrField is JsonPrimitive && nostrField.isString) {
|
||||
val pubkey = nostrField.content
|
||||
if (parsed.localPart == "_" && isValidPubkey(pubkey)) {
|
||||
@@ -305,48 +313,73 @@ class NamecoinNameResolver(
|
||||
if (parsed.localPart != "_") return null
|
||||
}
|
||||
|
||||
// Extended form: "nostr": { "names": {...}, "relays": {...} }
|
||||
if (nostrField is JsonObject) {
|
||||
val names = nostrField["names"]?.jsonObject ?: return null
|
||||
// Extended NIP-05-like form first: "nostr": { "names": {...}, "relays": {...} }
|
||||
val names = nostrField["names"]?.jsonObject
|
||||
|
||||
// Resolve: exact match → "_" root → first entry (root lookups only)
|
||||
val resolvedLocalPart: String
|
||||
val pubkey: String
|
||||
if (names != null) {
|
||||
val exactMatch = names[parsed.localPart]
|
||||
val rootMatch = names["_"]
|
||||
val firstEntry = if (parsed.localPart == "_") names.entries.firstOrNull() else null
|
||||
|
||||
val exactMatch = names[parsed.localPart]
|
||||
val rootMatch = names["_"]
|
||||
val firstEntry = if (parsed.localPart == "_") names.entries.firstOrNull() else null
|
||||
|
||||
when {
|
||||
exactMatch is JsonPrimitive && isValidPubkey(exactMatch.content) -> {
|
||||
resolvedLocalPart = parsed.localPart
|
||||
pubkey = exactMatch.content
|
||||
if (exactMatch is JsonPrimitive && isValidPubkey(exactMatch.content)) {
|
||||
return NamecoinNostrResult(
|
||||
pubkey = exactMatch.content.lowercase(),
|
||||
relays = extractRelays(nostrField, exactMatch.content),
|
||||
namecoinName = parsed.namecoinName,
|
||||
localPart = parsed.localPart,
|
||||
)
|
||||
}
|
||||
|
||||
rootMatch is JsonPrimitive && isValidPubkey(rootMatch.content) -> {
|
||||
resolvedLocalPart = "_"
|
||||
pubkey = rootMatch.content
|
||||
if (rootMatch is JsonPrimitive && isValidPubkey(rootMatch.content)) {
|
||||
return NamecoinNostrResult(
|
||||
pubkey = rootMatch.content.lowercase(),
|
||||
relays = extractRelays(nostrField, rootMatch.content),
|
||||
namecoinName = parsed.namecoinName,
|
||||
localPart = "_",
|
||||
)
|
||||
}
|
||||
|
||||
firstEntry != null &&
|
||||
if (firstEntry != null &&
|
||||
firstEntry.value is JsonPrimitive &&
|
||||
isValidPubkey((firstEntry.value as JsonPrimitive).content) -> {
|
||||
resolvedLocalPart = firstEntry.key
|
||||
pubkey = (firstEntry.value as JsonPrimitive).content
|
||||
}
|
||||
|
||||
else -> {
|
||||
return null
|
||||
isValidPubkey((firstEntry.value as JsonPrimitive).content)
|
||||
) {
|
||||
val pk = (firstEntry.value as JsonPrimitive).content
|
||||
return NamecoinNostrResult(
|
||||
pubkey = pk.lowercase(),
|
||||
relays = extractRelays(nostrField, pk),
|
||||
namecoinName = parsed.namecoinName,
|
||||
localPart = firstEntry.key,
|
||||
)
|
||||
}
|
||||
// names was present but didn't yield a match. Fall through to
|
||||
// the single-identity check below — only meaningful for root
|
||||
// lookups (non-root requests against a names-only record
|
||||
// correctly stop here).
|
||||
if (parsed.localPart != "_") return null
|
||||
}
|
||||
|
||||
val relays = extractRelays(nostrField, pubkey)
|
||||
return NamecoinNostrResult(
|
||||
pubkey = pubkey.lowercase(),
|
||||
relays = relays,
|
||||
namecoinName = parsed.namecoinName,
|
||||
localPart = resolvedLocalPart,
|
||||
)
|
||||
// Single-identity form: "nostr": { "pubkey": "hex", "relays": [...] }
|
||||
// Only resolves the root local-part; non-root requests against a
|
||||
// single-identity record fall through to null so we don't hand
|
||||
// alice@example.bit the root operator's identity.
|
||||
if (parsed.localPart == "_") {
|
||||
val pubkey = (nostrField["pubkey"] as? JsonPrimitive)?.content
|
||||
if (pubkey != null && isValidPubkey(pubkey)) {
|
||||
val relays =
|
||||
try {
|
||||
nostrField["relays"]?.jsonArray?.mapNotNull {
|
||||
(it as? JsonPrimitive)?.content
|
||||
} ?: emptyList()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
return NamecoinNostrResult(
|
||||
pubkey = pubkey.lowercase(),
|
||||
relays = relays,
|
||||
namecoinName = parsed.namecoinName,
|
||||
localPart = "_",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
+16
-4
@@ -457,7 +457,8 @@ class ElectrumXClient(
|
||||
/**
|
||||
* Parse a Namecoin name and value from a verbose transaction response.
|
||||
*
|
||||
* Scans each output for a NAME_UPDATE script (starts with OP_3 = 0x53),
|
||||
* Scans each output for a NAME_UPDATE (OP_3 = 0x53) or NAME_FIRSTUPDATE
|
||||
* (OP_2 = 0x52) script,
|
||||
* then extracts the name and value from the script's push data.
|
||||
*/
|
||||
private fun parseNameFromTransaction(
|
||||
@@ -482,8 +483,10 @@ class ElectrumXClient(
|
||||
?.content
|
||||
?: continue
|
||||
|
||||
// NAME_UPDATE scripts start with OP_3 (0x53)
|
||||
if (!scriptHex.startsWith("53")) continue
|
||||
// NAME_UPDATE scripts start with OP_3 (0x53);
|
||||
// NAME_FIRSTUPDATE scripts start with OP_2 (0x52). Both carry the
|
||||
// current value at the time of that on-chain operation.
|
||||
if (!scriptHex.startsWith("53") && !scriptHex.startsWith("52")) continue
|
||||
|
||||
val scriptBytes = hexToBytes(scriptHex)
|
||||
val parsed = parseNameScript(scriptBytes) ?: continue
|
||||
@@ -510,7 +513,9 @@ class ElectrumXClient(
|
||||
* @return Pair of (name, value) as strings, or null if parsing fails
|
||||
*/
|
||||
private fun parseNameScript(script: ByteArray): Pair<String, String>? {
|
||||
if (script.isEmpty() || script[0] != OP_NAME_UPDATE) return null
|
||||
if (script.isEmpty()) return null
|
||||
val op = script[0]
|
||||
if (op != OP_NAME_UPDATE && op != OP_NAME_FIRSTUPDATE) return null
|
||||
|
||||
var pos = 1
|
||||
|
||||
@@ -518,6 +523,12 @@ class ElectrumXClient(
|
||||
val (nameBytes, newPos1) = readPushData(script, pos) ?: return null
|
||||
pos = newPos1
|
||||
|
||||
// FIRSTUPDATE has an extra <rand> push between name and value; skip it.
|
||||
if (op == OP_NAME_FIRSTUPDATE) {
|
||||
val (_, newPos2) = readPushData(script, pos) ?: return null
|
||||
pos = newPos2
|
||||
}
|
||||
|
||||
// Read value
|
||||
val (valueBytes, _) = readPushData(script, pos) ?: return null
|
||||
|
||||
@@ -815,6 +826,7 @@ class ElectrumXClient(
|
||||
const val NAME_EXPIRE_DEPTH = 36_000
|
||||
|
||||
// Namecoin script opcodes
|
||||
private const val OP_NAME_FIRSTUPDATE: Byte = 0x52 // OP_2 repurposed by Namecoin
|
||||
private const val OP_NAME_UPDATE: Byte = 0x53 // OP_3 repurposed by Namecoin
|
||||
private const val OP_2DROP: Byte = 0x6d
|
||||
private const val OP_DROP: Byte = 0x75
|
||||
|
||||
Reference in New Issue
Block a user