Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

premium.gno

8.67 Kb · 278 lines
  1package nslogic
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/r/nym-mnsrehearsal001/nsdata/v2"
  8)
  9
 10// Global pricing layers that sit ACROSS every domain rather than inside
 11// one — see SPEC.md §27.
 12//
 13// Two of them, and they work at different points:
 14//
 15//	GLOBAL LENGTH RULES are a FLOOR beneath each domain's own length
 16//	rules, consulted only when the domain has nothing to say about that
 17//	length. "three-character names cost $5 everywhere" becomes one entry
 18//	instead of an edit to all twenty-seven domains, and a domain that
 19//	wants its own answer still overrides it.
 20//
 21//	GLOBAL NAME PREMIUMS are an ADJUSTMENT applied after the price has
 22//	already resolved. Making `og` premium is one entry covering
 23//	`og*degen`, `og*meme` and every domain invented later. Deliberately
 24//	not the per-name override, which REPLACES a price outright: "this
 25//	name costs exactly X here" and "this name is worth more everywhere"
 26//	are different tools and both are worth having.
 27//
 28// Full order, most specific first:
 29//
 30//  1. per-name override      (domain "nm")
 31//  2. per-domain length rule (domain "len")
 32//  3. global length rule     (global "glen")
 33//  4. domain base price      (domain "price")
 34//  5. global default         (global "defaultPrice")
 35//     then the global name premium modifies the result,
 36//     then any referral discount comes off what is left.
 37const (
 38	keyGlobalLens = "glen"    // global length rules
 39	keyPremium    = "premium" // premium1, premium2, ... see below
 40
 41	// The vault caps an extension value at maxExtraValueLen (512 bytes),
 42	// which is roughly 40-60 premium entries. The list is therefore split
 43	// across numbered keys rather than pretending one will do; a list
 44	// that silently truncated at the 512th byte would drop exactly the
 45	// entries nobody checked.
 46	maxPremiumKeys = 8
 47)
 48
 49// GlobalLengthRule returns the global rule for a label length, if any.
 50func GlobalLengthRule(n int) (string, bool) {
 51	return lookupRule(nsdata.GetGlobal(keyGlobalLens), strconv.Itoa(n))
 52}
 53
 54// PremiumFor returns the raw modifier for a name across all premium
 55// keys, or "" when the name is not premium anywhere.
 56func PremiumFor(label string) string {
 57	for i := 1; i <= maxPremiumKeys; i++ {
 58		csv := nsdata.GetGlobal(keyPremium + strconv.Itoa(i))
 59		if csv == "" {
 60			continue
 61		}
 62		if v, ok := lookupRule(csv, label); ok {
 63			return v
 64		}
 65	}
 66	return ""
 67}
 68
 69// applyPremium adjusts an already-resolved price.
 70//
 71// ZERO ALWAYS STAYS ZERO. A fixed "+$2.00" would otherwise silently undo
 72// a giveaway tier set months earlier, and a free tier that quietly stops
 73// being free is worse than never having offered one. This is decided
 74// explicitly rather than inherited from the percentage case happening to
 75// work out at zero.
 76//
 77// Forms: "%5" / "+%5" five percent more · "-%10" ten percent less ·
 78// "2.00" two dollars more · "-0.25" twenty-five cents less ·
 79// "=5.00" exactly five dollars, ignoring everything resolved above.
 80func applyPremium(mod string, price int64) int64 {
 81	mod = strings.TrimSpace(mod)
 82	if mod == "" || price <= 0 {
 83		return price
 84	}
 85
 86	// "=X" replaces outright. Checked before the sign handling because
 87	// an exact price is not a delta and has no direction.
 88	if strings.HasPrefix(mod, "=") {
 89		v, ok := parseUSD(mod[1:])
 90		if !ok || v < 0 {
 91			return price
 92		}
 93		return v
 94	}
 95
 96	neg := false
 97	switch {
 98	case strings.HasPrefix(mod, "-"):
 99		neg, mod = true, mod[1:]
100	case strings.HasPrefix(mod, "+"):
101		mod = mod[1:]
102	}
103
104	var delta int64
105	if strings.HasPrefix(mod, "%") {
106		pct, err := strconv.ParseInt(mod[1:], 10, 64)
107		// Out of range reads as malformed (see applyRule): an unbounded
108		// percentage would wrap into a wrong price rather than a high one.
109		if err != nil || pct < 0 || pct > maxRulePct || price > maxPriceUSD {
110			return price
111		}
112		delta = price * pct / 100
113	} else {
114		v, ok := parseUSD(mod)
115		if !ok || v < 0 {
116			return price
117		}
118		delta = v
119	}
120
121	if neg {
122		// A discount may take a name to free, but never below it: a
123		// negative price would be a payout, and this realm never pays
124		// anybody to register.
125		if delta >= price {
126			return 0
127		}
128		return price - delta
129	}
130	return price + delta
131}
132
133// parseUSD reads a human dollar figure — "2", "2.5", "2.50", "0.069" —
134// into micro-USD. Written by hand rather than with floats: a float would
135// make "0.07" cost a different number of micro-dollars depending on the
136// platform, and prices must be exact.
137func parseUSD(s string) (int64, bool) {
138	s = strings.TrimSpace(s)
139	if s == "" {
140		return 0, false
141	}
142	whole, frac := s, ""
143	if i := strings.IndexByte(s, '.'); i >= 0 {
144		whole, frac = s[:i], s[i+1:]
145	}
146	if whole == "" {
147		whole = "0"
148	}
149	w, err := strconv.ParseInt(whole, 10, 64)
150	// Capped at maxPriceUSD, so w*microUSD below cannot wrap.
151	if err != nil || w < 0 || w > maxPriceUSD/microUSD {
152		return 0, false
153	}
154	// Pad or truncate the fraction to exactly six digits of micro-USD.
155	if len(frac) > 6 {
156		frac = frac[:6]
157	}
158	for len(frac) < 6 {
159		frac += "0"
160	}
161	var f int64
162	if frac != "" {
163		f, err = strconv.ParseInt(frac, 10, 64)
164		if err != nil || f < 0 {
165			return 0, false
166		}
167	}
168	return w*microUSD + f, true
169}
170
171// SetGlobalLengthRules writes the project-wide length rules:
172// "1:300000000,2:150000000,3:60000000" prices one, two and three
173// character names everywhere a domain has not said otherwise.
174//
175// IT HAD NO SETTER UNTIL NOW, which meant the only way to write it was
176// the generic SetGlobal escape hatch — straight past normalizeRules, so
177// a typo stored fine and then vanished at lookup time, because
178// lookupRule skips a malformed pair in silence. A pricing layer whose
179// only writer cannot validate is a pricing layer that fails open.
180//
181// TIMELOCKED (timelock.gno), like every price rule set: "3:0" here makes
182// short names free on every domain that has no rule of its own.
183func SetGlobalLengthRules(cur realm, rules string) {
184	assertNoSend(cur)
185	assertIsAdmin(cur)
186	emitAdmin(cur, "SetGlobalLengthRules", "rules", rules)
187	norm := normalizeRules(rules, true)
188	requireAction(cur, actionID("SetGlobalLengthRules", norm))
189	setGlobal(cur, keyGlobalLens, norm)
190}
191
192// SetPremiumNames writes one page of the premium keyword list, 1-based.
193// The list is paged because the vault caps an extension value at 512
194// bytes; see maxPremiumKeys.
195//
196// Same gap as above: normalizePremiums existed and nothing exported
197// called it.
198func SetPremiumNames(cur realm, page int, csv string) {
199	assertNoSend(cur)
200	assertIsAdmin(cur)
201	emitAdmin(cur, "SetPremiumNames", "page", fmtI(page), "csv", csv)
202	if page < 1 || page > maxPremiumKeys {
203		panic("nslogic: premium page must be 1-" + strconv.Itoa(maxPremiumKeys))
204	}
205	v := normalizePremiums(csv)
206	if len(v) > 512 {
207		panic("nslogic: premium page " + strconv.Itoa(page) +
208			" is " + strconv.Itoa(len(v)) + " bytes; the vault caps a value at 512. Split it across pages.")
209	}
210	// TIMELOCKED: a premium of "=0" or "-%100" makes a name free everywhere.
211	requireAction(cur, actionID("SetPremiumNames", strconv.Itoa(page), v))
212	setGlobal(cur, keyPremium+strconv.Itoa(page), v)
213}
214
215// PremiumNames reads one page back, so the admin panel can show what is
216// stored rather than what it last sent.
217func PremiumNames(page int) string {
218	if page < 1 || page > maxPremiumKeys {
219		return ""
220	}
221	return nsdata.GetGlobal(keyPremium + strconv.Itoa(page))
222}
223
224// GlobalLengthRules returns the whole rule string, for the same reason.
225func GlobalLengthRules() string { return nsdata.GetGlobal(keyGlobalLens) }
226
227// normalizePremiums validates a premium list before storage. Same
228// reasoning as normalizeRules: a malformed entry must never reach the
229// vault, because every later lookup would carry it.
230func normalizePremiums(csv string) string {
231	if strings.TrimSpace(csv) == "" {
232		return ""
233	}
234	var out []string
235	seen := map[string]bool{}
236	for _, pair := range strings.Split(csv, ",") {
237		pair = strings.TrimSpace(pair)
238		if pair == "" {
239			continue
240		}
241		parts := strings.SplitN(pair, ":", 2)
242		if len(parts) != 2 {
243			panic("nslogic: bad premium, want name:modifier — got " + pair)
244		}
245		k, v := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
246		if k == "" || strings.ContainsAny(k, "*, \t\n") {
247			panic("nslogic: invalid name in premium — " + k)
248		}
249		if seen[k] {
250			panic("nslogic: duplicate premium name — " + k)
251		}
252		if !validPremiumValue(v) {
253			panic("nslogic: bad premium modifier for " + k + " — " + v)
254		}
255		seen[k] = true
256		out = append(out, k+":"+v)
257	}
258	return strings.Join(out, ",")
259}
260
261func validPremiumValue(v string) bool {
262	if v == "" {
263		return false
264	}
265	if strings.HasPrefix(v, "=") {
266		_, ok := parseUSD(v[1:])
267		return ok
268	}
269	if strings.HasPrefix(v, "-") || strings.HasPrefix(v, "+") {
270		v = v[1:]
271	}
272	if strings.HasPrefix(v, "%") {
273		p, err := strconv.ParseInt(v[1:], 10, 64)
274		return err == nil && p >= 0 && p <= maxRulePct
275	}
276	_, ok := parseUSD(v)
277	return ok
278}