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.

perk.gno

6.11 Kb · 309 lines
  1package perk
  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// Perk counts revealed, unlisted Bazaar Gens held in a wallet.
 16// Fee discount is the highest tier. Share weight is the sum.
 17// Mainnet staging retargets colPkg to c/bazaargens. Admin then SetGenesis
 18// on bazaarv5 so Buy sends the 11% protocol share here and NoteHolderDiscount
 19// accepts this realm. Sync must be called after mint, transfer, list, or reveal.
 20// Rarity stays unread until the collection reveals it.
 21
 22const (
 23	pkgPath   = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/perk"
 24	colPkg    = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/bazaargens"
 25	native    = "ugnot"
 26	scale     = int64(1_000_000)
 27	maxSpan   = 20
 28	maxDelta  = int64(9_000_000_000_000) // delta * scale stays inside int64
 29)
 30
 31type wallet struct {
 32	n25     int
 33	n50     int
 34	n75     int
 35	n100    int
 36	n125    int
 37	n150    int
 38	debt    int64
 39	accrued int64
 40}
 41
 42var (
 43	wallets     avl.Tree
 44	tokenWho    avl.Tree
 45	tokenBps    avl.Tree
 46	totalWeight int64
 47	acc         int64
 48	accounted   int64
 49	self        address
 50)
 51
 52func Sync(cur realm, id int) {
 53	requireUser(cur)
 54	if id < 1 {
 55		panic("perk: id")
 56	}
 57	syncID(cur, id)
 58}
 59
 60func SyncSpan(cur realm, start, end int) {
 61	requireUser(cur)
 62	if start < 1 || end < start || end-start >= maxSpan {
 63		panic("perk: span")
 64	}
 65	for id := start; id <= end; id++ {
 66		syncID(cur, id)
 67	}
 68}
 69
 70func Claim(cur realm) {
 71	caller := requireUser(cur)
 72	syncPot(cur)
 73	w := mustWallet(caller)
 74	settle(w)
 75	amt := w.accrued
 76	if amt <= 0 {
 77		panic("perk: nothing")
 78	}
 79	if amt > accounted {
 80		panic("perk: accounted")
 81	}
 82	bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
 83	if bk.GetCoins(self).AmountOf(native) < amt {
 84		panic("perk: funds")
 85	}
 86	w.accrued = 0
 87	accounted -= amt
 88	wallets.Set(caller.String(), w)
 89	bk.SendCoins(self, caller, chain.Coins{{Denom: native, Amount: amt}})
 90	chain.Emit("Claim", "owner", caller.String(), "amount", strconv.FormatInt(amt, 10))
 91}
 92
 93func ShareWeight(owner string) int64 {
 94	w := load(owner)
 95	if w == nil {
 96		return 0
 97	}
 98	return shareOf(w)
 99}
100
101func FeeDiscount(owner string) int64 {
102	w := load(owner)
103	if w == nil {
104		return 0
105	}
106	return bestOf(w)
107}
108
109func TotalWeight() int64 {
110	return totalWeight
111}
112
113func Render(_ string) string {
114	return "# Bazaar perk\n\n- total weight: " + strconv.FormatInt(totalWeight, 10) + "\n- accounted: " + strconv.FormatInt(accounted, 10) + " ugnot\n"
115}
116
117func syncID(cur realm, id int) {
118	who, bps := live(id)
119	key := strconv.Itoa(id)
120	oldWho := address("")
121	oldBps := int64(0)
122	if v := tokenWho.Get(key); v != nil {
123		oldWho = v.(address)
124	}
125	if v := tokenBps.Get(key); v != nil {
126		oldBps = v.(int64)
127	}
128	if oldWho == who && oldBps == bps {
129		return
130	}
131	if oldBps > 0 && oldWho.IsValid() {
132		addTier(cur, oldWho, oldBps, -1)
133	}
134	if bps > 0 && who.IsValid() {
135		addTier(cur, who, bps, 1)
136	}
137	if bps == 0 || !who.IsValid() {
138		tokenWho.Remove(key)
139		tokenBps.Remove(key)
140		chain.Emit("Sync", "id", key, "bps", "0")
141		return
142	}
143	tokenWho.Set(key, who)
144	tokenBps.Set(key, bps)
145	chain.Emit("Sync", "id", key, "owner", who.String(), "bps", strconv.FormatInt(bps, 10))
146}
147
148func live(id int) (address, int64) {
149	owner, err := col.OwnerOf(strconv.Itoa(id))
150	if err != nil || !owner.IsValid() {
151		panic("perk: unknown")
152	}
153	if owner == chain.PackageAddress(colPkg) {
154		return address(""), 0
155	}
156	return owner, bpsOf(col.RarityOf(id))
157}
158
159func bpsOf(rarity string) int64 {
160	switch strings.ToLower(strings.TrimSpace(rarity)) {
161	case "mythic":
162		return 150
163	case "legendary":
164		return 125
165	case "epic":
166		return 100
167	case "rare":
168		return 75
169	case "uncommon":
170		return 50
171	case "common":
172		return 25
173	default:
174		return 0
175	}
176}
177
178func addTier(cur realm, who address, bps int64, dir int) {
179	if dir != 1 && dir != -1 {
180		panic("perk: dir")
181	}
182	syncPot(cur)
183	w := mustWallet(who)
184	settle(w)
185	switch bps {
186	case 25:
187		w.n25 += dir
188	case 50:
189		w.n50 += dir
190	case 75:
191		w.n75 += dir
192	case 100:
193		w.n100 += dir
194	case 125:
195		w.n125 += dir
196	case 150:
197		w.n150 += dir
198	default:
199		panic("perk: tier")
200	}
201	if w.n25 < 0 || w.n50 < 0 || w.n75 < 0 || w.n100 < 0 || w.n125 < 0 || w.n150 < 0 {
202		panic("perk: count")
203	}
204	totalWeight += bps * int64(dir)
205	if totalWeight < 0 {
206		panic("perk: weight")
207	}
208	w.debt = shareOf(w) * acc / scale
209	wallets.Set(who.String(), w)
210	factory.NoteHolderDiscount(cross(cur), who.String(), bestOf(w))
211}
212
213func settle(w *wallet) {
214	sw := shareOf(w)
215	got := sw * acc / scale
216	if got < w.debt {
217		panic("perk: debt")
218	}
219	w.accrued += got - w.debt
220	w.debt = got
221}
222
223func syncPot(cur realm) {
224	if !self.IsValid() {
225		self = cur.Address()
226	}
227	bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
228	bal := bk.GetCoins(self).AmountOf(native)
229	if bal < accounted {
230		panic("perk: balance")
231	}
232	delta := bal - accounted
233	if delta == 0 || totalWeight <= 0 {
234		return
235	}
236	if delta > maxDelta {
237		panic("perk: amount")
238	}
239	bump := delta * scale / totalWeight
240	if bump <= 0 {
241		return
242	}
243	allocated := bump * totalWeight / scale
244	if allocated <= 0 {
245		return
246	}
247	if acc > maxDelta*scale-bump {
248		panic("perk: acc")
249	}
250	acc += bump
251	accounted += allocated
252}
253
254func shareOf(w *wallet) int64 {
255	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
256}
257
258func bestOf(w *wallet) int64 {
259	if w.n150 > 0 {
260		return 150
261	}
262	if w.n125 > 0 {
263		return 125
264	}
265	if w.n100 > 0 {
266		return 100
267	}
268	if w.n75 > 0 {
269		return 75
270	}
271	if w.n50 > 0 {
272		return 50
273	}
274	if w.n25 > 0 {
275		return 25
276	}
277	return 0
278}
279
280func load(owner string) *wallet {
281	v := wallets.Get(strings.ToLower(strings.TrimSpace(owner)))
282	if v == nil {
283		return nil
284	}
285	return v.(*wallet)
286}
287
288func mustWallet(who address) *wallet {
289	w := load(who.String())
290	if w == nil {
291		w = &wallet{}
292		wallets.Set(who.String(), w)
293	}
294	return w
295}
296
297func requireUser(cur realm) address {
298	if !cur.IsCurrent() {
299		panic("perk: spoofed realm")
300	}
301	prev := cur.Previous()
302	if !prev.IsUserCall() {
303		panic("perk: EOA only")
304	}
305	if len(unsafe.OriginSend()) != 0 {
306		panic("perk: extra denoms")
307	}
308	return prev.Address()
309}