diff --git a/design/potidaea/app.html b/design/potidaea/app.html index 4dd46fb..4a1272c 100644 --- a/design/potidaea/app.html +++ b/design/potidaea/app.html @@ -731,6 +731,44 @@ .ma-ico { width: 32px; height: 32px; } } + /* ============ Swap-sheet error chip ============ */ + /* Compact error surface on the review sheet: red mark + short code chip + + * one-line headline + copy / details toggle. Full text lives in a + * collapsible block below so raw broadcast/covenant dumps don't blow up + * the sheet vertically. */ + .wc-sheet-err { + font-family: var(--font-mono); font-size: 11px; + color: #ff7a8a; + background: rgba(255,122,138,.06); + border: 1px solid rgba(255,122,138,.28); + border-radius: 10px; + padding: 10px 12px; margin: 10px 0; + line-height: 1.55; + } + .wc-sheet-err .wc-err-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + .wc-sheet-err .wc-err-mark { color: #ff7a8a; font-size: 13px; } + .wc-sheet-err .wc-err-code { + font-family: var(--font-mono); font-size: 9px; letter-spacing: .08em; + padding: 2px 6px; border-radius: 4px; + color: #ff7a8a; border: 1px solid rgba(255,122,138,.32); + background: rgba(255,122,138,.08); + white-space: nowrap; flex: none; + } + .wc-sheet-err .wc-err-head { flex: 1; min-width: 0; color: var(--ink-bright); font-size: 12px; } + .wc-sheet-err .wc-err-btn { + background: transparent; border: 1px solid rgba(255,122,138,.22); + color: rgba(255,122,138,.7); border-radius: 5px; + padding: 2px 6px; font-family: var(--font-mono); font-size: 11px; line-height: 1; + cursor: pointer; transition: color .15s, border-color .15s; + flex: none; + } + .wc-sheet-err .wc-err-btn:hover { color: #ff7a8a; border-color: rgba(255,122,138,.5); } + .wc-sheet-err .wc-err-full { + margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(255,122,138,.22); + font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-soft); + max-height: 240px; overflow: auto; word-break: break-word; white-space: pre-wrap; + } + /* ============ Tx-receipt mini-buttons (copy / view on explorer) ============ */ /* Small square action buttons that sit next to a rendered txid across * the swap done sheet, wizard genesis done, and publish overlay. */ @@ -752,6 +790,12 @@ * card, not a half-empty grid behind it. */ #v-swap.v-swap-welcome-mode .od-layout { display: none; } .welcome-overlay { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - 140px); padding: 40px 20px; } + /* Explicit [hidden] override — an author `display: flex` rule wins over + * the user-agent stylesheet's `[hidden] { display: none }`, so we have + * to spell it out or the container keeps eating a full viewport of + * vertical space (and shoves the real swap layout to the bottom of the + * page) even when overlay.hidden = true. */ + .welcome-overlay[hidden] { display: none !important; } .welcome-card { max-width: 640px; width: 100%; background: var(--surface-1); border: 1px solid var(--border); diff --git a/design/potidaea/src/main.ts b/design/potidaea/src/main.ts index 838d088..12e3eb8 100644 --- a/design/potidaea/src/main.ts +++ b/design/potidaea/src/main.ts @@ -788,15 +788,87 @@ function applyOptimisticSwapDelta(kind: 'claim' | 'redeem', amountUnits: bigint, try { paintOdAll(); } catch { /* noop */ } } +/** Classify a raw error message into a short code + one-line headline the + * user can scan without reading a wall of text. Broadcast errors from + * Bitauth / cashscript often dump the failing tx as a multi-KB hex blob; + * the review sheet is not the place for that. Full text stays in the + * details section for copy-and-paste debugging. */ +function classifySwapError(raw: string): { code: string; headline: string; full: string } { + const full = raw.trim(); + // Pattern → { code, headline extractor } + const patterns: Array<{ re: RegExp; code: string; head?: (m: RegExpMatchArray) => string }> = [ + { re: /^insufficient\s+([a-z0-9]+):.*need\s+(\d+).*have\s+(\d+)/i, code: 'E-INSUFFICIENT', + head: (m) => `Not enough ${m[1].toUpperCase()} — need ${m[2]}, have ${m[3]}` }, + { re: /amount too small|below dust|feeSats\s*>=?/i, code: 'E-DUST', + head: () => 'Amount below the covenant’s dust floor' }, + { re: /change below dust/i, code: 'E-CHANGE-DUST', + head: () => 'BCH change would fall below the dust threshold' }, + { re: /covenant may be draining|no state utxo/i, code: 'E-COVENANT-STATE', + head: () => 'Registry covenant state UTXO missing' }, + { re: /64: bad-txns-inputs-missingorspent|missing inputs/i, code: 'E-STALE-UTXO', + head: () => 'Chosen UTXOs already spent — endpoint index is stale' }, + { re: /min relay fee|not enough fee/i, code: 'E-FEE-LOW', + head: () => 'Miner fee too low to relay' }, + { re: /timeout|connect failed|not connected|electrum:/i, code: 'E-NET', + head: () => 'Electrum endpoint unreachable' }, + { re: /require\s+statement\s+failed|failed\s+at\s+input/i, code: 'E-COVENANT', + head: (m) => { + const m2 = full.match(/DeviantOpenMintFT\.cash:(\d+)/i); + return m2 ? `Covenant require() failed at line ${m2[1]}` : 'Covenant require() failed'; + } }, + { re: /leftover dust padding/i, code: 'E-LEFTOVER-DUST', + head: () => 'Leftover after redeem would fall below dust' }, + { re: /^redeem too small/i, code: 'E-REDEEM-SMALL', + head: () => 'Redeem amount too small to cover miner fee' }, + ]; + for (const p of patterns) { + const m = full.match(p.re); + if (m) return { code: p.code, headline: p.head ? p.head(m) : m[0], full }; + } + // Fallback — take the first sentence-ish chunk (up to first period, dash, + // or 90 chars) as the headline; everything else lives in details. + const first = full.split(/[.\n]|—/, 1)[0].trim().slice(0, 90); + return { code: 'E-OTHER', headline: first || 'Swap failed', full }; +} + function setSheetError(msg: string): void { let el = document.getElementById('wc-sheet-err') as HTMLElement | null; if (!el) { el = document.createElement('div'); el.id = 'wc-sheet-err'; - el.style.cssText = 'font-family:var(--font-mono);font-size:11px;color:#ff7a8a;background:rgba(255,122,138,.08);border:1px solid rgba(255,122,138,.28);border-radius:8px;padding:8px 10px;margin:10px 0;'; const rev = document.getElementById('reviewSheet'); rev?.insertBefore(el, rev.firstChild); } - el.textContent = '✗ ' + msg; + el.className = 'wc-sheet-err'; + const { code, headline, full } = classifySwapError(msg); + const detailsCollapsed = full.length > 120; + el.innerHTML = + '
' + + '✗' + + '' + escHtml(code) + '' + + '' + escHtml(headline) + '' + + '' + + (detailsCollapsed + ? '' + : '') + + '
' + + (detailsCollapsed + ? '' + : (full !== headline ? '
' + escHtml(full) + '
' : '')); + el.querySelector('[data-err-copy]')?.addEventListener('click', async (ev) => { + ev.stopPropagation(); + try { + await navigator.clipboard.writeText('[' + code + '] ' + full); + const btn = ev.currentTarget as HTMLButtonElement; + const orig = btn.textContent; + btn.textContent = '✓'; + setTimeout(() => { btn.textContent = orig; }, 1000); + } catch { /* clipboard blocked — silent */ } + }); + el.querySelector('[data-err-toggle]')?.addEventListener('click', (ev) => { + ev.stopPropagation(); + const details = el!.querySelector('.wc-err-full'); + if (details) details.hidden = !details.hidden; + }); } function wireSwapConfirmInterceptor(): void {