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.

pricing.gno

9.33 Kb · 261 lines
  1package nslogic
  2
  3import (
  4	"math"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  9)
 10
 11// Pricing lives here, not in the vault. The vault stores numbers as
 12// opaque strings and has no opinion about what they mean, so the whole
 13// model can be replaced by swapping this realm.
 14//
 15// PRICES ARE DENOMINATED IN USD, held as MICRO-USD (1 USD = 1_000_000)
 16// so fractions of a cent are expressible — a name that costs about a
 17// gumball needs that. Nothing is stored in ugnot: the chain's token is
 18// just the settlement rail, converted at payment time from a stored
 19// rate. Accepting a second token later is then only a second rate, not
 20// a second price list.
 21//
 22// PRECEDENCE — most specific wins:
 23//
 24//  1. per-name override      (keyNames: "420:5000000,gm:%500")
 25//  2. per-length rule        (keyLens:  "3:2000000,4:%150,5:0")
 26//  3. GLOBAL length rule     (global keyGlobalLens — see premium.gno)
 27//  4. domain base price      (keyPrice: "500000"  = $0.50)
 28//  5. global default         (global keyDefaultPrice)
 29//
 30// then the GLOBAL NAME PREMIUM adjusts the result, and a referral
 31// discount comes off last. Both live in premium.gno.
 32//
 33// A rule value is either an absolute micro-USD amount or "%N", meaning N
 34// percent of that domain's base — so repricing a domain once carries
 35// every relative rule under it. "0" means free, which is how giveaway
 36// tiers are expressed: "all five-character names under *pleb are free"
 37// is just a length rule resolving to zero.
 38//
 39// Rules are stored as CSV in ONE key each rather than one key per rule.
 40// The vault caps extension keys at 32 bytes and 24 entries per record; a
 41// key like "nm:somelongname" breaks the first and a few dozen premium
 42// names break the second. Two CSV keys sidestep both.
 43const (
 44	keyPrice        = "price" // domain base, micro-USD
 45	keyLens         = "len"   // length rules
 46	keyNames        = "nm"    // per-name overrides
 47	keyOpen         = "open"  // "0" closes the domain to new names
 48	keyDefaultPrice = "defaultPrice"
 49	keyRate         = "rateUgnotPerUsd" // ugnot equal to 1 USD
 50
 51	microUSD         = int64(1_000_000)
 52	fallbackPriceUSD = int64(500_000) // $0.50
 53	// TESTNET PEG: $0.069 per GNOT, so 1 USD = 1/0.069 GNOT = 14.492753
 54	// GNOT = 14_492_753 ugnot. Deliberately a fixed number while we are on
 55	// a testnet whose token has no market price at all -- a "real" rate
 56	// there would be fiction dressed as precision. Replaced with a genuine
 57	// conversion before mainnet; tracked in MAINNET.md.
 58	fallbackRate = int64(14_492_753)
 59	maxRules     = 64
 60
 61	// The most any single USD price setting may say: one million dollars.
 62	// Far above any name this registry will list, and low enough that the
 63	// arithmetic on it stays far from overflow (see NamePrice).
 64	maxPriceUSD = int64(1_000_000) * microUSD
 65
 66	// The largest "%N" a price rule or premium may carry: a hundredfold.
 67	maxRulePct = 10_000
 68)
 69
 70// DomainOpen reports whether names may currently be registered under a
 71// domain. An absent key means open — a domain created before this
 72// existed must not silently close.
 73func DomainOpen(domainLabel string) bool {
 74	return nsdata.GetDomainExtra(domainLabel, keyOpen) != "0"
 75}
 76
 77// -- USD side --
 78
 79func DefaultPriceUSD() int64 {
 80	if v := nsdata.GetGlobal(keyDefaultPrice); v != "" {
 81		if p, err := strconv.ParseInt(v, 10, 64); err == nil && p >= 0 {
 82			return p
 83		}
 84	}
 85	return fallbackPriceUSD
 86}
 87
 88func DomainBasePriceUSD(domainLabel string) int64 {
 89	if v := nsdata.GetDomainExtra(domainLabel, keyPrice); v != "" {
 90		if p, err := strconv.ParseInt(v, 10, 64); err == nil && p >= 0 {
 91			return p
 92		}
 93	}
 94	return DefaultPriceUSD()
 95}
 96
 97// NamePriceUSD resolves the four inputs in precedence order and returns
 98// micro-USD. This is the authoritative price; ugnot is derived from it.
 99func NamePriceUSD(domainLabel, label string) int64 {
100	return applyPremium(PremiumFor(label), resolveBeforePremium(domainLabel, label))
101}
102
103// resolveBeforePremium runs steps 1-5. Separated from NamePriceUSD so
104// the admin panel can show WHY a price is what it is: the figure before
105// the premium and the figure after are both meaningful, and a single
106// function returning only the total makes a wrong price impossible to
107// diagnose.
108func resolveBeforePremium(domainLabel, label string) int64 {
109	base := DomainBasePriceUSD(domainLabel)
110	if v, ok := lookupRule(nsdata.GetDomainExtra(domainLabel, keyNames), label); ok {
111		return applyRule(v, base)
112	}
113	if v, ok := lookupRule(nsdata.GetDomainExtra(domainLabel, keyLens), strconv.Itoa(len(label))); ok {
114		return applyRule(v, base)
115	}
116	// The global length rule is a floor, not a replacement: it applies
117	// only where the domain itself had nothing to say about this length.
118	if v, ok := GlobalLengthRule(len(label)); ok {
119		return applyRule(v, base)
120	}
121	return base
122}
123
124// ExplainPriceUSD returns each stage of the resolution, so the control
125// panel can show the arithmetic rather than assert a total. Returns the
126// price before any premium, the premium modifier that applied (empty if
127// none), and the final price.
128func ExplainPriceUSD(domainLabel, label string) (beforePremium int64, modifier string, final int64) {
129	before := resolveBeforePremium(domainLabel, label)
130	mod := PremiumFor(label)
131	return before, mod, applyPremium(mod, before)
132}
133
134// -- settlement side --
135
136// RateUgnotPerUsd is how many ugnot one USD buys.
137//
138// This is a MANUALLY MAINTAINED rate, not a market oracle: an admin
139// writes it and it sits there until rewritten. That is a real
140// operational hazard — a stale rate during a sharp move sells names at
141// the wrong price in whichever direction hurts — and the mitigation is
142// keeping it fresh, not anything this code can do. Kept as one global so
143// a future logic realm can swap in a real oracle without touching the
144// vault or restating a single price.
145func RateUgnotPerUsd() int64 {
146	if v := nsdata.GetGlobal(keyRate); v != "" {
147		if r, err := strconv.ParseInt(v, 10, 64); err == nil && r > 0 {
148			return r
149		}
150	}
151	return fallbackRate
152}
153
154// NamePrice converts the USD price to ugnot for settlement. Rounds UP,
155// so rounding never sells a name below its listed price; a free name
156// stays exactly free.
157//
158// IT REFUSES TO OVERFLOW. usd*rate is int64 arithmetic, which wraps
159// silently, and a wrapped product is a price of millions of GNOT or a
160// negative one — and a negative price made takePaymentSplit refund more
161// than was paid (audit L5). A price that cannot be computed is refused,
162// which fails closed: no sale, never a wrong one.
163func NamePrice(domainLabel, label string) int64 {
164	usd := NamePriceUSD(domainLabel, label)
165	if usd <= 0 {
166		return 0
167	}
168	rate := RateUgnotPerUsd()
169	if usd > (math.MaxInt64-microUSD)/rate {
170		panic("nslogic: this name's price does not fit in a ugnot amount; the price list needs fixing")
171	}
172	return (usd*rate + microUSD - 1) / microUSD
173}
174
175// -- rule plumbing --
176
177// applyRule reads a stored rule. An out-of-range one is treated like a
178// malformed one — the base applies — because stored values can arrive by
179// the restore window, unvalidated, and an unbounded percentage would wrap
180// the multiplication into a small, wrong, price.
181func applyRule(v string, base int64) int64 {
182	if strings.HasPrefix(v, "%") {
183		pct, err := strconv.ParseInt(v[1:], 10, 64)
184		if err != nil || pct < 0 || pct > maxRulePct || base > maxPriceUSD {
185			return base
186		}
187		return base * pct / 100
188	}
189	n, err := strconv.ParseInt(v, 10, 64)
190	if err != nil || n < 0 || n > maxPriceUSD {
191		return base
192	}
193	return n
194}
195
196func lookupRule(csv, want string) (string, bool) {
197	if csv == "" {
198		return "", false
199	}
200	for _, pair := range strings.Split(csv, ",") {
201		parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
202		if len(parts) != 2 {
203			continue
204		}
205		if strings.TrimSpace(parts[0]) == want {
206			return strings.TrimSpace(parts[1]), true
207		}
208	}
209	return "", false
210}
211
212// normalizeRules validates and canonicalises a rule string before it is
213// stored, so a malformed entry can never reach storage and poison every
214// later lookup.
215func normalizeRules(csv string, numericKeys bool) string {
216	if strings.TrimSpace(csv) == "" {
217		return ""
218	}
219	var out []string
220	seen := map[string]bool{}
221	for _, pair := range strings.Split(csv, ",") {
222		pair = strings.TrimSpace(pair)
223		if pair == "" {
224			continue
225		}
226		parts := strings.SplitN(pair, ":", 2)
227		if len(parts) != 2 {
228			panic("nslogic: bad rule, want key:value — got " + pair)
229		}
230		k, v := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
231		if k == "" {
232			panic("nslogic: empty rule key")
233		}
234		if numericKeys {
235			if n, err := strconv.Atoi(k); err != nil || n <= 0 {
236				panic("nslogic: length must be a positive number — got " + k)
237			}
238		} else if strings.ContainsAny(k, "*, \t\n") {
239			panic("nslogic: invalid name in rule — " + k)
240		}
241		// Bounded as well as well-formed: an amount above maxPriceUSD or a
242		// percentage above maxRulePct is a typo, and an unbounded one could
243		// overflow the price arithmetic downstream.
244		if strings.HasPrefix(v, "%") {
245			if p, err := strconv.ParseInt(v[1:], 10, 64); err != nil || p < 0 || p > maxRulePct {
246				panic("nslogic: bad percentage (0-" + strconv.Itoa(maxRulePct) + ") — " + v)
247			}
248		} else if n, err := strconv.ParseInt(v, 10, 64); err != nil || n < 0 || n > maxPriceUSD {
249			panic("nslogic: bad amount — " + v)
250		}
251		if seen[k] {
252			panic("nslogic: duplicate rule key — " + k)
253		}
254		seen[k] = true
255		out = append(out, k+":"+v)
256		if len(out) > maxRules {
257			panic("nslogic: too many rules (max 64)")
258		}
259	}
260	return strings.Join(out, ",")
261}