1// Package bubblerumble6 is the sixth Bubble Rumble realm: v5's game without the2// last day (jae, 2026-10-01: "pools shouldn't expire… at all") — a pool lives3// as long as it is booped, as in v3 and v4, and the last booper takes the pot's4// rest whole. v5's one new slice stays. Of every bid 50% goes to the pot, 30% is credited at once to the5// pool's earlier bidders pro-rata to what they paid in, 10% to whoever invited the6// bidder, 5% to the pool's creator — and 5% to the BUBBLE treasury: the token7// realm's own sub-realm (bubbletreasury), which nothing moves but a decided vote8// of the BUBBLE holders. The five percent comes out of the pot's share alone; the9// dividends, the inviter's and the creator's slices are v4's. Seeds (a creator's10// own money into a new pool) and shots pay no royalty: a seed is a gift to the11// players, and a shot's losing side already feeds the pot. Every pool has a flag12// war at the close, credited earnings and Withdraw, a clock that can13// 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 what17// a pop would pay the shooter right now: the whole pot, the team's 35% counted. So the18// pot keeps a fifth of every shot's fair value, and a hit pops the bubble: the pool19// 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 own21// box, which also settles pools on time. The keeper publishes sha256 hashes of22// 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 secret24// (Reveal) in the blocks ShotDelay to ShotDelay+ShotWindow after the bid. The25// seed is the secret with the shot's own facts and the reveal block's height26// and time. Nobody without the secret can grind it: not the shooter, not a27// validator. The secret's holder knows the two candidate block times a block28// ahead and may pick which of the window's three blocks to reveal in — three29// rolls, at most 2.4× fair on a 1% shot — which is why the keeper key never30// bids or shoots, and its operator never shoots. (The site's own keep-alive boops come from another key and31// 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, so35// withholding a reveal never pays whoever withholds it. What remains is that a36// keeper who leads a pot could withhold a stranger's hit and let the money fall37// 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 keeper39// 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 commitment41// and a sign of life (Commit or Heartbeat) within KeeperFresh. Otherwise the bid42// stands and the shot's money goes straight back in the same transaction.43// - Shots are refused under MinShot. RevealTip is taken44// from every shot and credited to the keeper whoever sends the reveal (the45// secret sits in the mempool, and a proposer could copy it), so the keeper46// 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-defender50// veto above; a proposer who reads the keeper's reveal in the mempool holds the51// secret too, and proposing the window's blocks could drop the reveal for a roll52// it does not like or leave it out altogether (a void), which Late and Voided53// show; keeper and validator together could grind. SetShots is the switch if any54// of it is seen. SetKeeper drops every unused commitment.55package bubblerumble65657import (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)7172const (73 MinWindow int64 = 10 // seconds74 MaxWindow int64 = 30 * 24 * 3600 // 30 days75 MinBasePrice int64 = 1_000 // ugnot, 0.001 GNOT76 MaxBasePrice int64 = 1_000_000_000_000 // ugnot, 1,000,000 GNOT77 MaxNameLen = 4078 HistoryLen = 50 // bids kept per pool for the feed79 RosterMax = 100 // distinct bidders a pool pays dividends, team shares and the close split to80 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 answer8283 // where every bid goes, in percent84 PotShare int64 = 5085 DivShare int64 = 30 // to earlier bidders of the pool, pro-rata by weight86 RefShare int64 = 10 // to the bidder's inviter87 CreatorShare int64 = 588 TreasuryShare int64 = 5 // to the BUBBLE treasury: the token realm's sub-realm, spent only by the holders' vote89 // of the pot at the close, to the other bidders under the winner's flag90 TeamShare int64 = 359192 ShotMaxBps int64 = 100 // 1% a shot, whatever is paid93 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 holder96 MinShot int64 = 100_000 // ugnot: no shot under 0.1 GNOT, so dust cannot make reveals cost more than they tip97 RevealTip int64 = 20_000 // ugnot: off every shot, to whoever reveals it98 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 most100 CommitBatch = 100 // hashes one Commit may bring101 CommitCompact = 200 // used commitments kept before the queue is compacted102)103104// admin is the site's cold key (jae's rule of 2026-09-27: every realm hands its105// ownership to it; the deployer, the hotkey, keeps nothing): the only address that106// may turn shots off or name another keeper.107const admin = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")108109// pkgPath must match gnomod.toml: the realm's own address derives from it.110const pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble6"111112// The BUBBLE treasury is the token realm's sub-realm for its `treasury:spend` kind (the realm bubbletreasury113// 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)119120type Entry struct {121 N int64122 Bidder address123 Flag string124 Price int64 // what was paid for the bid itself; at least the price at the time125 Shot int64 // extra paid for a chance to pop the bubble126 Clock int64 // seconds this bid had to stand127 Pot int64 // pot right after this bid128 At time.Time129}130131// Member is a distinct bidder of a pool: dividends, team shares and the close132// split go to the roster. The flag is the one flown at the first bid here.133type Member struct {134 Addr address135 Flag string136 Paid int64 // gross paid into this pool (bids and shots)137 Earned int64 // dividends received from this pool138}139140// Shot is a shot at the bubble waiting to be revealed, or resolved.141type Shot struct {142 N int64 // 1-based within the pool143 Bidder address144 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 set146 Bps int64 // chance, in basis points147 CommitH int64 // block height when paid; revealable at CommitH+ShotDelay and the ShotWindow blocks after148 Hash string // the commitment it took: sha256 of the secret that decides it, 32 raw bytes149 Resolved bool150 Won bool151 Void bool // not revealed in its window, or the pool closed first: a miss152 DrawH int64153 Revealer address154}155156type Pool struct {157 ID int64158 Name string159 Creator address160 Window int64 // seconds the last bid must stand to win161 BasePrice int64 // ugnot; the Nth bid costs BasePrice·√N162 Seed int64163 Pot int64164 Bids int64165 LastBidder address166 LastBidAt time.Time167 FirstBidAt time.Time168 Clock int64 // seconds the current top bid has to stand169 CreatedAt time.Time170 Paid bool // the pot went out, or the seed back to the creator171 Cancelled bool172 Popped bool // a shot ended it173 PoppedAt time.Time174 DivPaid int64 // dividends paid out of this pool so far175 RefPaid int64176 CreatorPaid int64177 TreasuryPaid int64 // the BUBBLE treasury's 5% of this pool's bids178 ShotPaid int64 // slices paid to hits while the pool went on179 Misses int64 // shots resolved and lost, voids included: the scars on the bubble180 Voided int64 // of those, the shots nobody revealed in time181 Late int64 // shots revealed in their window but not at its first block: a keeper choosing its block would show here182 ShotEscrow int64 // shots paid for and not yet resolved: in the pot once they are183 WinnerFlag string184 TeamPaid int64 // what the winner's team received at the close185 SharedPaid int64 // the pro-rata part of the close186 WinnerPaid int64187 History []Entry188 Roster []*Member189 Shots []*Shot190 Settled int // shots before this index are all resolved (they resolve strictly in order): where settleShots starts191}192193// Player is what the realm remembers about an address across pools.194type Player struct {195 Referrer address // set by the first bid, permanent196 Recruits int64197 Flag string198 Bids int64199 Paid int64200 DivEarned int64201 RefEarned int64202 Won int64 // pots, team shares, close splits and shot slices received203 Shots int64204 Hits int64205 Owed int64 // credited and not yet withdrawn206 Withdrawn int64207}208209var (210 shotsOn = true211 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 too212 keeperSeen time.Time // the keeper's last Commit or Heartbeat213 commits []string // the hashes of the keeper's secrets (32 raw bytes each), in order; the first head are used214 head int215 voided int64 // shots voided over all pools216 late int64 // shots revealed after the first block of their window, over all pools217 pools []*Pool218 players = map[address]*Player{}219 teamPaid = map[string]int64{} // flag -> gross bid volume under it220 teamWon = map[string]int64{} // flag -> pots closed under it221 teamSize = map[string]int64{} // flag -> players flying it222 realmAddr = chain.PackageAddress(pkgPath)223 treasuryAddr = chain.DerivePkgSubAddr(bubblePath, treasurySubpath)224 treasuryPaid int64 // the royalty over all pools225)226227// TreasuryAddr is where 5% of every bid goes: the BUBBLE treasury.228func TreasuryAddr() address { return treasuryAddr }229230// TreasuryPaid is what every pool of this realm has sent there.231func TreasuryPaid() int64 { return treasuryPaid }232233// 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}237238// Deadline is when the pool closes if nothing changes: the clock running out239// 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 it241// is booped, as v3 and v4 did.242func (p *Pool) Deadline() time.Time {243 if p.Popped {244 return p.PoppedAt245 }246 return p.LastBidAt.Add(time.Duration(p.Clock) * time.Second)247}248249func (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) }251252253// ClockFor is the clock a bid buys by paying sent against a price: the window254// 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 / MaxBoost259 }260 if c < MinClock {261 c = MinClock262 }263 if c > window {264 c = window265 }266 return c267}268269// mulDiv is a×b/c without overflowing on the way: pots and paid-ins are both270// 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}282283// 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 0287 }288 bps := ShotFair * shot / take289 if bps > ShotMaxBps {290 bps = ShotMaxBps291 }292 return bps293}294295// Take is the most a pop could pay this shooter if the pool closed now on a pot296// of this size: Claim's own arithmetic, run dry, with the team's share counted297// as the shooter's too — one bid from a friend (or a second address) under a298// 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 + cut303}304305// CreatePool opens a pool. Any ugnot sent with the call seeds its pot. The306// 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.ID321}322323func 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 p330}331332// Bid pays at least the current price and resets the clock. ref is the address333// that invited the bidder ("" for none; only the first bid ever sets it), flag334// a two-letter country to play under ("" keeps the last one; the flag is locked335// per pool at the first bid there), shot how much of the money sent is a shot336// at popping the bubble (0 for none): the rest must cover the price, and paying337// 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 back346 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 return353 }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 - shot361 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 first365 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 = f372 teamSize[f]++373 }374 if pl.Referrer == "" && ref != "" {375 r := address(ref)376 if r.IsValid() && r != bidder {377 pl.Referrer = r378 player(r).Recruits++379 }380 }381382 // where the bid goes383 div, refCut, creatorCut, royalty := bid*DivShare/100, bid*RefShare/100, bid*CreatorShare/100, bid*TreasuryShare/100384 toPot := bid - div - refCut - creatorCut - royalty // the dust of the four divisions stays in the pot385 if creatorCut > 0 {386 send(cur, p.Creator, creatorCut)387 p.CreatorPaid += creatorCut388 }389 if royalty > 0 { // the BUBBLE treasury's share, sent now like the creator's and the inviter's390 send(cur, treasuryAddr, royalty)391 p.TreasuryPaid += royalty392 treasuryPaid += royalty393 }394 if pl.Referrer != "" && refCut > 0 {395 send(cur, pl.Referrer, refCut)396 p.RefPaid += refCut397 player(pl.Referrer).RefEarned += refCut398 } else {399 toPot += refCut400 }401 divPaid := div - p.payDividends(cur, bidder, div)402 toPot += div - divPaid403 p.Pot += toPot404405 m := p.member(bidder, pl.Flag)406 if m.Flag == "" { // a flagless first bid does not lock "no flag": the first flag flown here does407 m.Flag = pl.Flag408 }409 // the shot, if there can be one: else its money goes straight back and the bid stands410 why := ""411 if shot > 0 {412 why = p.shotRefusal(bidder, shot)413 }414 m.Paid += sent415 pl.Bids++416 pl.Paid += sent417 if pl.Flag != "" {418 teamPaid[pl.Flag] += sent419 }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 rest422 var take, bps int64423 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 out425 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 -= shot432 pl.Paid -= shot433 if pl.Flag != "" {434 teamPaid[pl.Flag] -= shot435 }436 send(cur, bidder, shot)437 chain.Emit("ShotReturned", "id", itoa(id), "bidder", bidder.String(), "ugnot", itoa(shot), "why", why)438 sent -= shot439 shot = 0440 }441 p.Bids++442 if p.FirstBidAt.IsZero() {443 p.FirstBidAt = time.Now()444 }445 p.LastBidder = bidder446 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))453454 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 += shot459 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}463464// shotRefusal is why a shot cannot be sold on this pool right now ("" when it465// 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}482483// 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 = on489 chain.Emit("Shots", "on", jbool(on))490}491492// SetKeeper names the address that commits and heartbeats; only the admin493// may. Every unused commitment is dropped with it: a rotated or compromised494// 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 = addr503 commits = commits[:head]504 chain.Emit("Keeper", "addr", addr.String())505}506507func keeperCaller(cur realm) address {508 who := userCaller(cur)509 if who == "" || who != keeper {510 panic("only the keeper")511 }512 return who513}514515func 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 who521}522523// Heartbeat is the keeper's sign of life: shots are sold only within524// KeeperFresh of the last one (a Commit counts too).525func Heartbeat(cur realm) {526 keeperCaller(cur)527 keeperSeen = time.Now()528}529530// 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, whose532// box holds the secrets. A hash still in the queue (used or not) is refused: a533// repeated secret would be public after its first reveal (a safety net against534// the keeper repeating itself; compaction forgets the oldest). The queue holds535// at most MaxCommits unused; the used ones in front of it are compacted away536// 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 = 0550 }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 holds558 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}566567// Retire drops the first n unused commitments: a keeper that lost the secrets568// 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}581582// Reveal draws shots with the secrets behind their commitments: a comma-separated583// list of pool:shot:secretHex, any pools, in one transaction — the keeper sends584// every shot that is due in one call, so a run of shots on one block never585// outpaces it. Anyone may call it; the secret is the proof, and the caller is586// tipped RevealTip out of each shot revealed. Shots go in order: an earlier one587// still pending must come first (in the list, or before). An entry not yet due588// is skipped, one with a wrong secret or already resolved is skipped with an589// event, so a stale entry never voids the rest; a malformed one (no such pool590// or shot, a cancelled pool, not pool:shot:secret) aborts the whole call, which591// only the keeper's own bug could send. The reveal must land in the592// blocks CommitH+ShotDelay through CommitH+ShotDelay+ShotWindow; later, or on a593// pool that has closed, the shot is already void (settleShots runs first): into594// the pot as a miss. A hit pops the bubble: the pool closes now with the shooter595// as its winner, settled at Claim.596func Reveal(cur realm, reveals string) {597 who := userCaller(cur)598 h := runtime.ChainHeight()599 done := 0600 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 continue618 }619 if h < t.CommitH+ShotDelay {620 continue // not due yet: the keeper sends a block early to be sure of the window621 }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 continue627 }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, who631 p.ShotEscrow -= t.Paid632 // the tip is the keeper's whoever sent the reveal: the secret sits in the mempool, and a proposer could633 // copy it into a reveal of its own; the seed does not care who reveals, so nothing else changes634 tip := RevealTip635 if tip > t.Paid {636 tip = t.Paid637 }638 if keeper != "" {639 credit(keeper, tip)640 } else {641 credit(who, tip)642 }643 p.Pot += t.Paid - tip644 if h > t.CommitH+ShotDelay { // revealed in the window but not at its first block: the honest keeper's tell, counted645 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 continue653 }654 player(t.Bidder).Hits++655 p.Popped, p.PoppedAt, p.LastBidder = true, time.Now(), t.Bidder656 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}662663// Settle voids the shots at a pool that nobody revealed in time, or that its664// close overtook. Anyone may call it; every bid, reveal and claim on the pool665// does the same first.666func Settle(cur realm, id int64) {667 pool(id).settleShots()668}669670// settleShots voids, in order, every pending shot whose window has passed or671// 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 continue682 }683 t.Resolved, t.Void, t.DrawH = true, true, h684 p.ShotEscrow -= t.Paid685 p.Pot += t.Paid686 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}692693func jif(c bool, a, b string) string {694 if c {695 return a696 }697 return b698}699700// seedFor is the draw's seed: the secret with the shot's own facts and the701// reveal block's height and time. Everything but the secret is public, and the702// 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 []byte705 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}716717// 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) < bps720}721722// payDividends credits div to the pool's earlier bidders, pro-rata to what723// each has paid in, skipping the bidder; whatever cannot be placed (no earlier724// bidders, rounding) is returned to go into the pot.725func (p *Pool) payDividends(cur realm, bidder address, div int64) int64 {726 var total int64727 for _, m := range p.Roster {728 if m.Addr != bidder {729 total += m.Paid730 }731 }732 if total == 0 || div == 0 {733 return div734 }735 var paid int64736 for _, m := range p.Roster {737 if m.Addr == bidder {738 continue739 }740 amt := mulDiv(div, m.Paid, total)741 if amt > 0 {742 credit(m.Addr, amt)743 m.Earned += amt744 player(m.Addr).DivEarned += amt745 paid += amt746 }747 }748 p.DivPaid += paid749 return div - paid750}751752// 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 m758 }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 m766}767768// Claim settles a finished pool: everything is credited, and Withdraw sends769// it. TeamShare of the pot goes to the OTHER roster members flying the770// winner's flag, pro-rata by paid-in — or, when nobody else flew it, to all771// the other roster members. The rest is the winner's (v6: no last day, no772// slide). A pool with a single bidder credits them everything. Rounding goes773// 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 = true787 winner := p.LastBidder788 flag := ""789 if m := p.find(winner); m != nil {790 flag = m.Flag791 }792 p.WinnerFlag = flag793 toWinner, _ := p.payout(winner, p.Pot, true)794 if toWinner > 0 {795 credit(winner, toWinner)796 player(winner).Won += toWinner797 }798 p.WinnerPaid = toWinner799 if flag != "" {800 teamWon[flag] += p.Pot801 }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}804805// payout is the close's arithmetic for a winner and a pot: TeamShare to the806// 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 shares808// 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 (Take810// 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.Flag815 }816 var team int64817 for _, m := range p.Roster {818 if m.Addr != winner && m.Flag == flag && flag != "" {819 team += m.Paid820 }821 }822 teamFlag := flag823 if team == 0 {824 teamFlag = "" // nobody else flew it: every other bidder is the team825 for _, m := range p.Roster {826 if m.Addr != winner {827 team += m.Paid828 }829 }830 }831 toWinner = pot832 if team == 0 {833 return toWinner, 0 // alone on the roster: everything834 }835 cut := pot * TeamShare / 100836 for _, m := range p.Roster {837 if m.Addr == winner || (teamFlag != "" && m.Flag != teamFlag) {838 continue839 }840 amt := mulDiv(cut, m.Paid, team)841 if amt > 0 {842 toWinner -= amt843 teamCut += amt844 if pay {845 credit(m.Addr, amt)846 player(m.Addr).Won += amt847 p.TeamPaid += amt848 }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, teamCut853}854855// 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 = true869 p.Paid = true870 if p.Pot > 0 {871 send(cur, caller, p.Pot)872 }873 chain.Emit("Cancelled", "id", itoa(id))874}875876func (p *Pool) find(a address) *Member {877 for _, m := range p.Roster {878 if m.Addr == a {879 return m880 }881 }882 return nil883}884885func player(a address) *Player {886 pl := players[a]887 if pl == nil {888 pl = &Player{}889 players[a] = pl890 }891 return pl892}893894func send(cur realm, to address, ugnot int64) {895 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr, to, chain.Coins{{"ugnot", ugnot}})896}897898// credit books ugnot to an address; Withdraw pays it out.899func credit(to address, ugnot int64) {900 player(to).Owed += ugnot901}902903// Withdraw sends the caller everything credited to them: dividends, team904// shares, close splits, slices and won pots.905func Withdraw(cur realm) int64 {906 who := userCaller(cur)907 pl := player(who)908 amt := pl.Owed909 if amt <= 0 {910 panic("nothing to withdraw")911 }912 pl.Owed = 0913 pl.Withdrawn += amt914 send(cur, who, amt)915 chain.Emit("Withdrawn", "to", who.String(), "ugnot", itoa(amt))916 return amt917}918919// Owed is what an address could withdraw right now.920func Owed(a address) int64 { return player(a).Owed }921922func pool(id int64) *Pool {923 if id < 1 || id > int64(len(pools)) {924 panic("no such pool")925 }926 return pools[id-1]927}928929// userCaller is the signer of the transaction, refusing calls relayed through930// another realm so nobody can bid on someone else's behalf, and so no realm can931// 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}939940func 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}949950func 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 s961}962963// 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 s970}971972func itoa(n int64) string { return strconv.FormatInt(n, 10) }973974func parseID(s string) int64 {975 n, err := strconv.ParseInt(s, 10, 64)976 if err != nil {977 panic("bad pool id")978 }979 return n980}981982func parseNum(s string) int64 {983 n, err := strconv.ParseInt(s, 10, 64)984 if err != nil {985 panic("bad number")986 }987 return n988}989990// 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_000993 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}9991000func short(a address) string {1001 s := a.String()1002 if len(s) < 12 {1003 return s1004 }1005 return s[:8] + "…" + s[len(s)-4:]1006}10071008func 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%601013 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}10251026// --- rendering ---------------------------------------------------------------10271028// 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}10491050func unix(t time.Time) string {1051 if t.IsZero() {1052 return "0"1053 }1054 return itoa(t.Unix())1055}10561057func jbool(b bool) string {1058 if b {1059 return "true"1060 }1061 return "false"1062}10631064func (p *Pool) json(detail bool) string {1065 var b strings.Builder1066 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.WinnerFlag1103 if m := p.find(p.LastBidder); m != nil && flag == "" {1104 flag = m.Flag1105 }1106 b.WriteString(",\"flag\":\"" + flag + "\"")1107 // what each flag has paid into this pool1108 b.WriteString(",\"teams\":{")1109 seen := map[string]int64{}1110 order := []string{}1111 for _, m := range p.Roster {1112 if m.Flag == "" {1113 continue1114 }1115 if _, ok := seen[m.Flag]; !ok {1116 order = append(order, m.Flag)1117 }1118 seen[m.Flag] += m.Paid1119 }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 first1130 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 first1145 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}11561157// jsonCommits lists the unused commitments in order, hex, so a keeper that1158// starts can tell which of them it holds the secrets for and Retire the rest.1159func jsonCommits() string {1160 var b strings.Builder1161 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}11711172// 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}11791180func jsonAll() string {1181 var b strings.Builder1182 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}11991200// jsonTeams is the country leaderboard: paid in, won, and players per flag.1201func jsonTeams() string {1202 var b strings.Builder1203 b.WriteString("{")1204 i := 01205 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}12151216// 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}12271228func (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 s1241 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}12521253func markdownAll() string {1254 var b strings.Builder1255 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}12671268func (p *Pool) markdown() string {1269 var b strings.Builder1270 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}1293Bid(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64, ref string, flag string, shot int64)
Cancel(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
Claim(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
ClockFor(window int64, price int64, sent int64) int64
Commit(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, hashes string)
CreatePool(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, window int64, basePrice int64) int64
GNOT(ugnot int64) string
Heartbeat(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Owed(a string) int64
Price(base int64, n int64) int64
Render(path string) string
Retire(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, n int64)
Reveal(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, reveals string)
SetKeeper(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string)
SetShots(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, on bool)
Settle(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
ShotBps(shot int64, take int64) int64
TreasuryAddr() string
TreasuryPaid() int64
Withdraw(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.