Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bubblerumble4.gno

45.95 Kb · 1336 lines
   1// Package bubblerumble4 is the fourth Bubble Rumble realm. v3's game stays whole:
   2// of every bid 55% goes to the pot, 30% is credited at once to the pool's earlier
   3// bidders pro-rata to what they paid in, 10% to whoever invited the bidder, 5% to
   4// the pool's creator; every pool has a last day, a flag war at the close, credited
   5// earnings and Withdraw, a clock that can be bought down but never under MinClock.
   6// What v4 changes is the shot: it can now win the pot.
   7//
   8//   - A shot is still extra money on a bid, a chance to pop the bubble at once.
   9//     The chance is 0.8 × shot / take, at most 1% a shot, where the take is what
  10//     a pop would pay the shooter right now: the pot less the team's 35%, times the
  11//     winner's live share (LiveBps). So the pot keeps a fifth of every shot's fair
  12//     value however far the last day has slid, and a hit pops the bubble: the pool
  13//     closes with the shooter as its winner and Claim pays it as any pop.
  14//   - The draw's randomness is a commit-reveal by the keeper — the operator's own
  15//     box, which also settles pools on time. The keeper publishes sha256 hashes of
  16//     secrets ahead of time (Commit); a bid with a shot takes the next unused one;
  17//     the shot is drawn when someone (the keeper, or anyone) reveals that secret
  18//     (Reveal) in the blocks ShotDelay to ShotDelay+ShotWindow after the bid. The
  19//     seed is the secret with the shot's own facts and the reveal block's height
  20//     and time. Nobody without the secret can grind it: not the shooter, not a
  21//     validator. The secret's holder knows the two candidate block times a block
  22//     ahead and may pick which of the window's three blocks to reveal in — three
  23//     rolls, at most 2.4× fair on a 1% shot — which is why the keeper key and its
  24//     operator never bid or shoot.
  25//   - A shot not revealed in its window, or on a pool that closed first, is void:
  26//     it goes into the pot and counts as a miss, as in v3. Nothing is refunded, so
  27//     withholding a reveal never pays whoever withholds it. What remains is that a
  28//     keeper who leads a pot could withhold a stranger's hit and let the money fall
  29//     into the pot it leads: refused shots are counted on the pool for all to see,
  30//     and so are reveals that came in the window's later blocks (the honest keeper
  31//     always reveals at the first), so a keeper choosing its block would show.
  32//   - Shots are sold only while the keeper is alive: a shot needs a free commitment
  33//     and a sign of life (Commit or Heartbeat) within KeeperFresh. Otherwise the bid
  34//     stands and the shot's money goes straight back in the same transaction.
  35//   - Shots are refused in the last quarter of a pool's life (which also keeps every
  36//     reveal window clear of the last day) and under MinShot. RevealTip is taken
  37//     from every shot and credited to the keeper whoever sends the reveal (the
  38//     secret sits in the mempool, and a proposer could copy it), so the keeper
  39//     pays for itself. The keeper's own shots are refused outright.
  40//   - Reveals go in shot order, so among two pending shots nobody chooses which pops.
  41//
  42// Residuals, stated plainly: the secret holder's three rolls; the keeper-defender
  43// veto above; a proposer who reads the keeper's reveal in the mempool holds the
  44// secret too, and proposing the window's blocks could drop the reveal for a roll
  45// it does not like or leave it out altogether (a void), which Late and Voided
  46// show; keeper and validator together could grind. SetShots is the switch if any
  47// of it is seen. SetKeeper drops every unused commitment.
  48package bubblerumble4
  49
  50import (
  51	"chain"
  52	"chain/banker"
  53	"chain/runtime"
  54	"chain/runtime/unsafe"
  55	"crypto/sha256"
  56	"encoding/binary"
  57	"encoding/hex"
  58	"math"
  59	"math/bits"
  60	"strconv"
  61	"strings"
  62	"time"
  63)
  64
  65const (
  66	MinWindow    int64 = 10                // seconds
  67	MaxWindow    int64 = 30 * 24 * 3600    // 30 days
  68	MinLife      int64 = 3600              // an hour
  69	MaxLife      int64 = 30 * 24 * 3600    // 30 days
  70	MinBasePrice int64 = 1_000             // ugnot, 0.001 GNOT
  71	MaxBasePrice int64 = 1_000_000_000_000 // ugnot, 1,000,000 GNOT
  72	MaxNameLen         = 40
  73	HistoryLen         = 50  // bids kept per pool for the feed
  74	RosterMax          = 100 // distinct bidders a pool pays dividends, team shares and the close split to
  75	MaxBoost     int64 = 10  // the shortest clock a bid can buy is window/MaxBoost...
  76	MinClock     int64 = 300 // ...and never under five minutes: people must be able to answer
  77
  78	// where every bid goes, in percent
  79	PotShare     int64 = 55
  80	DivShare     int64 = 30 // to earlier bidders of the pool, pro-rata by weight
  81	RefShare     int64 = 10 // to the bidder's inviter
  82	CreatorShare int64 = 5
  83	// of the pot at the close, to the other bidders under the winner's flag
  84	TeamShare int64 = 35
  85
  86	ShotMaxBps    int64 = 100     // 1% a shot, whatever is paid
  87	ShotFair      int64 = 8000    // chance in bps = ShotFair × shot / take (0.8 of fair odds)
  88	ShotDelay     int64 = 3       // blocks after the shot was paid for before it can be revealed...
  89	ShotWindow    int64 = 2       // ...and how many more blocks it may still be revealed in: three blocks, as v3 drew; three rolls at most for the secret's holder
  90	MinShot       int64 = 100_000 // ugnot: no shot under 0.1 GNOT, so dust cannot make reveals cost more than they tip
  91	RevealTip     int64 = 20_000  // ugnot: off every shot, to whoever reveals it
  92	KeeperFresh   int64 = 300     // seconds: shots are sold only this soon after the keeper's last sign of life (it heartbeats every two minutes)
  93	LiveFloor     int64 = 2500    // bps: no shots once the winner's live share has slid under a quarter (which also keeps every reveal window clear of the last day)
  94	MaxCommits          = 500     // unused commitments the queue holds at most
  95	CommitBatch         = 100     // hashes one Commit may bring
  96	CommitCompact       = 200     // used commitments kept before the queue is compacted
  97)
  98
  99// admin is the realm's deployer: the only address that may turn shots off.
 100const admin = address("g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr")
 101
 102// pkgPath must match gnomod.toml: the realm's own address derives from it.
 103const pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble4"
 104
 105type Entry struct {
 106	N      int64
 107	Bidder address
 108	Flag   string
 109	Price  int64 // what was paid for the bid itself; at least the price at the time
 110	Shot   int64 // extra paid for a chance to pop the bubble
 111	Clock  int64 // seconds this bid had to stand
 112	Pot    int64 // pot right after this bid
 113	At     time.Time
 114}
 115
 116// Member is a distinct bidder of a pool: dividends, team shares and the close
 117// split go to the roster. The flag is the one flown at the first bid here.
 118type Member struct {
 119	Addr   address
 120	Flag   string
 121	Paid   int64 // gross paid into this pool (bids and shots)
 122	Earned int64 // dividends received from this pool
 123}
 124
 125// Shot is a shot at the bubble waiting to be revealed, or resolved.
 126type Shot struct {
 127	N        int64 // 1-based within the pool
 128	Bidder   address
 129	Paid     int64    // ugnot paid for it (the tip comes out of this at the reveal)
 130	Take     int64    // what a pop would have paid the shooter when the odds were set
 131	Bps      int64    // chance, in basis points
 132	CommitH  int64    // block height when paid; revealable at CommitH+ShotDelay and the ShotWindow blocks after
 133	Hash     string   // the commitment it took: sha256 of the secret that decides it, 32 raw bytes
 134	Resolved bool
 135	Won      bool
 136	Void     bool // not revealed in its window, or the pool closed first: a miss
 137	DrawH    int64
 138	Revealer address
 139}
 140
 141type Pool struct {
 142	ID          int64
 143	Name        string
 144	Creator     address
 145	Window      int64 // seconds the last bid must stand to win
 146	Life        int64 // seconds from the first bid to the last day
 147	BasePrice   int64 // ugnot; the Nth bid costs BasePrice·√N
 148	Seed        int64
 149	Pot         int64
 150	Bids        int64
 151	LastBidder  address
 152	LastBidAt   time.Time
 153	FirstBidAt  time.Time
 154	Clock       int64 // seconds the current top bid has to stand
 155	CreatedAt   time.Time
 156	Paid        bool // the pot went out, or the seed back to the creator
 157	Cancelled   bool
 158	Popped      bool // a shot ended it
 159	PoppedAt    time.Time
 160	DivPaid     int64 // dividends paid out of this pool so far
 161	RefPaid     int64
 162	CreatorPaid int64
 163	ShotPaid    int64 // slices paid to hits while the pool went on
 164	Misses      int64 // shots resolved and lost, voids included: the scars on the bubble
 165	Voided      int64 // of those, the shots nobody revealed in time
 166	Late        int64 // shots revealed in their window but not at its first block: a keeper choosing its block would show here
 167	ShotEscrow  int64 // shots paid for and not yet resolved: in the pot once they are
 168	WinnerFlag  string
 169	TeamPaid    int64 // what the winner's team received at the close
 170	SharedPaid  int64 // the pro-rata part of the close
 171	WinnerPaid  int64
 172	History     []Entry
 173	Roster      []*Member
 174	Shots       []*Shot
 175	Settled     int // shots before this index are all resolved (they resolve strictly in order): where settleShots starts
 176}
 177
 178// Player is what the realm remembers about an address across pools.
 179type Player struct {
 180	Referrer  address // set by the first bid, permanent
 181	Recruits  int64
 182	Flag      string
 183	Bids      int64
 184	Paid      int64
 185	DivEarned int64
 186	RefEarned int64
 187	Won       int64 // pots, team shares, close splits and shot slices received
 188	Shots     int64
 189	Hits      int64
 190	Owed      int64 // credited and not yet withdrawn
 191	Withdrawn int64
 192}
 193
 194var (
 195	shotsOn    = true
 196	keeper     address    // who commits and heartbeats (SetKeeper); the admin may too
 197	keeperSeen time.Time  // the keeper's last Commit or Heartbeat
 198	commits    []string   // the hashes of the keeper's secrets (32 raw bytes each), in order; the first head are used
 199	head       int
 200	voided     int64 // shots voided over all pools
 201	late       int64 // shots revealed after the first block of their window, over all pools
 202	pools      []*Pool
 203	players   = map[address]*Player{}
 204	teamPaid  = map[string]int64{} // flag -> gross bid volume under it
 205	teamWon   = map[string]int64{} // flag -> pots closed under it
 206	teamSize  = map[string]int64{} // flag -> players flying it
 207	realmAddr = chain.PackageAddress(pkgPath)
 208)
 209
 210// Price of the next bid in a pool with the given base price and n bids so far.
 211func Price(base, n int64) int64 {
 212	return int64(math.Ceil(float64(base) * math.Sqrt(float64(n+1))))
 213}
 214
 215// LastDay is when the pool's life runs out; zero before the first bid.
 216func (p *Pool) LastDay() time.Time {
 217	if p.FirstBidAt.IsZero() {
 218		return time.Time{}
 219	}
 220	return p.FirstBidAt.Add(time.Duration(p.Life) * time.Second)
 221}
 222
 223// Deadline is when the pool closes if nothing changes: the clock or the last
 224// day, whichever comes first; the pop, if a shot ended it.
 225func (p *Pool) Deadline() time.Time {
 226	if p.Popped {
 227		return p.PoppedAt
 228	}
 229	d := p.LastBidAt.Add(time.Duration(p.Clock) * time.Second)
 230	if last := p.LastDay(); !last.IsZero() && last.Before(d) {
 231		return last
 232	}
 233	return d
 234}
 235
 236func (p *Pool) Over() bool       { return p.Bids > 0 && (p.Popped || !time.Now().Before(p.Deadline())) }
 237func (p *Pool) NextPrice() int64 { return Price(p.BasePrice, p.Bids) }
 238
 239// LiveBps is how much of the pot still goes the winner's way, in basis points
 240// of the pot: 10000 at the first bid, sliding to 0 on the last day. Frozen at
 241// the close.
 242func (p *Pool) LiveBps() int64 {
 243	if p.FirstBidAt.IsZero() || p.Life <= 0 {
 244		return 10000
 245	}
 246	at := time.Now()
 247	if p.Over() {
 248		at = p.Deadline()
 249	}
 250	elapsed := at.Unix() - p.FirstBidAt.Unix()
 251	if elapsed <= 0 {
 252		return 10000
 253	}
 254	if elapsed >= p.Life {
 255		return 0
 256	}
 257	return 10000 * (p.Life - elapsed) / p.Life
 258}
 259
 260// ClockFor is the clock a bid buys by paying sent against a price: the window
 261// divided by the multiple paid, never below window/MaxBoost.
 262func ClockFor(window, price, sent int64) int64 {
 263	c := int64(float64(window) * float64(price) / float64(sent))
 264	if c < window/MaxBoost {
 265		c = window / MaxBoost
 266	}
 267	if c < MinClock {
 268		c = MinClock
 269	}
 270	if c > window {
 271		c = window
 272	}
 273	return c
 274}
 275
 276// mulDiv is a×b/c without overflowing on the way: pots and paid-ins are both
 277// in ugnot and their product passes int64 at a few thousand GNOT each.
 278func mulDiv(a, b, c int64) int64 {
 279	if a < 0 || b < 0 || c <= 0 {
 280		panic("mulDiv: negative")
 281	}
 282	hi, lo := bits.Mul64(uint64(a), uint64(b))
 283	if hi >= uint64(c) {
 284		panic("mulDiv: overflow")
 285	}
 286	q, _ := bits.Div64(hi, lo, uint64(c))
 287	return int64(q)
 288}
 289
 290// ShotBps is the chance a shot buys against what a pop would pay, in basis points.
 291func ShotBps(shot, take int64) int64 {
 292	if shot <= 0 || take <= 0 {
 293		return 0
 294	}
 295	bps := ShotFair * shot / take
 296	if bps > ShotMaxBps {
 297		bps = ShotMaxBps
 298	}
 299	return bps
 300}
 301
 302// Take is the most a pop could pay this shooter if the pool closed now on a pot
 303// of this size: Claim's own arithmetic, run dry, with the team's share counted
 304// as the shooter's too — one bid from a friend (or a second address) under a
 305// flag nobody else flies collects it, so the odds must assume it is collected.
 306// What is not the shooter's is the slide: the part of the rest that the last
 307// day has moved to the whole roster by paid-in.
 308func (p *Pool) Take(shooter address, pot int64) int64 {
 309	toWinner, cut := p.payout(shooter, pot, false)
 310	return toWinner + cut
 311}
 312
 313// CreatePool opens a pool. Any ugnot sent with the call seeds its pot. The
 314// creator earns CreatorShare of every bid. life is the pool's life in seconds
 315// from its first bid; 0 means ten windows (at least an hour, at most 30 days).
 316func CreatePool(cur realm, name string, window int64, basePrice int64, life int64) int64 {
 317	creator := userCaller(cur)
 318	name = cleanName(name)
 319	if window < MinWindow || window > MaxWindow {
 320		panic("window must be between 10s and 30d")
 321	}
 322	if basePrice < MinBasePrice || basePrice > MaxBasePrice {
 323		panic("base price must be between 0.001 and 1,000,000 GNOT")
 324	}
 325	if life == 0 { // ten windows of fighting, within the bounds
 326		life = 10 * window
 327		if life < MinLife {
 328			life = MinLife
 329		}
 330		if life > MaxLife {
 331			life = MaxLife
 332		}
 333	}
 334	if life < MinLife || life > MaxLife {
 335		panic("life must be between 1h and 30d")
 336	}
 337	if life < window {
 338		panic("life must be at least the window")
 339	}
 340	seed := ugnotSent()
 341	p := newPool(name, creator, window, basePrice, life, seed)
 342	chain.Emit("PoolCreated", "id", itoa(p.ID), "creator", creator.String(), "seed", itoa(seed))
 343	return p.ID
 344}
 345
 346func newPool(name string, creator address, window, basePrice, life, seed int64) *Pool {
 347	p := &Pool{
 348		ID: int64(len(pools)) + 1, Name: name, Creator: creator, Window: window, Life: life,
 349		BasePrice: basePrice, Seed: seed, Pot: seed, CreatedAt: time.Now(),
 350	}
 351	pools = append(pools, p)
 352	return p
 353}
 354
 355// Bid pays at least the current price and resets the clock. ref is the address
 356// that invited the bidder ("" for none; only the first bid ever sets it), flag
 357// a two-letter country to play under ("" keeps the last one; the flag is locked
 358// per pool at the first bid there), shot how much of the money sent is a shot
 359// at popping the bubble (0 for none): the rest must cover the price, and paying
 360// more than the price shortens the clock.
 361func Bid(cur realm, id int64, ref string, flag string, shot int64) {
 362	bidder := userCaller(cur)
 363	p := pool(id)
 364	sent := ugnotSent()
 365	if p.Cancelled {
 366		panic("pool was cancelled")
 367	}
 368	// a pool that closed (or popped) since the bidder looked: the money goes back
 369	if p.Over() {
 370		if sent > 0 {
 371			send(cur, bidder, sent)
 372			chain.Emit("Refund", "id", itoa(id), "to", bidder.String(), "ugnot", itoa(sent))
 373		}
 374		if p.Popped {
 375			return
 376		}
 377		panic("pool is closed")
 378	}
 379	if shot < 0 || shot > sent {
 380		panic("shot must be between 0 and the amount sent")
 381	}
 382	price := p.NextPrice()
 383	bid := sent - shot
 384	if bid < price {
 385		panic("send at least " + itoa(price) + "ugnot for the bid, got " + itoa(bid) + "ugnot")
 386	}
 387	// shots of earlier bids whose window has passed are settled first
 388	p.settleShots()
 389	pl := player(bidder)
 390	if f := cleanFlag(flag); f != "" && f != pl.Flag {
 391		if pl.Flag != "" {
 392			teamSize[pl.Flag]--
 393		}
 394		pl.Flag = f
 395		teamSize[f]++
 396	}
 397	if pl.Referrer == "" && ref != "" {
 398		r := address(ref)
 399		if r.IsValid() && r != bidder {
 400			pl.Referrer = r
 401			player(r).Recruits++
 402		}
 403	}
 404
 405	// where the bid goes
 406	div, refCut, creatorCut := bid*DivShare/100, bid*RefShare/100, bid*CreatorShare/100
 407	toPot := bid - div - refCut - creatorCut
 408	if creatorCut > 0 {
 409		send(cur, p.Creator, creatorCut)
 410		p.CreatorPaid += creatorCut
 411	}
 412	if pl.Referrer != "" && refCut > 0 {
 413		send(cur, pl.Referrer, refCut)
 414		p.RefPaid += refCut
 415		player(pl.Referrer).RefEarned += refCut
 416	} else {
 417		toPot += refCut
 418	}
 419	divPaid := div - p.payDividends(cur, bidder, div)
 420	toPot += div - divPaid
 421	p.Pot += toPot
 422
 423	m := p.member(bidder, pl.Flag)
 424	if m.Flag == "" { // a flagless first bid does not lock "no flag": the first flag flown here does
 425		m.Flag = pl.Flag
 426	}
 427	// the shot, if there can be one: else its money goes straight back and the bid stands
 428	why := ""
 429	if shot > 0 {
 430		why = p.shotRefusal(bidder, shot)
 431	}
 432	m.Paid += sent
 433	pl.Bids++
 434	pl.Paid += sent
 435	if pl.Flag != "" {
 436		teamPaid[pl.Flag] += sent
 437	}
 438	// the odds, on the pool as it stands with this bid booked (the shooter's own paid-in weighs in the slide):
 439	// priced here once, and a shot that buys no chance at all is refused like the rest
 440	var take, bps int64
 441	if shot > 0 && why == "" {
 442		take = p.Take(bidder, p.Pot+shot-RevealTip) // the pot a pop would pay from: this bid's share in, the shot itself in, the tip out
 443		bps = ShotBps(shot-RevealTip, take)
 444		if bps == 0 {
 445			why = "too small for any chance at this pot"
 446		}
 447	}
 448	if why != "" {
 449		m.Paid -= shot
 450		pl.Paid -= shot
 451		if pl.Flag != "" {
 452			teamPaid[pl.Flag] -= shot
 453		}
 454		send(cur, bidder, shot)
 455		chain.Emit("ShotReturned", "id", itoa(id), "bidder", bidder.String(), "ugnot", itoa(shot), "why", why)
 456		sent -= shot
 457		shot = 0
 458	}
 459	p.Bids++
 460	if p.FirstBidAt.IsZero() {
 461		p.FirstBidAt = time.Now()
 462	}
 463	p.LastBidder = bidder
 464	p.LastBidAt = time.Now()
 465	p.Clock = ClockFor(p.Window, price, bid)
 466	p.History = append(p.History, Entry{N: p.Bids, Bidder: bidder, Flag: m.Flag, Price: bid, Shot: shot, Clock: p.Clock, Pot: p.Pot, At: p.LastBidAt})
 467	if len(p.History) > HistoryLen {
 468		p.History = p.History[len(p.History)-HistoryLen:]
 469	}
 470	chain.Emit("Bid", "id", itoa(id), "n", itoa(p.Bids), "bidder", bidder.String(), "paid", itoa(bid), "shot", itoa(shot), "pot", itoa(p.Pot), "div", itoa(divPaid))
 471
 472	if shot > 0 {
 473		t := &Shot{N: int64(len(p.Shots)) + 1, Bidder: bidder, Paid: shot, Take: take, Bps: bps, CommitH: runtime.ChainHeight(), Hash: commits[head]}
 474		head++
 475		p.Shots = append(p.Shots, t)
 476		p.ShotEscrow += shot
 477		pl.Shots++
 478		chain.Emit("Shot", "id", itoa(id), "shot", itoa(t.N), "bidder", bidder.String(), "paid", itoa(shot), "take", itoa(take), "bps", itoa(bps), "commit", hex.EncodeToString([]byte(t.Hash)), "revealAt", itoa(t.CommitH+ShotDelay))
 479	}
 480}
 481
 482// shotRefusal is why a shot cannot be sold on this pool right now ("" when it
 483// can): the checks a bid runs before taking one, in the order the page shows.
 484// The odds themselves are checked by the caller, on the booked state.
 485func (p *Pool) shotRefusal(shooter address, shot int64) string {
 486	switch {
 487	case !shotsOn:
 488		return "shots are off"
 489	case keeper != "" && shooter == keeper:
 490		return "the keeper never shoots"
 491	case shot < MinShot:
 492		return "under the smallest shot, " + GNOT(MinShot)
 493	case head >= len(commits):
 494		return "no commitment free: the keeper is away"
 495	case keeperSeen.IsZero() || time.Now().Unix()-keeperSeen.Unix() > KeeperFresh:
 496		return "the keeper has not been seen for " + dur(KeeperFresh)
 497	case p.LiveBps() < LiveFloor:
 498		return "the winner's share has slid under a quarter"
 499	}
 500	return ""
 501}
 502
 503// SetShots turns shots off (or on again); only the deployer may.
 504func SetShots(cur realm, on bool) {
 505	if userCaller(cur) != admin {
 506		panic("only the deployer")
 507	}
 508	shotsOn = on
 509	chain.Emit("Shots", "on", jbool(on))
 510}
 511
 512// SetKeeper names the address that commits and heartbeats; only the deployer
 513// may. Every unused commitment is dropped with it: a rotated or compromised
 514// keeper's secrets never decide a shot.
 515func SetKeeper(cur realm, addr address) {
 516	if userCaller(cur) != admin {
 517		panic("only the deployer")
 518	}
 519	if !addr.IsValid() {
 520		panic("bad address")
 521	}
 522	keeper = addr
 523	commits = commits[:head]
 524	chain.Emit("Keeper", "addr", addr.String())
 525}
 526
 527func keeperCaller(cur realm) address {
 528	who := userCaller(cur)
 529	if who == "" || who != keeper {
 530		panic("only the keeper")
 531	}
 532	return who
 533}
 534
 535func keeperOrAdmin(cur realm) address {
 536	who := userCaller(cur)
 537	if who != admin && (who == "" || who != keeper) {
 538		panic("only the keeper or the deployer")
 539	}
 540	return who
 541}
 542
 543// Heartbeat is the keeper's sign of life: shots are sold only within
 544// KeeperFresh of the last one (a Commit counts too).
 545func Heartbeat(cur realm) {
 546	keeperCaller(cur)
 547	keeperSeen = time.Now()
 548}
 549
 550// Commit queues the hashes of secrets the keeper will reveal later, in order,
 551// as a comma-separated list of 64 hex characters each; only the keeper, whose
 552// box holds the secrets. A hash still in the queue (used or not) is refused: a
 553// repeated secret would be public after its first reveal (a safety net against
 554// the keeper repeating itself; compaction forgets the oldest). The queue holds
 555// at most MaxCommits unused; the used ones in front of it are compacted away
 556// past CommitCompact.
 557func Commit(cur realm, hashes string) {
 558	keeperCaller(cur)
 559	keeperSeen = time.Now()
 560	parts := strings.Split(hashes, ",")
 561	if len(parts) == 0 || len(parts) > CommitBatch {
 562		panic("1 to " + strconv.Itoa(CommitBatch) + " hashes")
 563	}
 564	if len(commits)-head+len(parts) > MaxCommits {
 565		panic("the queue holds " + strconv.Itoa(MaxCommits) + " unused commitments at most")
 566	}
 567	if head > CommitCompact {
 568		commits = append([]string(nil), commits[head:]...)
 569		head = 0
 570	}
 571	for _, part := range parts {
 572		b, err := hex.DecodeString(strings.TrimSpace(part))
 573		if err != nil || len(b) != 32 {
 574			panic("a hash is 64 hex characters")
 575		}
 576		h := string(b)
 577		for _, c := range commits { // a safety net against the keeper repeating itself, over what the queue still holds
 578			if c == h {
 579				panic("hash already committed: " + part)
 580			}
 581		}
 582		commits = append(commits, h)
 583	}
 584	chain.Emit("Commit", "n", itoa(int64(len(parts))), "free", itoa(int64(len(commits)-head)))
 585}
 586
 587// Retire drops the first n unused commitments: a keeper that lost the secrets
 588// behind them (the box rebuilt, the file gone) clears them so shots can go on.
 589// The deployer may too. It is not a sign of life.
 590func Retire(cur realm, n int64) {
 591	keeperOrAdmin(cur)
 592	if n <= 0 {
 593		panic("retire at least one")
 594	}
 595	if int(n) > len(commits)-head {
 596		n = int64(len(commits) - head)
 597	}
 598	commits = append(commits[:head], commits[head+int(n):]...)
 599	chain.Emit("Retire", "n", itoa(n), "free", itoa(int64(len(commits)-head)))
 600}
 601
 602// Reveal draws shots with the secrets behind their commitments: a comma-separated
 603// list of pool:shot:secretHex, any pools, in one transaction — the keeper sends
 604// every shot that is due in one call, so a run of shots on one block never
 605// outpaces it. Anyone may call it; the secret is the proof, and the caller is
 606// tipped RevealTip out of each shot revealed. Shots go in order: an earlier one
 607// still pending must come first (in the list, or before). An entry not yet due
 608// is skipped, one with a wrong secret or already resolved is skipped with an
 609// event, so a stale entry never voids the rest; a malformed one (no such pool
 610// or shot, a cancelled pool, not pool:shot:secret) aborts the whole call, which
 611// only the keeper's own bug could send. The reveal must land in the
 612// blocks CommitH+ShotDelay through CommitH+ShotDelay+ShotWindow; later, or on a
 613// pool that has closed, the shot is already void (settleShots runs first): into
 614// the pot as a miss. A hit pops the bubble: the pool closes now with the shooter
 615// as its winner, settled at Claim.
 616func Reveal(cur realm, reveals string) {
 617	who := userCaller(cur)
 618	h := runtime.ChainHeight()
 619	done := 0
 620	for _, entry := range strings.Split(reveals, ",") {
 621		f := strings.Split(strings.TrimSpace(entry), ":")
 622		if len(f) != 3 {
 623			panic("each entry is pool:shot:secret")
 624		}
 625		id, n := parseID(f[0]), parseNum(f[1])
 626		p := pool(id)
 627		if p.Cancelled {
 628			panic("pool was cancelled")
 629		}
 630		p.settleShots()
 631		if n < 1 || int(n) > len(p.Shots) {
 632			panic("no such shot")
 633		}
 634		t := p.Shots[n-1]
 635		if t.Resolved || (n > 1 && !p.Shots[n-2].Resolved) {
 636			chain.Emit("RevealSkipped", "id", itoa(id), "shot", itoa(n), "why", jif(t.Resolved, "resolved", "earlier shot pending"))
 637			continue
 638		}
 639		if h < t.CommitH+ShotDelay {
 640			continue // not due yet: the keeper sends a block early to be sure of the window
 641		}
 642		secret, err := hex.DecodeString(strings.TrimSpace(f[2]))
 643		sum := sha256.Sum256(secret)
 644		if err != nil || string(sum[:]) != t.Hash {
 645			chain.Emit("RevealSkipped", "id", itoa(id), "shot", itoa(n), "why", "wrong secret")
 646			continue
 647		}
 648		// (a shot past its window, or on a pool that closed, was voided by settleShots above and skipped as resolved)
 649		done++
 650		t.Resolved, t.DrawH, t.Revealer = true, h, who
 651		p.ShotEscrow -= t.Paid
 652		// the tip is the keeper's whoever sent the reveal: the secret sits in the mempool, and a proposer could
 653		// copy it into a reveal of its own; the seed does not care who reveals, so nothing else changes
 654		tip := RevealTip
 655		if tip > t.Paid {
 656			tip = t.Paid
 657		}
 658		if keeper != "" {
 659			credit(keeper, tip)
 660		} else {
 661			credit(who, tip)
 662		}
 663		p.Pot += t.Paid - tip
 664		if h > t.CommitH+ShotDelay { // revealed in the window but not at its first block: the honest keeper's tell, counted
 665			p.Late++
 666			late++
 667		}
 668		t.Won = shotHits(seedFor(id, t, secret, h), t.Bps)
 669		chain.Emit("Revealed", "id", itoa(id), "shot", itoa(n), "bidder", t.Bidder.String(), "hit", jbool(t.Won), "bps", itoa(t.Bps), "by", who.String())
 670		if !t.Won {
 671			p.Misses++
 672			continue
 673		}
 674		player(t.Bidder).Hits++
 675		p.Popped, p.PoppedAt, p.LastBidder = true, time.Now(), t.Bidder
 676		chain.Emit("Popped", "id", itoa(id), "by", t.Bidder.String(), "pot", itoa(p.Pot))
 677	}
 678	if done == 0 {
 679		panic("nothing revealed")
 680	}
 681}
 682
 683// Settle voids the shots at a pool that nobody revealed in time, or that its
 684// close overtook. Anyone may call it; every bid, reveal and claim on the pool
 685// does the same first.
 686func Settle(cur realm, id int64) {
 687	pool(id).settleShots()
 688}
 689
 690// settleShots voids, in order, every pending shot whose window has passed or
 691// whose pool is over: the shot's money goes into the pot and counts as a miss;
 692// nobody is tipped.
 693func (p *Pool) settleShots() {
 694	h := runtime.ChainHeight()
 695	over := p.Over()
 696	for p.Settled < len(p.Shots) && p.Shots[p.Settled].Resolved {
 697		p.Settled++
 698	}
 699	for _, t := range p.Shots[p.Settled:] {
 700		if t.Resolved || (h <= t.CommitH+ShotDelay+ShotWindow && !over) {
 701			continue
 702		}
 703		t.Resolved, t.Void, t.DrawH = true, true, h
 704		p.ShotEscrow -= t.Paid
 705		p.Pot += t.Paid
 706		p.Misses++
 707		p.Voided++
 708		voided++
 709		chain.Emit("Voided", "id", itoa(p.ID), "shot", itoa(t.N), "bidder", t.Bidder.String(), "why", jif(over, "closed", "unrevealed"))
 710	}
 711}
 712
 713func jif(c bool, a, b string) string {
 714	if c {
 715		return a
 716	}
 717	return b
 718}
 719
 720// seedFor is the draw's seed: the secret with the shot's own facts and the
 721// reveal block's height and time. Everything but the secret is public, and the
 722// secret is the keeper's until the reveal; the block's time is the validators'.
 723func seedFor(poolID int64, t *Shot, secret []byte, h int64) uint64 {
 724	var b []byte
 725	b = append(b, pkgPath...)
 726	b = binary.BigEndian.AppendUint64(b, uint64(poolID))
 727	b = binary.BigEndian.AppendUint64(b, uint64(t.N))
 728	b = append(b, t.Bidder.String()...)
 729	b = binary.BigEndian.AppendUint64(b, uint64(t.CommitH))
 730	b = append(b, secret...)
 731	b = binary.BigEndian.AppendUint64(b, uint64(h))
 732	b = binary.BigEndian.AppendUint64(b, uint64(time.Now().UnixNano()))
 733	sum := sha256.Sum256(b)
 734	return binary.BigEndian.Uint64(sum[:8])
 735}
 736
 737// shotHits is the draw: a uniform number in [0, 10000) under the chance.
 738func shotHits(seed uint64, bps int64) bool {
 739	return int64(seed%10000) < bps
 740}
 741
 742// payDividends credits div to the pool's earlier bidders, pro-rata to what
 743// each has paid in, skipping the bidder; whatever cannot be placed (no earlier
 744// bidders, rounding) is returned to go into the pot.
 745func (p *Pool) payDividends(cur realm, bidder address, div int64) int64 {
 746	var total int64
 747	for _, m := range p.Roster {
 748		if m.Addr != bidder {
 749			total += m.Paid
 750		}
 751	}
 752	if total == 0 || div == 0 {
 753		return div
 754	}
 755	var paid int64
 756	for _, m := range p.Roster {
 757		if m.Addr == bidder {
 758			continue
 759		}
 760		amt := mulDiv(div, m.Paid, total)
 761		if amt > 0 {
 762			credit(m.Addr, amt)
 763			m.Earned += amt
 764			player(m.Addr).DivEarned += amt
 765			paid += amt
 766		}
 767	}
 768	p.DivPaid += paid
 769	return div - paid
 770}
 771
 772// member finds or adds a bidder on the roster; a full roster drops its oldest.
 773// A new member's flag is the one given; once set it is locked for this pool.
 774func (p *Pool) member(a address, flag string) *Member {
 775	for _, m := range p.Roster {
 776		if m.Addr == a {
 777			return m
 778		}
 779	}
 780	if len(p.Roster) >= RosterMax {
 781		p.Roster = p.Roster[1:]
 782	}
 783	m := &Member{Addr: a, Flag: flag}
 784	p.Roster = append(p.Roster, m)
 785	return m
 786}
 787
 788// Claim settles a finished pool: everything is credited, and Withdraw sends
 789// it. TeamShare of the pot goes to the OTHER roster members flying the
 790// winner's flag, pro-rata by paid-in — or, when nobody else flew it, to all
 791// the other roster members. Of the rest, the live part (LiveBps) is the
 792// winner's and the shared part is split among the whole roster pro-rata by
 793// paid-in. A pool with a single bidder credits them everything. Rounding goes
 794// to the winner. Anyone may call it.
 795func Claim(cur realm, id int64) {
 796	p := pool(id)
 797	if p.Cancelled {
 798		panic("pool was cancelled")
 799	}
 800	p.settleShots()
 801	if !p.Over() {
 802		panic("clock still running")
 803	}
 804	if p.Paid {
 805		panic("already paid out")
 806	}
 807	p.Paid = true
 808	winner := p.LastBidder
 809	flag := ""
 810	if m := p.find(winner); m != nil {
 811		flag = m.Flag
 812	}
 813	p.WinnerFlag = flag
 814	toWinner, _ := p.payout(winner, p.Pot, true)
 815	if toWinner > 0 {
 816		credit(winner, toWinner)
 817		player(winner).Won += toWinner
 818	}
 819	p.WinnerPaid = toWinner
 820	if flag != "" {
 821		teamWon[flag] += p.Pot
 822	}
 823	chain.Emit("Paid", "id", itoa(id), "winner", winner.String(), "pot", itoa(p.Pot), "winnerPaid", itoa(toWinner), "team", itoa(p.TeamPaid), "shared", itoa(p.SharedPaid), "liveBps", itoa(p.LiveBps()))
 824}
 825
 826// payout is the close's arithmetic for a winner and a pot: TeamShare to the
 827// other roster members under the winner's flag (or, when nobody else flew it,
 828// to all the others), then the shared part of the rest — what the live share
 829// has slid away from — to the whole roster pro-rata by paid-in; the winner
 830// keeps the rest and the rounding. With pay it credits and books the shares;
 831// dry, it only says what the winner would get and what the team would (Take
 832// prices shots with both).
 833func (p *Pool) payout(winner address, pot int64, pay bool) (toWinner, teamCut int64) {
 834	flag := ""
 835	if m := p.find(winner); m != nil {
 836		flag = m.Flag
 837	}
 838	var team int64
 839	for _, m := range p.Roster {
 840		if m.Addr != winner && m.Flag == flag && flag != "" {
 841			team += m.Paid
 842		}
 843	}
 844	teamFlag := flag
 845	if team == 0 {
 846		teamFlag = "" // nobody else flew it: every other bidder is the team
 847		for _, m := range p.Roster {
 848			if m.Addr != winner {
 849				team += m.Paid
 850			}
 851		}
 852	}
 853	toWinner = pot
 854	if team == 0 {
 855		return toWinner, 0 // alone on the roster: everything
 856	}
 857	cut := pot * TeamShare / 100
 858	for _, m := range p.Roster {
 859		if m.Addr == winner || (teamFlag != "" && m.Flag != teamFlag) {
 860			continue
 861		}
 862		amt := mulDiv(cut, m.Paid, team)
 863		if amt > 0 {
 864			toWinner -= amt
 865			teamCut += amt
 866			if pay {
 867				credit(m.Addr, amt)
 868				player(m.Addr).Won += amt
 869				p.TeamPaid += amt
 870			}
 871		}
 872	}
 873	// the rest slides: the live part stays the winner's, the shared part is everyone's by paid-in
 874	rest := pot - cut
 875	shared := rest - mulDiv(rest, p.LiveBps(), 10000)
 876	if shared > 0 {
 877		var total int64
 878		for _, m := range p.Roster {
 879			total += m.Paid
 880		}
 881		for _, m := range p.Roster {
 882			amt := mulDiv(shared, m.Paid, total)
 883			if amt > 0 && m.Addr != winner {
 884				toWinner -= amt
 885				if pay {
 886					credit(m.Addr, amt)
 887					player(m.Addr).Won += amt
 888					p.SharedPaid += amt
 889				}
 890			}
 891		}
 892	}
 893	return toWinner, teamCut
 894}
 895
 896// Cancel lets the creator take the seed back from a pool nobody has bid on.
 897func Cancel(cur realm, id int64) {
 898	caller := userCaller(cur)
 899	p := pool(id)
 900	if p.Creator != caller {
 901		panic("only the creator can cancel")
 902	}
 903	if p.Bids > 0 {
 904		panic("pool already has bids")
 905	}
 906	if p.Cancelled {
 907		panic("already cancelled")
 908	}
 909	p.Cancelled = true
 910	p.Paid = true
 911	if p.Pot > 0 {
 912		send(cur, caller, p.Pot)
 913	}
 914	chain.Emit("Cancelled", "id", itoa(id))
 915}
 916
 917func (p *Pool) find(a address) *Member {
 918	for _, m := range p.Roster {
 919		if m.Addr == a {
 920			return m
 921		}
 922	}
 923	return nil
 924}
 925
 926func player(a address) *Player {
 927	pl := players[a]
 928	if pl == nil {
 929		pl = &Player{}
 930		players[a] = pl
 931	}
 932	return pl
 933}
 934
 935func send(cur realm, to address, ugnot int64) {
 936	banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr, to, chain.Coins{{"ugnot", ugnot}})
 937}
 938
 939// credit books ugnot to an address; Withdraw pays it out.
 940func credit(to address, ugnot int64) {
 941	player(to).Owed += ugnot
 942}
 943
 944// Withdraw sends the caller everything credited to them: dividends, team
 945// shares, close splits, slices and won pots.
 946func Withdraw(cur realm) int64 {
 947	who := userCaller(cur)
 948	pl := player(who)
 949	amt := pl.Owed
 950	if amt <= 0 {
 951		panic("nothing to withdraw")
 952	}
 953	pl.Owed = 0
 954	pl.Withdrawn += amt
 955	send(cur, who, amt)
 956	chain.Emit("Withdrawn", "to", who.String(), "ugnot", itoa(amt))
 957	return amt
 958}
 959
 960// Owed is what an address could withdraw right now.
 961func Owed(a address) int64 { return player(a).Owed }
 962
 963func pool(id int64) *Pool {
 964	if id < 1 || id > int64(len(pools)) {
 965		panic("no such pool")
 966	}
 967	return pools[id-1]
 968}
 969
 970// userCaller is the signer of the transaction, refusing calls relayed through
 971// another realm so nobody can bid on someone else's behalf, and so no realm can
 972// pay for a shot and revert on a miss.
 973func userCaller(cur realm) address {
 974	prev := cur.Previous()
 975	if !prev.IsUserCall() {
 976		panic("must be a direct user call")
 977	}
 978	return prev.Address()
 979}
 980
 981func ugnotSent() int64 {
 982	sent := unsafe.OriginSend()
 983	for _, c := range sent {
 984		if c.Denom != "ugnot" {
 985			panic("only ugnot is accepted")
 986		}
 987	}
 988	return sent.AmountOf("ugnot")
 989}
 990
 991func cleanName(s string) string {
 992	s = strings.TrimSpace(s)
 993	if s == "" || len(s) > MaxNameLen {
 994		panic("name must be 1-40 characters")
 995	}
 996	for _, c := range s {
 997		if c < 0x20 || c == 0x7f {
 998			panic("name has control characters")
 999		}
1000	}
1001	return s
1002}
1003
1004// cleanFlag accepts a two-letter country code, upper-cased; anything else is "".
1005func cleanFlag(s string) string {
1006	s = strings.ToUpper(strings.TrimSpace(s))
1007	if len(s) != 2 || s[0] < 'A' || s[0] > 'Z' || s[1] < 'A' || s[1] > 'Z' {
1008		return ""
1009	}
1010	return s
1011}
1012
1013func itoa(n int64) string { return strconv.FormatInt(n, 10) }
1014
1015func parseID(s string) int64 {
1016	n, err := strconv.ParseInt(s, 10, 64)
1017	if err != nil {
1018		panic("bad pool id")
1019	}
1020	return n
1021}
1022
1023func parseNum(s string) int64 {
1024	n, err := strconv.ParseInt(s, 10, 64)
1025	if err != nil {
1026		panic("bad number")
1027	}
1028	return n
1029}
1030
1031// GNOT formats ugnot as GNOT with the trailing zeros trimmed.
1032func GNOT(ugnot int64) string {
1033	whole, frac := ugnot/1_000_000, ugnot%1_000_000
1034	if frac == 0 {
1035		return itoa(whole) + " GNOT"
1036	}
1037	f := strconv.FormatInt(1_000_000+frac, 10)[1:]
1038	return itoa(whole) + "." + strings.TrimRight(f, "0") + " GNOT"
1039}
1040
1041func short(a address) string {
1042	s := a.String()
1043	if len(s) < 12 {
1044		return s
1045	}
1046	return s[:8] + "…" + s[len(s)-4:]
1047}
1048
1049func dur(sec int64) string {
1050	if sec <= 0 {
1051		return "0s"
1052	}
1053	d, h, m, s := sec/86400, sec%86400/3600, sec%3600/60, sec%60
1054	out := ""
1055	if d > 0 {
1056		out += itoa(d) + "d "
1057	}
1058	if d > 0 || h > 0 {
1059		out += itoa(h) + "h "
1060	}
1061	if d > 0 || h > 0 || m > 0 {
1062		out += itoa(m) + "m "
1063	}
1064	return out + itoa(s) + "s"
1065}
1066
1067// --- rendering ---------------------------------------------------------------
1068
1069// Render serves gnoweb ("" and "<id>") and the page ("json", "json/<id>",
1070// "me/<address>", "teams").
1071func Render(path string) string {
1072	path = strings.Trim(path, "/")
1073	switch {
1074	case path == "json":
1075		return jsonAll()
1076	case strings.HasPrefix(path, "json/"):
1077		return pool(parseID(path[5:])).json(true)
1078	case strings.HasPrefix(path, "me/"):
1079		return jsonMe(address(path[3:]))
1080	case path == "teams":
1081		return jsonTeams()
1082	case path == "commits":
1083		return jsonCommits()
1084	case path == "":
1085		return markdownAll()
1086	default:
1087		return pool(parseID(path)).markdown()
1088	}
1089}
1090
1091func unix(t time.Time) string {
1092	if t.IsZero() {
1093		return "0"
1094	}
1095	return itoa(t.Unix())
1096}
1097
1098func jbool(b bool) string {
1099	if b {
1100		return "true"
1101	}
1102	return "false"
1103}
1104
1105func (p *Pool) json(detail bool) string {
1106	var b strings.Builder
1107	b.WriteString("{\"id\":" + itoa(p.ID))
1108	b.WriteString(",\"name\":" + strconv.Quote(p.Name))
1109	b.WriteString(",\"creator\":\"" + p.Creator.String() + "\"")
1110	b.WriteString(",\"window\":" + itoa(p.Window))
1111	b.WriteString(",\"life\":" + itoa(p.Life))
1112	b.WriteString(",\"basePrice\":" + itoa(p.BasePrice))
1113	b.WriteString(",\"seed\":" + itoa(p.Seed))
1114	b.WriteString(",\"pot\":" + itoa(p.Pot))
1115	b.WriteString(",\"bids\":" + itoa(p.Bids))
1116	b.WriteString(",\"nextPrice\":" + itoa(p.NextPrice()))
1117	b.WriteString(",\"lastBidder\":\"" + p.LastBidder.String() + "\"")
1118	b.WriteString(",\"lastBidAt\":" + unix(p.LastBidAt))
1119	b.WriteString(",\"firstBidAt\":" + unix(p.FirstBidAt))
1120	b.WriteString(",\"deadline\":" + unix(p.Deadline()))
1121	b.WriteString(",\"lastDay\":" + unix(p.LastDay()))
1122	b.WriteString(",\"liveBps\":" + itoa(p.LiveBps()))
1123	b.WriteString(",\"clock\":" + itoa(p.Clock))
1124	b.WriteString(",\"createdAt\":" + unix(p.CreatedAt))
1125	b.WriteString(",\"over\":" + jbool(p.Over()))
1126	b.WriteString(",\"popped\":" + jbool(p.Popped))
1127	b.WriteString(",\"paid\":" + jbool(p.Paid))
1128	b.WriteString(",\"cancelled\":" + jbool(p.Cancelled))
1129	b.WriteString(",\"bidders\":" + itoa(int64(len(p.Roster))))
1130	b.WriteString(",\"divPaid\":" + itoa(p.DivPaid))
1131	b.WriteString(",\"refPaid\":" + itoa(p.RefPaid))
1132	b.WriteString(",\"creatorPaid\":" + itoa(p.CreatorPaid))
1133	b.WriteString(",\"shotPaid\":" + itoa(p.ShotPaid))
1134	b.WriteString(",\"teamPaid\":" + itoa(p.TeamPaid))
1135	b.WriteString(",\"sharedPaid\":" + itoa(p.SharedPaid))
1136	b.WriteString(",\"winnerPaid\":" + itoa(p.WinnerPaid))
1137	pending := int64(0)
1138	for _, t := range p.Shots {
1139		if !t.Resolved {
1140			pending++
1141		}
1142	}
1143	b.WriteString(",\"shots\":" + itoa(int64(len(p.Shots))) + ",\"misses\":" + itoa(p.Misses) + ",\"voided\":" + itoa(p.Voided) + ",\"late\":" + itoa(p.Late) + ",\"pending\":" + itoa(pending) + ",\"escrow\":" + itoa(p.ShotEscrow))
1144	flag := p.WinnerFlag
1145	if m := p.find(p.LastBidder); m != nil && flag == "" {
1146		flag = m.Flag
1147	}
1148	b.WriteString(",\"flag\":\"" + flag + "\"")
1149	// what each flag has paid into this pool
1150	b.WriteString(",\"teams\":{")
1151	seen := map[string]int64{}
1152	order := []string{}
1153	for _, m := range p.Roster {
1154		if m.Flag == "" {
1155			continue
1156		}
1157		if _, ok := seen[m.Flag]; !ok {
1158			order = append(order, m.Flag)
1159		}
1160		seen[m.Flag] += m.Paid
1161	}
1162	for i, f := range order {
1163		if i > 0 {
1164			b.WriteString(",")
1165		}
1166		b.WriteString("\"" + f + "\":" + itoa(seen[f]))
1167	}
1168	b.WriteString("}")
1169	if detail {
1170		b.WriteString(",\"history\":[")
1171		for i := len(p.History) - 1; i >= 0; i-- { // newest first
1172			h := p.History[i]
1173			if i < len(p.History)-1 {
1174				b.WriteString(",")
1175			}
1176			b.WriteString("{\"n\":" + itoa(h.N) + ",\"bidder\":\"" + h.Bidder.String() + "\",\"flag\":\"" + h.Flag + "\",\"price\":" + itoa(h.Price) + ",\"shot\":" + itoa(h.Shot) + ",\"clock\":" + itoa(h.Clock) + ",\"pot\":" + itoa(h.Pot) + ",\"at\":" + unix(h.At) + "}")
1177		}
1178		b.WriteString("],\"roster\":[")
1179		for i, m := range p.Roster {
1180			if i > 0 {
1181				b.WriteString(",")
1182			}
1183			b.WriteString("{\"addr\":\"" + m.Addr.String() + "\",\"flag\":\"" + m.Flag + "\",\"paid\":" + itoa(m.Paid) + ",\"earned\":" + itoa(m.Earned) + "}")
1184		}
1185		b.WriteString("],\"shotlog\":[")
1186		for i := len(p.Shots) - 1; i >= 0; i-- { // newest first
1187			t := p.Shots[i]
1188			if i < len(p.Shots)-1 {
1189				b.WriteString(",")
1190			}
1191			b.WriteString("{\"n\":" + itoa(t.N) + ",\"bidder\":\"" + t.Bidder.String() + "\",\"paid\":" + itoa(t.Paid) + ",\"take\":" + itoa(t.Take) + ",\"bps\":" + itoa(t.Bps) + ",\"commitH\":" + itoa(t.CommitH) + ",\"commit\":\"" + hex.EncodeToString([]byte(t.Hash)) + "\",\"revealAt\":" + itoa(t.CommitH+ShotDelay) + ",\"revealBy\":" + itoa(t.CommitH+ShotDelay+ShotWindow) + ",\"resolved\":" + jbool(t.Resolved) + ",\"hit\":" + jbool(t.Won) + ",\"void\":" + jbool(t.Void) + ",\"drawH\":" + itoa(t.DrawH) + "}")
1192		}
1193		b.WriteString("]")
1194	}
1195	b.WriteString("}")
1196	return b.String()
1197}
1198
1199// jsonCommits lists the unused commitments in order, hex, so a keeper that
1200// starts can tell which of them it holds the secrets for and Retire the rest.
1201func jsonCommits() string {
1202	var b strings.Builder
1203	b.WriteString("{\"height\":" + itoa(runtime.ChainHeight()) + ",\"commits\":[")
1204	for i := head; i < len(commits); i++ {
1205		if i > head {
1206			b.WriteString(",")
1207		}
1208		b.WriteString("\"" + hex.EncodeToString([]byte(commits[i])) + "\"")
1209	}
1210	b.WriteString("]}")
1211	return b.String()
1212}
1213
1214// commitHead is the next commitment a shot would take, hex, "" when none is free.
1215func commitHead() string {
1216	if head >= len(commits) {
1217		return ""
1218	}
1219	return hex.EncodeToString([]byte(commits[head]))
1220}
1221
1222func jsonAll() string {
1223	var b strings.Builder
1224	b.WriteString("{\"now\":" + itoa(time.Now().Unix()) + ",\"height\":" + itoa(runtime.ChainHeight()) + ",\"realm\":\"" + pkgPath + "\"")
1225	b.WriteString(",\"split\":{\"pot\":" + itoa(PotShare) + ",\"div\":" + itoa(DivShare) + ",\"ref\":" + itoa(RefShare) + ",\"creator\":" + itoa(CreatorShare) + ",\"team\":" + itoa(TeamShare) + ",\"roster\":" + itoa(int64(RosterMax)) +
1226		",\"shotMaxBps\":" + itoa(ShotMaxBps) + ",\"shotFair\":" + itoa(ShotFair) + ",\"shotDelay\":" + itoa(ShotDelay) + ",\"shotWindow\":" + itoa(ShotWindow) + ",\"minClock\":" + itoa(MinClock) +
1227		",\"minShot\":" + itoa(MinShot) + ",\"revealTip\":" + itoa(RevealTip) + ",\"keeperFresh\":" + itoa(KeeperFresh) + ",\"liveFloor\":" + itoa(LiveFloor) + "}")
1228	b.WriteString(",\"shotsOn\":" + jbool(shotsOn))
1229	b.WriteString(",\"keeper\":\"" + keeper.String() + "\",\"keeperSeen\":" + unix(keeperSeen) + ",\"commitsFree\":" + itoa(int64(len(commits)-head)) + ",\"commitHead\":\"" + commitHead() + "\",\"voided\":" + itoa(voided) + ",\"late\":" + itoa(late))
1230	b.WriteString(",\"pools\":[")
1231	for i, p := range pools {
1232		if i > 0 {
1233			b.WriteString(",")
1234		}
1235		b.WriteString(p.json(false))
1236	}
1237	b.WriteString("],\"teams\":" + jsonTeams() + "}")
1238	return b.String()
1239}
1240
1241// jsonTeams is the country leaderboard: paid in, won, and players per flag.
1242func jsonTeams() string {
1243	var b strings.Builder
1244	b.WriteString("{")
1245	i := 0
1246	for f, paid := range teamPaid {
1247		if i > 0 {
1248			b.WriteString(",")
1249		}
1250		i++
1251		b.WriteString("\"" + f + "\":{\"paid\":" + itoa(paid) + ",\"won\":" + itoa(teamWon[f]) + ",\"players\":" + itoa(teamSize[f]) + "}")
1252	}
1253	b.WriteString("}")
1254	return b.String()
1255}
1256
1257// jsonMe is what the page shows a connected wallet about itself.
1258func jsonMe(a address) string {
1259	pl := players[a]
1260	if pl == nil {
1261		pl = &Player{}
1262	}
1263	return "{\"addr\":\"" + a.String() + "\",\"referrer\":\"" + pl.Referrer.String() + "\",\"recruits\":" + itoa(pl.Recruits) +
1264		",\"flag\":\"" + pl.Flag + "\",\"bids\":" + itoa(pl.Bids) + ",\"paid\":" + itoa(pl.Paid) +
1265		",\"divEarned\":" + itoa(pl.DivEarned) + ",\"refEarned\":" + itoa(pl.RefEarned) + ",\"won\":" + itoa(pl.Won) +
1266		",\"shots\":" + itoa(pl.Shots) + ",\"hits\":" + itoa(pl.Hits) + ",\"owed\":" + itoa(pl.Owed) + ",\"withdrawn\":" + itoa(pl.Withdrawn) + "}"
1267}
1268
1269func (p *Pool) status() string {
1270	switch {
1271	case p.Cancelled:
1272		return "cancelled"
1273	case p.Over():
1274		s := "won by " + short(p.LastBidder)
1275		if p.Popped {
1276			s = "popped by " + short(p.LastBidder)
1277		}
1278		if !p.Paid {
1279			s += " (unclaimed)"
1280		}
1281		return s
1282	case p.Bids == 0:
1283		return "no bids yet · " + dur(p.Window) + " window · next " + GNOT(p.NextPrice())
1284	default:
1285		left := p.Deadline().Unix() - time.Now().Unix()
1286		s := dur(left) + " left"
1287		if p.Clock < p.Window {
1288			s += " (⚡ " + dur(p.Clock) + " clock)"
1289		}
1290		return s + " · " + short(p.LastBidder) + " on top · next " + GNOT(p.NextPrice()) + " · winner's share " + itoa(p.LiveBps()/100) + "%"
1291	}
1292}
1293
1294func markdownAll() string {
1295	var b strings.Builder
1296	b.WriteString("# Bubble Rumble v4\n\n")
1297	b.WriteString("Last bid standing takes the pot. Of every bid, 55% goes into the pot, 30% is paid at once to the pool's earlier bidders, 10% to whoever invited the bidder and 5% to the pool's creator. ")
1298	b.WriteString("Earnings are credited and withdrawn with Withdraw. At the close " + itoa(TeamShare) + "% of the pot goes to the other bidders who flew the winner's flag. Every pool has a last day: as it nears, the winner's own share slides toward a split of the pot among all its bidders. A bid may carry a shot at the bubble: a chance of at most " + itoa(ShotMaxBps/100) + "% to pop it and take the pot, drawn by the keeper's commit-reveal a few blocks later (the keeper never bids; a shot it fails to reveal in time is a miss); every miss is counted on the bubble.\n\n")
1299	b.WriteString("Play at [bubblerumble.net](https://bubblerumble.net).\n\n## Pools\n\n")
1300	if len(pools) == 0 {
1301		b.WriteString("_none yet_\n")
1302	}
1303	for _, p := range pools {
1304		b.WriteString("- [#" + itoa(p.ID) + " " + p.Name + "](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble4:" + itoa(p.ID) + ") · " + GNOT(p.Pot) + " · " + p.status() + "\n")
1305	}
1306	return b.String()
1307}
1308
1309func (p *Pool) markdown() string {
1310	var b strings.Builder
1311	b.WriteString("# " + p.Name + "\n\n")
1312	b.WriteString("- pot: **" + GNOT(p.Pot) + "**\n- status: " + p.status() + "\n- window: " + dur(p.Window) + " · life: " + dur(p.Life) + "\n- base price: " + GNOT(p.BasePrice) + " · next bid: " + GNOT(p.NextPrice()) + "\n- bids: " + itoa(p.Bids) + " · creator: " + short(p.Creator) + "\n")
1313	if !p.LastDay().IsZero() {
1314		b.WriteString("- last day: " + p.LastDay().UTC().Format("2006-01-02 15:04 UTC") + " · winner's share now " + itoa(p.LiveBps()/100) + "%\n")
1315	}
1316	if len(p.Shots) > 0 {
1317		b.WriteString("- shots: " + itoa(int64(len(p.Shots))) + " · misses: " + itoa(p.Misses) + " · unrevealed: " + itoa(p.Voided) + "\n")
1318	}
1319	b.WriteString("\n## Bids\n\n")
1320	if len(p.History) == 0 {
1321		b.WriteString("_none yet_\n")
1322	}
1323	for i := len(p.History) - 1; i >= 0; i-- {
1324		h := p.History[i]
1325		line := "- #" + itoa(h.N) + " " + short(h.Bidder)
1326		if h.Flag != "" {
1327			line += " (" + h.Flag + ")"
1328		}
1329		line += " paid " + GNOT(h.Price)
1330		if h.Shot > 0 {
1331			line += " + a " + GNOT(h.Shot) + " shot"
1332		}
1333		b.WriteString(line + " · pot " + GNOT(h.Pot) + "\n")
1334	}
1335	return b.String()
1336}