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

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}