← Back to Realms

Realm

gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble5

Overview

Realm Path
gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble5
Exported Functions
20
State Entries
52
Source Files
2
Total Package Entries
100

Exported Functions

20 exported functions

State

52 state entries

Source Code

FILES
bubblerumble5.gno
go
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}
1369

Raw Package Data

Raw JSON data