PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
bubblerumble
Namespace
g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr
Files
2 (gnomod.toml)
Exported functions
8
Module
gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble
gno
0.9

Files (2)

  • gnomod.tomltoml
  • bubblerumble.gnogno
bubblerumble.gnogno
1// Package bubblerumble is a last-bidder-wins game in GNOT.2//3// A pool has a pot and a clock. Every bid goes into the pot and resets the4// clock; the Nth bid costs BasePrice·√N. When the clock runs out, the last5// bidder takes the whole pot. Anyone can open a pool, and the coins sent with6// CreatePool seed its pot.7//8// Paying more than the price buys a shorter clock: k× the price gives that bid9// a clock of window/k, never below a tenth of the window. The whole amount goes10// into the pot, and the next bid runs the full window again.11package bubblerumble1213import (14	"chain"15	"chain/banker"16	"chain/runtime/unsafe"17	"math"18	"strconv"19	"strings"20	"time"21)2223const (24	MinWindow    int64 = 10                // seconds25	MaxWindow    int64 = 30 * 24 * 3600    // 30 days26	MinBasePrice int64 = 1_000             // ugnot, 0.001 GNOT27	MaxBasePrice int64 = 1_000_000_000_000 // ugnot, 1,000,000 GNOT28	MaxNameLen         = 4029	HistoryLen         = 50 // bids kept per pool for the feed30	MaxBoost     int64 = 10 // the shortest clock a bid can buy is window/MaxBoost31)3233type Entry struct {34	N      int6435	Bidder address36	Price  int64 // what was paid; at least the price at the time37	Clock  int64 // seconds this bid had to stand38	Pot    int64 // pot right after this bid39	At     time.Time40}4142type Pool struct {43	ID         int6444	Name       string45	Creator    address46	Window     int64 // seconds the last bid must stand to win47	BasePrice  int64 // ugnot; the Nth bid costs BasePrice·√N48	Seed       int6449	Pot        int6450	Bids       int6451	LastBidder address52	LastBidAt  time.Time53	Clock      int64 // seconds the current top bid has to stand: Window, or less if it paid for it54	CreatedAt  time.Time55	Paid       bool // the pot went to the winner, or the seed back to the creator56	Cancelled  bool57	History    []Entry58}5960// pkgPath must match gnomod.toml: the realm's own address derives from it.61const pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble"6263var (64	pools     []*Pool65	realmAddr = chain.PackageAddress(pkgPath)66)6768// Price of the next bid in a pool with the given base price and n bids so far.69// Sublinear in n: bid 1 costs base, bid 4 costs 2·base, bid 100 costs 10·base.70func Price(base, n int64) int64 {71	return int64(math.Ceil(float64(base) * math.Sqrt(float64(n+1))))72}7374// Deadline is when the pool closes if nobody else bids. Meaningless before the first bid.75func (p *Pool) Deadline() time.Time {76	return p.LastBidAt.Add(time.Duration(p.Clock) * time.Second)77}7879// ClockFor is the clock a bid buys by paying sent against a price: the window80// divided by the multiple paid, never below window/MaxBoost.81func ClockFor(window, price, sent int64) int64 {82	c := int64(float64(window) * float64(price) / float64(sent))83	if c < window/MaxBoost {84		c = window / MaxBoost85	}86	if c < 1 {87		c = 188	}89	return c90}9192// Over reports whether the clock has run out, which is the moment a pool has a winner.93func (p *Pool) Over() bool {94	return p.Bids > 0 && !time.Now().Before(p.Deadline())95}9697// NextPrice is what the next bid costs.98func (p *Pool) NextPrice() int64 {99	return Price(p.BasePrice, p.Bids)100}101102// CreatePool opens a pool. Any ugnot sent with the call seeds its pot.103// Returns the new pool's id.104func CreatePool(cur realm, name string, window int64, basePrice int64) int64 {105	creator := userCaller(cur)106	name = cleanName(name)107	if window < MinWindow || window > MaxWindow {108		panic("window must be between 10s and 30d")109	}110	if basePrice < MinBasePrice || basePrice > MaxBasePrice {111		panic("base price must be between 0.001 and 1,000,000 GNOT")112	}113	seed := ugnotSent()114	p := &Pool{115		ID: int64(len(pools)) + 1, Name: name, Creator: creator, Window: window,116		BasePrice: basePrice, Seed: seed, Pot: seed, CreatedAt: time.Now(),117	}118	pools = append(pools, p)119	chain.Emit("PoolCreated", "id", itoa(p.ID), "creator", creator.String(), "seed", itoa(seed))120	return p.ID121}122123// Bid pays at least the current price into a pool's pot and resets its clock;124// paying k× the price makes this bid's clock window/k (see ClockFor).125func Bid(cur realm, id int64) {126	bidder := userCaller(cur)127	p := pool(id)128	if p.Cancelled {129		panic("pool was cancelled")130	}131	if p.Over() {132		panic("pool is closed")133	}134	if p.LastBidder == bidder {135		panic("you are already the last bidder")136	}137	price := p.NextPrice()138	sent := ugnotSent()139	if sent < price {140		panic("send at least " + itoa(price) + "ugnot, got " + itoa(sent) + "ugnot")141	}142	p.Pot += sent143	p.Bids++144	p.LastBidder = bidder145	p.LastBidAt = time.Now()146	p.Clock = ClockFor(p.Window, price, sent)147	p.History = append(p.History, Entry{N: p.Bids, Bidder: bidder, Price: sent, Clock: p.Clock, Pot: p.Pot, At: p.LastBidAt})148	if len(p.History) > HistoryLen {149		p.History = p.History[len(p.History)-HistoryLen:]150	}151	chain.Emit("Bid", "id", itoa(id), "n", itoa(p.Bids), "bidder", bidder.String(), "paid", itoa(sent), "clock", itoa(p.Clock), "pot", itoa(p.Pot))152}153154// Claim sends a finished pool's pot to its winner. Anyone may call it.155func Claim(cur realm, id int64) {156	p := pool(id)157	if p.Cancelled {158		panic("pool was cancelled")159	}160	if !p.Over() {161		panic("clock still running")162	}163	if p.Paid {164		panic("already paid out")165	}166	p.Paid = true167	send(cur, p.LastBidder, p.Pot)168	chain.Emit("Paid", "id", itoa(id), "winner", p.LastBidder.String(), "pot", itoa(p.Pot))169}170171// Cancel lets the creator take the seed back from a pool nobody has bid on.172func Cancel(cur realm, id int64) {173	caller := userCaller(cur)174	p := pool(id)175	if p.Creator != caller {176		panic("only the creator can cancel")177	}178	if p.Bids > 0 {179		panic("pool already has bids")180	}181	if p.Cancelled {182		panic("already cancelled")183	}184	p.Cancelled = true185	p.Paid = true186	if p.Pot > 0 {187		send(cur, caller, p.Pot)188	}189	chain.Emit("Cancelled", "id", itoa(id))190}191192func send(cur realm, to address, ugnot int64) {193	banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(realmAddr, to, chain.Coins{{"ugnot", ugnot}})194}195196func pool(id int64) *Pool {197	if id < 1 || id > int64(len(pools)) {198		panic("no such pool")199	}200	return pools[id-1]201}202203// userCaller is the signer of the transaction, refusing calls relayed through204// another realm so nobody can bid on someone else's behalf.205func userCaller(cur realm) address {206	prev := cur.Previous()207	if !prev.IsUserCall() {208		panic("must be a direct user call")209	}210	return prev.Address()211}212213func ugnotSent() int64 {214	sent := unsafe.OriginSend()215	for _, c := range sent {216		if c.Denom != "ugnot" {217			panic("only ugnot is accepted")218		}219	}220	return sent.AmountOf("ugnot")221}222223func cleanName(s string) string {224	s = strings.TrimSpace(s)225	if s == "" || len(s) > MaxNameLen {226		panic("name must be 1-40 characters")227	}228	for _, c := range s {229		if c < 0x20 || c == 0x7f {230			panic("name has control characters")231		}232	}233	return s234}235236func itoa(n int64) string { return strconv.FormatInt(n, 10) }237238func parseID(s string) int64 {239	n, err := strconv.ParseInt(s, 10, 64)240	if err != nil {241		panic("bad pool id")242	}243	return n244}245246// GNOT formats ugnot as GNOT with the trailing zeros trimmed.247func GNOT(ugnot int64) string {248	whole, frac := ugnot/1_000_000, ugnot%1_000_000249	if frac == 0 {250		return itoa(whole) + " GNOT"251	}252	f := strconv.FormatInt(1_000_000+frac, 10)[1:] // six digits, zero padded253	return itoa(whole) + "." + strings.TrimRight(f, "0") + " GNOT"254}255256func short(a address) string {257	s := a.String()258	if len(s) < 12 {259		return s260	}261	return s[:8] + "…" + s[len(s)-4:]262}263264func dur(sec int64) string {265	if sec <= 0 {266		return "0s"267	}268	d, h, m, s := sec/86400, sec%86400/3600, sec%3600/60, sec%60269	out := ""270	if d > 0 {271		out += itoa(d) + "d "272	}273	if d > 0 || h > 0 {274		out += itoa(h) + "h "275	}276	if d > 0 || h > 0 || m > 0 {277		out += itoa(m) + "m "278	}279	return out + itoa(s) + "s"280}281282// --- rendering ---------------------------------------------------------------283284// Render serves gnoweb ("" and "<id>") and the web page ("json" and "json/<id>").285func Render(path string) string {286	path = strings.Trim(path, "/")287	switch {288	case path == "json":289		return jsonAll()290	case strings.HasPrefix(path, "json/"):291		return pool(parseID(path[5:])).json(true)292	case path == "":293		return markdownAll()294	default:295		return pool(parseID(path)).markdown()296	}297}298299func unix(t time.Time) string {300	if t.IsZero() {301		return "0"302	}303	return itoa(t.Unix())304}305306func jbool(b bool) string {307	if b {308		return "true"309	}310	return "false"311}312313func (p *Pool) json(withHistory bool) string {314	var b strings.Builder315	b.WriteString("{\"id\":" + itoa(p.ID))316	b.WriteString(",\"name\":" + strconv.Quote(p.Name))317	b.WriteString(",\"creator\":\"" + p.Creator.String() + "\"")318	b.WriteString(",\"window\":" + itoa(p.Window))319	b.WriteString(",\"basePrice\":" + itoa(p.BasePrice))320	b.WriteString(",\"seed\":" + itoa(p.Seed))321	b.WriteString(",\"pot\":" + itoa(p.Pot))322	b.WriteString(",\"bids\":" + itoa(p.Bids))323	b.WriteString(",\"nextPrice\":" + itoa(p.NextPrice()))324	b.WriteString(",\"lastBidder\":\"" + p.LastBidder.String() + "\"")325	b.WriteString(",\"lastBidAt\":" + unix(p.LastBidAt))326	b.WriteString(",\"clock\":" + itoa(p.Clock))327	b.WriteString(",\"createdAt\":" + unix(p.CreatedAt))328	b.WriteString(",\"over\":" + jbool(p.Over()))329	b.WriteString(",\"paid\":" + jbool(p.Paid))330	b.WriteString(",\"cancelled\":" + jbool(p.Cancelled))331	if withHistory {332		b.WriteString(",\"history\":[")333		for i := len(p.History) - 1; i >= 0; i-- { // newest first334			h := p.History[i]335			if i < len(p.History)-1 {336				b.WriteString(",")337			}338			b.WriteString("{\"n\":" + itoa(h.N) + ",\"bidder\":\"" + h.Bidder.String() + "\",\"price\":" + itoa(h.Price) + ",\"clock\":" + itoa(h.Clock) + ",\"pot\":" + itoa(h.Pot) + ",\"at\":" + unix(h.At) + "}")339		}340		b.WriteString("]")341	}342	b.WriteString("}")343	return b.String()344}345346func jsonAll() string {347	var b strings.Builder348	b.WriteString("{\"now\":" + itoa(time.Now().Unix()) + ",\"realm\":\"" + pkgPath + "\",\"pools\":[")349	for i, p := range pools {350		if i > 0 {351			b.WriteString(",")352		}353		b.WriteString(p.json(false))354	}355	b.WriteString("]}")356	return b.String()357}358359func (p *Pool) status() string {360	switch {361	case p.Cancelled:362		return "cancelled"363	case p.Over():364		s := "won by " + short(p.LastBidder)365		if !p.Paid {366			s += " (unclaimed)"367		}368		return s369	case p.Bids == 0:370		return "no bids yet · " + dur(p.Window) + " window · next " + GNOT(p.NextPrice())371	default:372		left := p.Deadline().Unix() - time.Now().Unix()373		s := dur(left) + " left"374		if p.Clock < p.Window {375			s += " of a " + dur(p.Clock) + " clock"376		}377		return s + " · " + short(p.LastBidder) + " on top · next " + GNOT(p.NextPrice())378	}379}380381func markdownAll() string {382	var b strings.Builder383	b.WriteString("# Bubble Rumble\n\n")384	b.WriteString("Every bid goes into a pool's pot and resets its clock. The Nth bid costs base·√N. ")385	b.WriteString("When the clock runs out, the last bidder takes the whole pot. Play at [bubblerumble.net](https://bubblerumble.net).\n\n")386	if len(pools) == 0 {387		b.WriteString("_No pools yet._\n")388		return b.String()389	}390	b.WriteString("## Pools\n\n")391	for i := len(pools) - 1; i >= 0; i-- {392		p := pools[i]393		b.WriteString("- [" + p.Name + "](?" + itoa(p.ID) + ") — **" + GNOT(p.Pot) + "** · " + itoa(p.Bids) + " bids · " + p.status() + "\n")394	}395	return b.String()396}397398func (p *Pool) markdown() string {399	var b strings.Builder400	b.WriteString("# " + p.Name + "\n\n")401	b.WriteString("**" + GNOT(p.Pot) + "** in the pot · " + p.status() + "\n\n")402	b.WriteString("Opened by " + short(p.Creator) + " · " + dur(p.Window) + " window · base " + GNOT(p.BasePrice) + " · seed " + GNOT(p.Seed) + "\n\n")403	if len(p.History) > 0 {404		b.WriteString("## Bids\n\n")405		for i := len(p.History) - 1; i >= 0; i-- {406			h := p.History[i]407			b.WriteString("- #" + itoa(h.N) + " " + short(h.Bidder) + " paid " + GNOT(h.Price) + " → pot " + GNOT(h.Pot) + "\n")408		}409	}410	return b.String()411}412

Functions

  • Bid(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)

  • 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

  • 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

  • Price(base int64, n int64) int64

  • Render(path string) string

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

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.