1package perk23import (4 "chain"5 "chain/banker"6 "chain/runtime/unsafe"7 "strconv"8 "strings"910 "gno.land/p/nt/avl/v0"11 col "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/bazaargens"12 factory "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5"13)1415// 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 SetGenesis18// on bazaarv5 so Buy sends the 11% protocol share here and NoteHolderDiscount19// accepts this realm. Sync must be called after mint, transfer, list, or reveal.20// Rarity stays unread until the collection reveals it.2122const (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 = 2028 maxDelta = int64(9_000_000_000_000) // delta * scale stays inside int6429)3031type wallet struct {32 n25 int33 n50 int34 n75 int35 n100 int36 n125 int37 n150 int38 debt int6439 accrued int6440}4142var (43 wallets avl.Tree44 tokenWho avl.Tree45 tokenBps avl.Tree46 totalWeight int6447 acc int6448 accounted int6449 self address50)5152func Sync(cur realm, id int) {53 requireUser(cur)54 if id < 1 {55 panic("perk: id")56 }57 syncID(cur, id)58}5960func 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}6970func Claim(cur realm) {71 caller := requireUser(cur)72 syncPot(cur)73 w := mustWallet(caller)74 settle(w)75 amt := w.accrued76 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 = 087 accounted -= amt88 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}9293func ShareWeight(owner string) int64 {94 w := load(owner)95 if w == nil {96 return 097 }98 return shareOf(w)99}100101func FeeDiscount(owner string) int64 {102 w := load(owner)103 if w == nil {104 return 0105 }106 return bestOf(w)107}108109func TotalWeight() int64 {110 return totalWeight111}112113func Render(_ string) string {114 return "# Bazaar perk\n\n- total weight: " + strconv.FormatInt(totalWeight, 10) + "\n- accounted: " + strconv.FormatInt(accounted, 10) + " ugnot\n"115}116117func 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 return130 }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 return142 }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}147148func 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(""), 0155 }156 return owner, bpsOf(col.RarityOf(id))157}158159func bpsOf(rarity string) int64 {160 switch strings.ToLower(strings.TrimSpace(rarity)) {161 case "mythic":162 return 150163 case "legendary":164 return 125165 case "epic":166 return 100167 case "rare":168 return 75169 case "uncommon":170 return 50171 case "common":172 return 25173 default:174 return 0175 }176}177178func 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 += dir188 case 50:189 w.n50 += dir190 case 75:191 w.n75 += dir192 case 100:193 w.n100 += dir194 case 125:195 w.n125 += dir196 case 150:197 w.n150 += dir198 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 / scale209 wallets.Set(who.String(), w)210 factory.NoteHolderDiscount(cross(cur), who.String(), bestOf(w))211}212213func settle(w *wallet) {214 sw := shareOf(w)215 got := sw * acc / scale216 if got < w.debt {217 panic("perk: debt")218 }219 w.accrued += got - w.debt220 w.debt = got221}222223func 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 - accounted233 if delta == 0 || totalWeight <= 0 {234 return235 }236 if delta > maxDelta {237 panic("perk: amount")238 }239 bump := delta * scale / totalWeight240 if bump <= 0 {241 return242 }243 allocated := bump * totalWeight / scale244 if allocated <= 0 {245 return246 }247 if acc > maxDelta*scale-bump {248 panic("perk: acc")249 }250 acc += bump251 accounted += allocated252}253254func 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)*150256}257258func bestOf(w *wallet) int64 {259 if w.n150 > 0 {260 return 150261 }262 if w.n125 > 0 {263 return 125264 }265 if w.n100 > 0 {266 return 100267 }268 if w.n75 > 0 {269 return 75270 }271 if w.n50 > 0 {272 return 50273 }274 if w.n25 > 0 {275 return 25276 }277 return 0278}279280func load(owner string) *wallet {281 v := wallets.Get(strings.ToLower(strings.TrimSpace(owner)))282 if v == nil {283 return nil284 }285 return v.(*wallet)286}287288func 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 w295}296297func 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}310Claim(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
FeeDiscount(owner string) int64
Render(string) string
ShareWeight(owner string) int64
Sync(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
SyncSpan(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, start int, end int)
TotalWeight() int64
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.