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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}