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

bubblerumble6.gno

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