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) }