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

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}