Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

mint.gno

4.80 Kb · 156 lines
  1package bubble
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"time"
  7
  8	"chain"
  9)
 10
 11// The supply, in three pools that add up to the maximum and never mix.
 12const (
 13	MaxSupply    = 100_000_000 * one
 14	premine      = 10_000_000 * one // Jae's, at init
 15	ReferralPool = 40_000_000 * one // the minter's, for referrals, under the day cap
 16	Reserve      = 50_000_000 * one // the holders', by govern:reserve only
 17
 18	MaxBatch = 100          // entries in one MintMany
 19	MaxEntry = 50_000 * one // per address per batch: room for a big inviter's day, never a whole day's cap
 20)
 21
 22// premineTo is the address the premine goes to (Jae, jarvis room 99/102/105, 2026-09-25).
 23const premineTo = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")
 24
 25var (
 26	premined       int64 // what init minted (the premine)
 27	referralMinted int64 // out of ReferralPool
 28	reserveMinted  int64 // out of Reserve
 29	mintedDay      int64 // minted by the minter on dayKey's day
 30	dayKey         int64 // unix day of mintedDay
 31	lastBatch      string
 32	batches        int64
 33)
 34
 35func Premined() int64       { return premined }
 36func ReferralMinted() int64 { return referralMinted }
 37func ReserveMinted() int64  { return reserveMinted }
 38func Batches() int64        { return batches }
 39func LastBatch() string     { return lastBatch }
 40
 41// MintedToday is what the minter has minted on the current UTC day, and the day's cap.
 42func MintedToday() (minted, ceiling int64) {
 43	if dayOf() != dayKey {
 44		return 0, dayCap
 45	}
 46	return mintedDay, dayCap
 47}
 48
 49func dayOf() int64 { return time.Now().Unix() / 86400 }
 50
 51func mustBeMinter(cur realm) {
 52	who := caller(cur)
 53	if minter == "" {
 54		panic("bubble: nobody may mint; the holders can name a minter")
 55	}
 56	if who != minter && (admin == "" || who != admin) {
 57		panic("bubble: only the minter")
 58	}
 59	if paused {
 60		panic("bubble: minting is paused")
 61	}
 62}
 63
 64// referralMint is one credit out of the referral pool, under every wall. amount is base units.
 65func referralMint(to address, amount int64) {
 66	if !to.IsValid() {
 67		panic("bubble: not an address: " + to.String())
 68	}
 69	if amount <= 0 || amount > MaxEntry {
 70		panic("bubble: an entry is between 1 and " + whole(MaxEntry) + " BUBBLE")
 71	}
 72	if referralMinted+amount > ReferralPool {
 73		panic("bubble: the referral pool is spent; what is left is the holders' reserve")
 74	}
 75	if d := dayOf(); d != dayKey {
 76		dayKey, mintedDay = d, 0
 77	}
 78	if mintedDay+amount > dayCap {
 79		panic("bubble: over today's cap of " + whole(dayCap) + " BUBBLE")
 80	}
 81	mintedDay += amount
 82	referralMinted += amount
 83	ledger.Mint(to, amount)
 84}
 85
 86// Mint credits one address. The minter's hand tool; the box mints through MintMany.
 87func Mint(cur realm, to address, amount int64) {
 88	mustBeMinter(cur)
 89	referralMint(to, amount)
 90	chain.Emit("Credit", "to", to.String(), "amount", strconv.FormatInt(amount, 10))
 91}
 92
 93// MintMany credits a batch: "g1…:amount,g1…:amount" (base units), at most MaxBatch entries, no address twice, under
 94// the pool and the day cap. All or nothing — a bad entry panics, which aborts the whole transaction, and names the
 95// entry ("entry 3 g1…: over today's cap") so the box can drop it and send the rest. batchID is eight ASCII digits
 96// that must exceed the last one landed, so a batch the box re-sends after a lost receipt cannot land twice.
 97func MintMany(cur realm, batchID string, list string) {
 98	mustBeMinter(cur)
 99	if len(batchID) != 8 {
100		panic("bubble: a batch id is eight digits")
101	}
102	for i := 0; i < len(batchID); i++ {
103		if batchID[i] < '0' || batchID[i] > '9' {
104			panic("bubble: a batch id is eight digits")
105		}
106	}
107	if batchID <= lastBatch {
108		panic("bubble: batch " + batchID + " is not after " + lastBatch)
109	}
110	parts := strings.Split(list, ",")
111	if list == "" || len(parts) > MaxBatch {
112		panic("bubble: a batch is 1 to " + strconv.Itoa(MaxBatch) + " entries")
113	}
114	seen := map[string]bool{}
115	total := int64(0)
116	for i, p := range parts {
117		c := strings.Index(p, ":")
118		if c <= 0 {
119			panic("bubble: entry " + strconv.Itoa(i+1) + ": expected g1…:amount")
120		}
121		to, raw := p[:c], p[c+1:]
122		n, err := strconv.ParseInt(raw, 10, 64)
123		if err != nil {
124			panic("bubble: entry " + strconv.Itoa(i+1) + " " + to + ": bad amount")
125		}
126		if seen[to] {
127			panic("bubble: entry " + strconv.Itoa(i+1) + " " + to + ": twice in one batch")
128		}
129		seen[to] = true
130		func() {
131			defer func() {
132				if r := recover(); r != nil {
133					panic("bubble: entry " + strconv.Itoa(i+1) + " " + to + ": " + strings.TrimPrefix(toString(r), "bubble: "))
134				}
135			}()
136			referralMint(address(to), n)
137		}()
138		total += n
139	}
140	lastBatch = batchID
141	batches++
142	chain.Emit("Batch", "id", batchID, "entries", strconv.Itoa(len(parts)), "total", strconv.FormatInt(total, 10))
143}
144
145func toString(r any) string {
146	switch e := r.(type) {
147	case error:
148		return e.Error()
149	case string:
150		return e
151	}
152	return "failed"
153}
154
155// whole renders base units as whole BUBBLE for a message.
156func whole(n int64) string { return strconv.FormatInt(n/one, 10) }