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

campaign.gno

4.27 Kb · 166 lines
  1// Package campaign is a near-verbatim copy of the real gems6/campaign
  2// realm, redeployed here under the test address so this whole
  3// reveal-delay + gemsartfab prototype is self-contained and testable
  4// without touching the live gems6 deployment or needing the vesting
  5// wallet's own key. Logic is unchanged from the original -- see that
  6// package's own doc comments for the full design rationale (why a
  7// dedicated prunable realm instead of a Merkle root, the stacking rule
  8// in grantOne, etc.).
  9package campaign
 10
 11// Storage (2026-09-26): every grant lives in ONE map, packed as a single
 12// int64 (remainingUses*packBase + bps), plus one slice of wallets for
 13// EntryAt's stable order. The first version kept an avl.Tree of *entry
 14// pointers: every wallet became its own tree node plus entry object, about
 15// 2.9 KB each on a local chain (0.29 GNOT per wallet, ~85 GNOT for the
 16// 284-wallet campaign). A map and a slice are stored as a few objects
 17// whose size grows by roughly the key and value per wallet.
 18
 19import (
 20	unsaferealm "chain/runtime/unsafe"
 21	"strings"
 22)
 23
 24const packBase = 10001 // bps is 0..10000
 25
 26var (
 27	collectionOwner  address = "g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems"
 28	approvedConsumer string
 29	grants           = map[string]int64{} // wallet -> remainingUses*packBase + bps
 30	order            []string             // wallets with a grant, for EntryAt
 31)
 32
 33func pack(bps, uses int64) int64     { return uses*packBase + bps }
 34func unpack(v int64) (bps, uses int64) { return v % packBase, v / packBase }
 35
 36func callerAddress() address {
 37	return unsaferealm.PreviousRealm().Address()
 38}
 39
 40func assertOwner() {
 41	if callerAddress() != collectionOwner {
 42		panic("unauthorized: caller is not the collection owner")
 43	}
 44}
 45
 46func SetApprovedConsumer(cur realm, pkgpath string) {
 47	assertOwner()
 48	approvedConsumer = pkgpath
 49}
 50
 51func validateBps(bps int64) {
 52	if bps < 0 || bps > 10000 {
 53		panic("bps must be between 0 and 10000")
 54	}
 55}
 56
 57func validateMaxUses(maxUses int64) {
 58	if maxUses <= 0 {
 59		panic("maxUses must be positive")
 60	}
 61}
 62
 63// grantOne adds uses to a wallet; a second grant stacks its uses and keeps
 64// 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 = oldBps
 70		}
 71		grants[wallet] = pack(bps, uses+maxUses)
 72		return
 73	}
 74	grants[wallet] = pack(bps, maxUses)
 75	order = append(order, wallet)
 76}
 77
 78func removeOne(wallet string) {
 79	if _, ok := grants[wallet]; !ok {
 80		return
 81	}
 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			break
 88		}
 89	}
 90}
 91
 92func AddWallet(cur realm, wallet address, bps int64, maxUses int64) {
 93	assertOwner()
 94	validateBps(bps)
 95	validateMaxUses(maxUses)
 96	grantOne(wallet.String(), bps, maxUses)
 97}
 98
 99func 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			continue
107		}
108		grantOne(w, bps, maxUses)
109	}
110}
111
112func RemoveWallet(cur realm, wallet address) {
113	assertOwner()
114	removeOne(wallet.String())
115}
116
117func RemoveWalletsBatch(cur realm, walletsCSV string) {
118	assertOwner()
119	for _, w := range strings.Split(walletsCSV, ",") {
120		w = strings.TrimSpace(w)
121		if w == "" {
122			continue
123		}
124		removeOne(w)
125	}
126}
127
128func 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}
134
135func Peek(wallet address) (bps int64, remainingUses int64, ok bool) {
136	v, has := grants[wallet.String()]
137	if !has {
138		return 0, 0, false
139	}
140	bps, remainingUses = unpack(v)
141	return bps, remainingUses, true
142}
143
144func 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, false
150	}
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, true
158}
159
160func Count() int64 { return int64(len(order)) }
161
162func EntryAt(index int) (wallet string, bps int64, remainingUses int64) {
163	wallet = order[index]
164	bps, remainingUses = unpack(grants[wallet])
165	return wallet, bps, remainingUses
166}