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 <name> <rand> <value>, so skip the extra <rand> push
before reading the value.
This commit is contained in:
m
2026-05-08 16:20:15 +10:00
parent 4ab9cae213
commit 5efead42fb
@@ -457,7 +457,8 @@ class ElectrumXClient(
/** /**
* Parse a Namecoin name and value from a verbose transaction response. * 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. * then extracts the name and value from the script's push data.
*/ */
private fun parseNameFromTransaction( private fun parseNameFromTransaction(
@@ -482,8 +483,10 @@ class ElectrumXClient(
?.content ?.content
?: continue ?: continue
// NAME_UPDATE scripts start with OP_3 (0x53) // NAME_UPDATE scripts start with OP_3 (0x53);
if (!scriptHex.startsWith("53")) continue // 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 scriptBytes = hexToBytes(scriptHex)
val parsed = parseNameScript(scriptBytes) ?: continue val parsed = parseNameScript(scriptBytes) ?: continue
@@ -510,7 +513,9 @@ class ElectrumXClient(
* @return Pair of (name, value) as strings, or null if parsing fails * @return Pair of (name, value) as strings, or null if parsing fails
*/ */
private fun parseNameScript(script: ByteArray): Pair<String, String>? { 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 var pos = 1
@@ -518,6 +523,12 @@ class ElectrumXClient(
val (nameBytes, newPos1) = readPushData(script, pos) ?: return null val (nameBytes, newPos1) = readPushData(script, pos) ?: return null
pos = newPos1 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 // Read value
val (valueBytes, _) = readPushData(script, pos) ?: return null val (valueBytes, _) = readPushData(script, pos) ?: return null
@@ -815,6 +826,7 @@ class ElectrumXClient(
const val NAME_EXPIRE_DEPTH = 36_000 const val NAME_EXPIRE_DEPTH = 36_000
// Namecoin script opcodes // 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_NAME_UPDATE: Byte = 0x53 // OP_3 repurposed by Namecoin
private const val OP_2DROP: Byte = 0x6d private const val OP_2DROP: Byte = 0x6d
private const val OP_DROP: Byte = 0x75 private const val OP_DROP: Byte = 0x75