From 5efead42fb73452a0f76d654b1224951654f6425 Mon Sep 17 00:00:00 2001 From: m Date: Fri, 8 May 2026 16:20:15 +1000 Subject: [PATCH] fix(quartz/electrumx): parse NAME_FIRSTUPDATE outputs alongside NAME_UPDATE Names whose latest on-chain transaction is still the initial registration (OP_NAME_FIRSTUPDATE = OP_2 = 0x52) were silently dropped because the parser only matched OP_NAME_UPDATE (OP_3 = 0x53). The scripthash index returns the FIRSTUPDATE tx in that case, so resolution looked 'unreachable' even though every server answered. Accept both opcodes when scanning vouts and when parsing the script. FIRSTUPDATE pushes , so skip the extra push before reading the value. --- .../namecoin/ElectrumXClient.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt index a48604a48..721a9a48e 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt @@ -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? { - 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 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