perk2.gno
8.93 Kb · 470 lines
1package perk2
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "strconv"
8 "strings"
9
10 "gno.land/p/nt/avl/v0"
11 col "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/bazaargens"
12 factory "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5"
13)
14
15// Perk2 records share weight of minted, revealed, unlisted Bazaar Gens.
16// Sync does not assign coins. Settle (admin) splits unsettled ugnot across
17// the weights recorded at that call. Claim pays only settled accrued.
18// Mainnet staging retargets colPkg and factory. Buy fees reach this sink
19// only after factory.SetGenesis points here.
20
21const (
22 pkgPath = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/perk2"
23 colPkg = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/bazaargens"
24 native = "ugnot"
25 maxSpan = 20
26 maxDelta = int64(9_000_000_000_000) // one Settle refuses a larger gap
27 maxInt64 = int64(9223372036854775807)
28)
29
30type wallet struct {
31 n25 int
32 n50 int
33 n75 int
34 n100 int
35 n125 int
36 n150 int
37 accrued int64
38}
39
40var (
41 wallets avl.Tree
42 tokenWho avl.Tree
43 tokenBps avl.Tree
44 totalWeight int64
45 accounted int64
46 epochs int64
47 self address
48 admin address
49)
50
51func Init(cur realm) {
52 caller := requireUser(cur)
53 if admin.IsValid() {
54 panic("perk2: init")
55 }
56 self = chain.PackageAddress(pkgPath)
57 if !self.IsValid() || self != cur.Address() {
58 panic("perk2: self")
59 }
60 admin = caller
61 chain.Emit("Init", "admin", caller.String())
62}
63
64func Sync(cur realm, id int) {
65 requireUser(cur)
66 requireInit()
67 if id < 1 {
68 panic("perk2: id")
69 }
70 syncID(cur, id)
71}
72
73func SyncSpan(cur realm, start, end int) {
74 requireUser(cur)
75 requireInit()
76 if start < 1 || end < start || end-start >= maxSpan {
77 panic("perk2: span")
78 }
79 for id := start; id <= end; id++ {
80 syncID(cur, id)
81 }
82}
83
84func Settle(cur realm) {
85 caller := requireUser(cur)
86 requireInit()
87 if caller != admin {
88 panic("perk2: admin")
89 }
90 delta := unsettledAmount()
91 if delta == 0 || totalWeight <= 0 {
92 return
93 }
94 if delta > maxDelta {
95 panic("perk2: amount")
96 }
97 keys := walletKeys()
98 parts := make([]int64, len(keys))
99 sum := int64(0)
100 allocated := int64(0)
101 for i, key := range keys {
102 share := shareOf(mustGet(key))
103 if share < 0 {
104 panic("perk2: weight")
105 }
106 if sum > maxInt64-share {
107 panic("perk2: weight")
108 }
109 sum += share
110 if share == 0 {
111 continue
112 }
113 if share > totalWeight || delta > maxInt64/share {
114 panic("perk2: amount")
115 }
116 part := share * delta / totalWeight
117 if part < 0 || allocated > maxInt64-part {
118 panic("perk2: amount")
119 }
120 parts[i] = part
121 allocated += part
122 }
123 if sum != totalWeight {
124 panic("perk2: weight")
125 }
126 if allocated == 0 || allocated > delta {
127 if allocated > delta {
128 panic("perk2: amount")
129 }
130 return
131 }
132 if accounted > maxInt64-allocated {
133 panic("perk2: amount")
134 }
135 for i, key := range keys {
136 if parts[i] == 0 {
137 continue
138 }
139 w := mustGet(key)
140 if w.accrued > maxInt64-parts[i] {
141 panic("perk2: amount")
142 }
143 w.accrued += parts[i]
144 wallets.Set(key, w)
145 }
146 accounted += allocated
147 if sumAccrued(keys) != accounted {
148 panic("perk2: accounted")
149 }
150 epochs++
151 chain.Emit("Settle", "allocated", strconv.FormatInt(allocated, 10), "epoch", strconv.FormatInt(epochs, 10))
152}
153
154func Claim(cur realm) {
155 caller := requireUser(cur)
156 requireInit()
157 w := load(caller.String())
158 if w == nil || w.accrued <= 0 {
159 panic("perk2: nothing")
160 }
161 amt := w.accrued
162 if amt > accounted {
163 panic("perk2: accounted")
164 }
165 if bankBalance() < amt {
166 panic("perk2: funds")
167 }
168 w.accrued = 0
169 accounted -= amt
170 key := addrKey(caller)
171 if shareOf(w) == 0 {
172 wallets.Remove(key)
173 } else {
174 wallets.Set(key, w)
175 }
176 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
177 bk.SendCoins(self, caller, chain.Coins{{Denom: native, Amount: amt}})
178 chain.Emit("Claim", "owner", caller.String(), "amount", strconv.FormatInt(amt, 10))
179}
180
181func ShareWeight(owner string) int64 {
182 w := load(owner)
183 if w == nil {
184 return 0
185 }
186 return shareOf(w)
187}
188
189func FeeDiscount(owner string) int64 {
190 w := load(owner)
191 if w == nil {
192 return 0
193 }
194 return bestOf(w)
195}
196
197func TotalWeight() int64 {
198 return totalWeight
199}
200
201func Claimable(owner string) int64 {
202 w := load(owner)
203 if w == nil {
204 return 0
205 }
206 if w.accrued < 0 {
207 panic("perk2: accounted")
208 }
209 return w.accrued
210}
211
212func Unsettled() int64 {
213 if !self.IsValid() {
214 return 0
215 }
216 return unsettledAmount()
217}
218
219func Epochs() int64 {
220 return epochs
221}
222
223func Accounted() int64 {
224 return accounted
225}
226
227func Admin() string {
228 if !admin.IsValid() {
229 return ""
230 }
231 return admin.String()
232}
233
234func Address() address {
235 return chain.PackageAddress(pkgPath)
236}
237
238func Render(_ string) string {
239 u := int64(0)
240 if self.IsValid() {
241 u = Unsettled()
242 }
243 return "# Bazaar perk2\n\n- total weight: " + strconv.FormatInt(totalWeight, 10) +
244 "\n- accounted: " + strconv.FormatInt(accounted, 10) +
245 " ugnot\n- unsettled: " + strconv.FormatInt(u, 10) +
246 " ugnot\n- epochs: " + strconv.FormatInt(epochs, 10) + "\n"
247}
248
249func syncID(cur realm, id int) {
250 who, bps := live(id)
251 key := strconv.Itoa(id)
252 oldWho := address("")
253 oldBps := int64(0)
254 if v := tokenWho.Get(key); v != nil {
255 oldWho = v.(address)
256 }
257 if v := tokenBps.Get(key); v != nil {
258 oldBps = v.(int64)
259 }
260 if oldWho == who && oldBps == bps {
261 return
262 }
263 if oldBps > 0 && oldWho.IsValid() {
264 addTier(cur, oldWho, oldBps, -1)
265 }
266 if bps > 0 && who.IsValid() {
267 addTier(cur, who, bps, 1)
268 }
269 if bps == 0 || !who.IsValid() {
270 tokenWho.Remove(key)
271 tokenBps.Remove(key)
272 chain.Emit("Sync", "id", key, "bps", "0")
273 return
274 }
275 tokenWho.Set(key, who)
276 tokenBps.Set(key, bps)
277 chain.Emit("Sync", "id", key, "owner", who.String(), "bps", strconv.FormatInt(bps, 10))
278}
279
280func live(id int) (address, int64) {
281 if id > col.Minted() {
282 panic("perk2: not minted")
283 }
284 owner, err := col.OwnerOf(strconv.Itoa(id))
285 if err != nil || !owner.IsValid() {
286 panic("perk2: unknown")
287 }
288 if owner == chain.PackageAddress(colPkg) {
289 return address(""), 0
290 }
291 return owner, bpsOf(col.RarityOf(id))
292}
293
294func bpsOf(rarity string) int64 {
295 switch strings.ToLower(strings.TrimSpace(rarity)) {
296 case "mythic":
297 return 150
298 case "legendary":
299 return 125
300 case "epic":
301 return 100
302 case "rare":
303 return 75
304 case "uncommon":
305 return 50
306 case "common":
307 return 25
308 default:
309 return 0
310 }
311}
312
313func addTier(cur realm, who address, bps int64, dir int) {
314 if dir != 1 && dir != -1 {
315 panic("perk2: dir")
316 }
317 if !who.IsValid() {
318 panic("perk2: owner")
319 }
320 w := mustWallet(who)
321 switch bps {
322 case 25:
323 w.n25 += dir
324 case 50:
325 w.n50 += dir
326 case 75:
327 w.n75 += dir
328 case 100:
329 w.n100 += dir
330 case 125:
331 w.n125 += dir
332 case 150:
333 w.n150 += dir
334 default:
335 panic("perk2: tier")
336 }
337 if w.n25 < 0 || w.n50 < 0 || w.n75 < 0 || w.n100 < 0 || w.n125 < 0 || w.n150 < 0 {
338 panic("perk2: count")
339 }
340 totalWeight += bps * int64(dir)
341 if totalWeight < 0 {
342 panic("perk2: weight")
343 }
344 key := addrKey(who)
345 if shareOf(w) == 0 && w.accrued == 0 {
346 wallets.Remove(key)
347 } else {
348 wallets.Set(key, w)
349 }
350 factory.NoteHolderDiscount(cross(cur), who.String(), bestOf(w))
351}
352
353func shareOf(w *wallet) int64 {
354 return int64(w.n25)*25 + int64(w.n50)*50 + int64(w.n75)*75 + int64(w.n100)*100 + int64(w.n125)*125 + int64(w.n150)*150
355}
356
357func bestOf(w *wallet) int64 {
358 if w.n150 > 0 {
359 return 150
360 }
361 if w.n125 > 0 {
362 return 125
363 }
364 if w.n100 > 0 {
365 return 100
366 }
367 if w.n75 > 0 {
368 return 75
369 }
370 if w.n50 > 0 {
371 return 50
372 }
373 if w.n25 > 0 {
374 return 25
375 }
376 return 0
377}
378
379func load(owner string) *wallet {
380 v := wallets.Get(strings.ToLower(strings.TrimSpace(owner)))
381 if v == nil {
382 return nil
383 }
384 w, ok := v.(*wallet)
385 if !ok || w == nil {
386 panic("perk2: wallet")
387 }
388 return w
389}
390
391func mustWallet(who address) *wallet {
392 w := load(who.String())
393 if w == nil {
394 w = &wallet{}
395 wallets.Set(addrKey(who), w)
396 }
397 return w
398}
399
400func mustGet(key string) *wallet {
401 v := wallets.Get(key)
402 if v == nil {
403 panic("perk2: wallet")
404 }
405 w, ok := v.(*wallet)
406 if !ok || w == nil {
407 panic("perk2: wallet")
408 }
409 return w
410}
411
412func walletKeys() []string {
413 keys := make([]string, 0)
414 wallets.Iterate("", "", func(key string, _ any) bool {
415 keys = append(keys, key)
416 return false
417 })
418 return keys
419}
420
421func sumAccrued(keys []string) int64 {
422 s := int64(0)
423 for _, key := range keys {
424 w := mustGet(key)
425 if w.accrued < 0 || s > maxInt64-w.accrued {
426 panic("perk2: accounted")
427 }
428 s += w.accrued
429 }
430 return s
431}
432
433func bankBalance() int64 {
434 if !self.IsValid() {
435 panic("perk2: init")
436 }
437 bal := banker.NewReadonlyBanker().GetCoins(self).AmountOf(native)
438 if bal < 0 || bal < accounted {
439 panic("perk2: balance")
440 }
441 return bal
442}
443
444func unsettledAmount() int64 {
445 return bankBalance() - accounted
446}
447
448func addrKey(who address) string {
449 return strings.ToLower(who.String())
450}
451
452func requireInit() {
453 if !admin.IsValid() || !self.IsValid() {
454 panic("perk2: init")
455 }
456}
457
458func requireUser(cur realm) address {
459 if !cur.IsCurrent() {
460 panic("perk2: spoofed realm")
461 }
462 prev := cur.Previous()
463 if !prev.IsUserCall() {
464 panic("perk2: EOA only")
465 }
466 if len(unsafe.OriginSend()) != 0 {
467 panic("perk2: extra denoms")
468 }
469 return prev.Address()
470}