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.
*
* 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