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.

bubblerumble5.gno

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