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}