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.
This commit is contained in:
Local Dev 2026-09-21 23:46:06 +02:00
parent 79a8a6fa16
commit e94399a1d2

View file

@ -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;
}