| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 98ec43f commit 1c0c756
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -683,7 +683,13 @@ export async function activate(api) { | |||
| 683 | 683 | }); | |
| 684 | 684 | } | |
| 685 | 685 | const settlements = (state.settlements[agent] || []).slice(-25).reverse() | |
| 686 | - .map((s) => ({ ...s, payout: s.payout / MICRO, at: new Date(s.at).toISOString() })); | ||
| 686 | + .map((s) => ({ | ||
| 687 | + ...s, | ||
| 688 | + payout: s.payout / MICRO, | ||
| 689 | + cost: (s.cost || 0) / MICRO, | ||
| 690 | + net: (s.payout - (s.cost || 0)) / MICRO, | ||
| 691 | + at: new Date(s.at).toISOString(), | ||
| 692 | + })); | ||
| 687 | 693 | return reply.send({ | |
| 688 | 694 | agent, | |
| 689 | 695 | balance: balanceOf(agent) / MICRO, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -215,14 +215,23 @@ export function applyEvent(state, ev, prices) { | |||
| 215 | 215 | } | |
| 216 | 216 | case 'market.settle': { | |
| 217 | 217 | const m = state.markets[ev.marketId]; | |
| 218 | - for (const [agent, micro] of Object.entries(ev.payouts)) { | ||
| 219 | - row(state, agent).balanceMicro += micro; | ||
| 218 | + // Everyone who HELD is given a receipt, not only those who were | ||
| 219 | + // paid: "you lost 12.40 on this" is the settlement a bettor most | ||
| 220 | + // needs to see, and a payout-only list silently drops it. | ||
| 221 | + const holders = new Set([...Object.keys(ev.payouts), ...Object.keys(m.positions)]); | ||
| 222 | + for (const agent of holders) { | ||
| 223 | + const micro = ev.payouts[agent] || 0; | ||
| 224 | + if (micro) row(state, agent).balanceMicro += micro; | ||
| 225 | + const pos = m.positions[agent]; | ||
| 226 | + if (!micro && !(pos && pos.shares.some((x) => x !== 0))) continue; | ||
| 220 | 227 | pushSettlement(state, agent, { | |
| 221 | 228 | market: m.id, | |
| 222 | 229 | title: m.title, | |
| 223 | 230 | status: ev.status, | |
| 224 | 231 | outcome: ev.status === 'resolved' ? m.resolvedOutcome : null, | |
| 225 | 232 | payout: micro, | |
| 233 | + // Cost basis of what was held, so the receipt can show net P&L. | ||
| 234 | + cost: pos ? pos.costMicro.reduce((a, x) => a + x, 0) : 0, | ||
| 226 | 235 | at: ev.t, | |
| 227 | 236 | }); | |
| 228 | 237 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -214,6 +214,12 @@ export function renderUi(prefix) { | |||
| 214 | 214 | const COLORS = ['#0f7d5c','#97a09c','#b45309','#1d4ed8','#7c3aed','#0e7490', | |
| 215 | 215 | '#a21caf','#4d7c0f','#b91c1c','#334155','#9a3412','#115e59']; | |
| 216 | 216 | let me = null, current = null, pick = 0, tab = 'open', cursor = null, quoteSeq = 0, lastQuote = null; | |
| 217 | + // One key per BET INTENT. Minting it per click defeats the point: the | ||
| 218 | + // case idempotency exists for is the user retrying after a timeout, | ||
| 219 | + // and a fresh key on the retry executes a second trade. It is renewed | ||
| 220 | + // when the intent changes (stake or outcome) or after one succeeds. | ||
| 221 | + let betKey = null; | ||
| 222 | + const newIntent = () => { betKey = null; }; | ||
| 217 | 223 | ||
| 218 | 224 | async function api(path, opts = {}) { | |
| 219 | 225 | const res = await fetch(API + path, { | |
@@ -318,8 +324,9 @@ export function renderUi(prefix) { | |||
| 318 | 324 | const cls = p.unrealizedPnl >= 0 ? 'up' : 'down'; | |
| 319 | 325 | const sign = p.unrealizedPnl >= 0 ? '+' : ''; | |
| 320 | 326 | return '<tr><td><a href="#m/' + esc(p.market) + '">' + esc(p.title) + '</a>' | |
| 321 | - + '<div class="meta">' + p.shares.map((s, i) => s > 0 | ||
| 322 | - ? cr(s) + ' × ' + esc(p.outcomes[i]) + ' @ ' + pct(p.prices[i]) : '').filter(Boolean).join(' · ') | ||
| 327 | + + '<div class="meta">' + p.shares.map((s, i) => (s > 0 | ||
| 328 | + ? cr(s) + ' × ' + esc(p.outcomes[i]) + ' — in at ' + pct(p.cost[i] / s) | ||
| 329 | + + ', now ' + pct(p.prices[i]) : '')).filter(Boolean).join(' · ') | ||
| 323 | 330 | + '</div></td>' | |
| 324 | 331 | + '<td class="num">' + cr(p.totalValue) + '</td>' | |
| 325 | 332 | + '<td class="num"><span class="pnl ' + cls + '">' + sign + cr(p.unrealizedPnl) + '</span></td>' | |
@@ -333,11 +340,14 @@ export function renderUi(prefix) { | |||
| 333 | 340 | } | |
| 334 | 341 | const s = $('settled'); | |
| 335 | 342 | s.innerHTML = me.settlements.length | |
| 336 | - ? '<table><tbody>' + me.settlements.slice(0, 10).map((x) => | ||
| 337 | - '<tr><td>' + esc(x.title) + '<div class="meta">' + esc(x.status) | ||
| 338 | - + (x.outcome != null ? '' : ' (voided)') + ' · ' + new Date(x.at).toLocaleString() + '</div></td>' | ||
| 339 | - + '<td class="num"><span class="pnl ' + (x.payout > 0 ? 'up' : 'down') + '">+' | ||
| 340 | - + cr(x.payout) + '</span></td></tr>').join('') + '</tbody></table>' | ||
| 343 | + ? '<table><tbody>' + me.settlements.slice(0, 10).map((x) => { | ||
| 344 | + const net = x.net === undefined ? x.payout : x.net; | ||
| 345 | + return '<tr><td>' + esc(x.title) + '<div class="meta">' + esc(x.status) | ||
| 346 | + + (x.outcome == null ? ' (voided)' : '') + ' · staked ' + cr(x.cost || 0) | ||
| 347 | + + ' · returned ' + cr(x.payout) + ' · ' + new Date(x.at).toLocaleString() + '</div></td>' | ||
| 348 | + + '<td class="num"><span class="pnl ' + (net >= 0 ? 'up' : 'down') + '">' | ||
| 349 | + + (net >= 0 ? '+' : '') + cr(net) + '</span></td></tr>'; | ||
| 350 | + }).join('') + '</tbody></table>' | ||
| 341 | 351 | : '<span class="hint">nothing settled yet</span>'; | |
| 342 | 352 | } | |
| 343 | 353 | ||
@@ -426,7 +436,8 @@ export function renderUi(prefix) { | |||
| 426 | 436 | $('d-meta').innerHTML = '<span class="status ' + esc(m.status) + '">' + esc(m.status) + '</span>' | |
| 427 | 437 | + (m.status === 'resolved' && m.resolvedOutcome != null ? ' → <b>' + esc(m.outcomes[m.resolvedOutcome]) + '</b>' : '') | |
| 428 | 438 | + (m.status === 'resolving' ? ' → <b>' + esc(m.outcomes[m.resolvedOutcome]) + '</b> · settles ' + new Date(m.settleAt).toLocaleTimeString() + ' (disputable)' : '') | |
| 429 | - + ' · ' + (m.tradable ? countdown(m.closesAt) : 'closed ' + new Date(m.closesAt).toLocaleString()) | ||
| 439 | + + ' · <span id="d-countdown">' + (m.tradable ? countdown(m.closesAt) | ||
| 440 | + : 'closed ' + new Date(m.closesAt).toLocaleString()) + '</span>' | ||
| 430 | 441 | + ' · pool ' + cr(m.liquidity) + ' · ' + m.trades + ' trades' | |
| 431 | 442 | + (m.description ? '<br>' + esc(m.description) : ''); | |
| 432 | 443 | priceBar($('d-bar'), m.outcomes, m.prices); | |
@@ -439,7 +450,7 @@ export function renderUi(prefix) { | |||
| 439 | 450 | + '<span class="px">' + pct(m.prices[i]) + ' · ' + (1 / Math.max(m.prices[i], 1e-6)).toFixed(2) + '</span>' | |
| 440 | 451 | + '</button>').join(''); | |
| 441 | 452 | $('d-outcomes').querySelectorAll('.out-btn').forEach((b) => { | |
| 442 | - b.onclick = () => { pick = Number(b.dataset.i); renderDetail(m.id, true); }; | ||
| 453 | + b.onclick = () => { pick = Number(b.dataset.i); newIntent(); renderDetail(m.id, true); }; | ||
| 443 | 454 | }); | |
| 444 | 455 | $('t-pick').textContent = m.outcomes[pick]; | |
| 445 | 456 | ||
@@ -510,11 +521,13 @@ export function renderUi(prefix) { | |||
| 510 | 521 | $('t-msg').textContent = ''; $('t-msg').className = 'msg'; | |
| 511 | 522 | $('t-buy').disabled = true; | |
| 512 | 523 | try { | |
| 524 | + if (!betKey) betKey = uid(); | ||
| 513 | 525 | const r = await api('/markets/' + current.id + '/trade', { | |
| 514 | 526 | method: 'POST', | |
| 515 | - headers: { 'idempotency-key': uid() }, | ||
| 527 | + headers: { 'idempotency-key': betKey }, | ||
| 516 | 528 | body: JSON.stringify({ side: 'buy', outcome: pick, spend: Number($('t-stake').value), maxCost: lastQuote.total * 1.02 }), | |
| 517 | 529 | }); | |
| 530 | + betKey = null; // that intent is spent; the next bet is a new one | ||
| 518 | 531 | toast('Bet placed — ' + cr(r.shares) + ' × ' + r.outcomeLabel + ' to win ' + cr(r.toWin)); | |
| 519 | 532 | await refreshMe(); await renderDetail(current.id, true); | |
| 520 | 533 | } catch (e) { | |
@@ -563,7 +576,7 @@ export function renderUi(prefix) { | |||
| 563 | 576 | let searchTimer; | |
| 564 | 577 | $('search').oninput = () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { cursor = null; renderList(); }, 250); }; | |
| 565 | 578 | let quoteTimer; | |
| 566 | - $('t-stake').oninput = () => { clearTimeout(quoteTimer); quoteTimer = setTimeout(quote, 220); }; | ||
| 579 | + $('t-stake').oninput = () => { newIntent(); clearTimeout(quoteTimer); quoteTimer = setTimeout(quote, 220); }; | ||
| 567 | 580 | document.querySelectorAll('[data-stake]').forEach((b) => { | |
| 568 | 581 | b.onclick = () => { | |
| 569 | 582 | $('t-stake').value = b.dataset.stake === 'max' ? Math.floor((me ? me.balance : 0) * 100) / 100 : b.dataset.stake; | |
@@ -572,9 +585,15 @@ export function renderUi(prefix) { | |||
| 572 | 585 | }); | |
| 573 | 586 | $('o-resolve').onclick = () => { | |
| 574 | 587 | const i = Number($('o-outcome').value); | |
| 575 | - act('resolve', { outcome: i }, | ||
| 576 | - 'Resolve "' + current.title + '" as "' + current.outcomes[i] + '"?\\n\\n' | ||
| 577 | - + 'This pays out the whole pool after the dispute window. It cannot be undone.'); | ||
| 588 | + const label = current.outcomes[i]; | ||
| 589 | + // Typed confirmation, not a one-click OK: this pays out the entire | ||
| 590 | + // pool and there is no undo. | ||
| 591 | + const typed = prompt('Resolve "' + current.title + '" as "' + label + '".\\n\\n' | ||
| 592 | + + 'This pays out the whole pool after the dispute window and cannot be undone.\\n' | ||
| 593 | + + 'Type the winning outcome to confirm:'); | ||
| 594 | + if (typed === null) return; | ||
| 595 | + if (typed.trim().toLowerCase() !== label.toLowerCase()) { toast('Not resolved — that did not match "' + label + '"'); return; } | ||
| 596 | + act('resolve', { outcome: i }); | ||
| 578 | 597 | }; | |
| 579 | 598 | $('o-void').onclick = () => act('void', {}, 'Void this market? Every share is redeemed at the average price over the window before close.'); | |
| 580 | 599 | $('o-close').onclick = () => act('close', {}); | |
@@ -633,7 +652,16 @@ export function renderUi(prefix) { | |||
| 633 | 652 | }; | |
| 634 | 653 | } | |
| 635 | 654 | connect(); | |
| 636 | - setInterval(() => { if (current) { const el = $('d-meta'); if (el && current.tradable) renderDetail(current.id, true); } }, 30000); | ||
| 655 | + // Tick the countdown locally every second; only re-fetch when the | ||
| 656 | + // market actually crosses its close time and the UI must change state. | ||
| 657 | + setInterval(() => { | ||
| 658 | + if (!current || !current.tradable) return; | ||
| 659 | + const el = $('d-countdown'); | ||
| 660 | + if (!el) return; | ||
| 661 | + const left = new Date(current.closesAt).getTime() - Date.now(); | ||
| 662 | + if (left <= 0) renderDetail(current.id, true); | ||
| 663 | + else el.textContent = countdown(current.closesAt); | ||
| 664 | + }, 1000); | ||
| 637 | 665 | escrowPreview(); | |
| 638 | 666 | refreshMe(true).then(route); | |
| 639 | 667 | })(); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments