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

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}