1// Package campaign is a near-verbatim copy of the real gems6/campaign2// realm, redeployed here under the test address so this whole3// reveal-delay + gemsartfab prototype is self-contained and testable4// without touching the live gems6 deployment or needing the vesting5// wallet's own key. Logic is unchanged from the original -- see that6// package's own doc comments for the full design rationale (why a7// dedicated prunable realm instead of a Merkle root, the stacking rule8// in grantOne, etc.).9package campaign1011// Storage (2026-09-26): every grant lives in ONE map, packed as a single12// int64 (remainingUses*packBase + bps), plus one slice of wallets for13// EntryAt's stable order. The first version kept an avl.Tree of *entry14// pointers: every wallet became its own tree node plus entry object, about15// 2.9 KB each on a local chain (0.29 GNOT per wallet, ~85 GNOT for the16// 284-wallet campaign). A map and a slice are stored as a few objects17// whose size grows by roughly the key and value per wallet.1819import (20 unsaferealm "chain/runtime/unsafe"21 "strings"22)2324const packBase = 10001 // bps is 0..100002526var (27 collectionOwner address = "g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems"28 approvedConsumer string29 grants = map[string]int64{} // wallet -> remainingUses*packBase + bps30 order []string // wallets with a grant, for EntryAt31)3233func pack(bps, uses int64) int64 { return uses*packBase + bps }34func unpack(v int64) (bps, uses int64) { return v % packBase, v / packBase }3536func callerAddress() address {37 return unsaferealm.PreviousRealm().Address()38}3940func assertOwner() {41 if callerAddress() != collectionOwner {42 panic("unauthorized: caller is not the collection owner")43 }44}4546func SetApprovedConsumer(cur realm, pkgpath string) {47 assertOwner()48 approvedConsumer = pkgpath49}5051func validateBps(bps int64) {52 if bps < 0 || bps > 10000 {53 panic("bps must be between 0 and 10000")54 }55}5657func validateMaxUses(maxUses int64) {58 if maxUses <= 0 {59 panic("maxUses must be positive")60 }61}6263// grantOne adds uses to a wallet; a second grant stacks its uses and keeps64// the better (lower) bps.65func grantOne(wallet string, bps int64, maxUses int64) {66 if v, ok := grants[wallet]; ok {67 oldBps, uses := unpack(v)68 if oldBps < bps {69 bps = oldBps70 }71 grants[wallet] = pack(bps, uses+maxUses)72 return73 }74 grants[wallet] = pack(bps, maxUses)75 order = append(order, wallet)76}7778func removeOne(wallet string) {79 if _, ok := grants[wallet]; !ok {80 return81 }82 delete(grants, wallet)83 for i, w := range order {84 if w == wallet {85 order[i] = order[len(order)-1]86 order = order[:len(order)-1]87 break88 }89 }90}9192func AddWallet(cur realm, wallet address, bps int64, maxUses int64) {93 assertOwner()94 validateBps(bps)95 validateMaxUses(maxUses)96 grantOne(wallet.String(), bps, maxUses)97}9899func AddWalletsBatch(cur realm, walletsCSV string, bps int64, maxUses int64) {100 assertOwner()101 validateBps(bps)102 validateMaxUses(maxUses)103 for _, w := range strings.Split(walletsCSV, ",") {104 w = strings.TrimSpace(w)105 if w == "" {106 continue107 }108 grantOne(w, bps, maxUses)109 }110}111112func RemoveWallet(cur realm, wallet address) {113 assertOwner()114 removeOne(wallet.String())115}116117func RemoveWalletsBatch(cur realm, walletsCSV string) {118 assertOwner()119 for _, w := range strings.Split(walletsCSV, ",") {120 w = strings.TrimSpace(w)121 if w == "" {122 continue123 }124 removeOne(w)125 }126}127128func assertCallerIsApprovedConsumer(cur realm) {129 caller := cur.Previous().PkgPath()130 if approvedConsumer == "" || caller != approvedConsumer {131 panic("unauthorized: caller is not the approved consumer realm")132 }133}134135func Peek(wallet address) (bps int64, remainingUses int64, ok bool) {136 v, has := grants[wallet.String()]137 if !has {138 return 0, 0, false139 }140 bps, remainingUses = unpack(v)141 return bps, remainingUses, true142}143144func ConsumeDiscount(cur realm, wallet address) (bps int64, ok bool) {145 assertCallerIsApprovedConsumer(cur)146 w := wallet.String()147 v, has := grants[w]148 if !has {149 return 0, false150 }151 bps, uses := unpack(v)152 if uses <= 1 {153 removeOne(w)154 } else {155 grants[w] = pack(bps, uses-1)156 }157 return bps, true158}159160func Count() int64 { return int64(len(order)) }161162func EntryAt(index int) (wallet string, bps int64, remainingUses int64) {163 wallet = order[index]164 bps, remainingUses = unpack(grants[wallet])165 return wallet, bps, remainingUses166}167AddWallet(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}, wallet string, bps int64, maxUses int64)
AddWalletsBatch(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}, walletsCSV string, bps int64, maxUses int64)
ConsumeDiscount(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}, wallet string) (bps int64, ok bool)
Count() int64
EntryAt(index int) (wallet string, bps int64, remainingUses int64)
Peek(wallet string) (bps int64, remainingUses int64, ok bool)
RemoveWallet(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}, wallet string)
RemoveWalletsBatch(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}, walletsCSV string)
SetApprovedConsumer(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}, pkgpath string)
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.