From e94399a1d2365eae968f77fb6cc19121846e7185 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Mon, 21 Sep 2026 23:46:06 +0200 Subject: [PATCH] market: a dollar listing must not crash the Sell panel, and the gateway must not forget a fresh one After a successful USD listing the panel tried to show the BCH-style offer hex, which dollar listings do not have, so the seller saw an error and a bare Cancel button. The panel now shows the contract address instead. The gateway pruned listings from its store whenever one electrum lookup did not show the covenant output. A listing a few seconds old can be invisible to one server while another already relayed it; deleting it then loses a real on-chain listing. Absence is now trusted only after 20 minutes; before that the listing is hidden from buyers but kept. --- src/gateway/public-gateway.mjs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/gateway/public-gateway.mjs b/src/gateway/public-gateway.mjs index 6d7b4b2..0c69abe 100644 --- a/src/gateway/public-gateway.mjs +++ b/src/gateway/public-gateway.mjs @@ -215,17 +215,26 @@ async function activeListings() { const stored = readMarket(); if (!stored.length) { _marketCheck = { at: Date.now(), list: [] }; return []; } const idx = await getIndex(); - const keep = []; + const keep = []; // shown to buyers + const store = []; // kept on disk (a superset: a fresh usd listing may be + // invisible to one electrum server for a while, and + // deleting it then loses a real on-chain listing) const byLock = new Map(); + const USD_GRACE_MS = 20 * 60 * 1000; for (const l of stored) { const entry = idx.get(l.name); if (l.kind === "usd") { - // Alive while the certificate still sits in the covenant. - if (!entry || entry.category !== l.category) continue; + // Alive while the certificate still sits in the covenant. Absence from + // listunspent is only trusted once the listing is old enough for every + // server to have seen its transaction; before that it is hidden, not + // forgotten. + const young = Date.now() - (Date.parse(l.created_at || "") || 0) < USD_GRACE_MS; + if (!entry || entry.category !== l.category) { if (young) store.push(l); continue; } const clock = usdListingLockHex(l); if (!byLock.has(clock)) byLock.set(clock, await listUnspentForLock(clock).catch(() => null)); const clist = byLock.get(clock); - if (clist && !clist.some((u) => u.tx_hash === l.outpoint.txid && u.tx_pos === l.outpoint.vout)) continue; // bought or cancelled + if (clist && !clist.some((u) => u.tx_hash === l.outpoint.txid && u.tx_pos === l.outpoint.vout)) { if (young) store.push(l); continue; } // bought or cancelled (or not relayed yet) + store.push(l); keep.push({ ...l, records: entry.records ?? {} }); continue; } @@ -234,9 +243,10 @@ async function activeListings() { if (!byLock.has(lock)) byLock.set(lock, await listUnspentForLock(lock).catch(() => null)); const list = byLock.get(lock); if (list && !list.some((u) => u.tx_hash === l.source_output.txid && u.tx_pos === l.source_output.vout)) continue; // spent + store.push(l); keep.push({ ...l, records: entry.records ?? {} }); } - if (keep.length !== stored.length) writeMarket(keep); + if (store.length !== stored.length) writeMarket(store); _marketCheck = { at: Date.now(), list: keep }; return keep; }