term.gno
4.65 Kb · 135 lines
1package nslogic
2
3import (
4 "strconv"
5 "time"
6
7 "gno.land/p/nt/ufmt/v0"
8 "gno.land/r/nym-mnsrehearsal001/nsdata/v2"
9)
10
11// How far ahead a name may be paid.
12//
13// Renewal was unbounded. RenewNameRecord adds one term to the previous
14// due date and asks no further questions, so a holder pressing renew
15// seven times bought seven years of runway, and nothing anywhere said
16// that was not intended. It is a bug in two directions at once:
17//
18// - Free domains have a base price of zero. Renewal is free too, so a
19// script could take every good name under *gnoland and hold it for a
20// century for the cost of gas. There is no economic brake on a free
21// name at all; the term limit IS the brake.
22// - A paid name held fifty years ahead is fifty years of a promise we
23// have made against a rate written by hand and a testnet that will
24// be replaced. Selling that is easy and honouring it is not.
25//
26// The limit is RUNWAY, not total lifetime: at most N years may be
27// outstanding at any moment. Nobody is stopped from holding a name
28// forever — they renew as the runway burns down, which is the same
29// arrangement every domain registrar has always used and the one people
30// already understand.
31//
32// Per domain first, then a project-wide default, so a free domain can be
33// capped at one year without capping anything else.
34const (
35 keyMaxTerm = "maxterm" // domain extra AND global: years of runway
36 defaultMaxTerm = int64(5)
37 maxMaxTermYears = int64(100)
38)
39
40// MaxTermYears is the cap that applies to names under this domain.
41func MaxTermYears(domainLabel string) int64 {
42 if v := nsdata.GetDomainExtra(domainLabel, keyMaxTerm); v != "" {
43 if y, err := strconv.ParseInt(v, 10, 64); err == nil && y > 0 {
44 return y
45 }
46 }
47 if v := nsdata.GetGlobal(keyMaxTerm); v != "" {
48 if y, err := strconv.ParseInt(v, 10, 64); err == nil && y > 0 {
49 return y
50 }
51 }
52 return defaultMaxTerm
53}
54
55// SetMaxTerm caps a single domain, or the whole project when the domain
56// label is empty. A domain's own cap always wins, so tightening one free
57// domain does not disturb the rest.
58func SetMaxTerm(cur realm, domainLabel string, years int64) {
59 assertNoSend(cur)
60 assertIsAdmin(cur)
61 emitAdmin(cur, "SetMaxTerm", "domain", domainLabel, "years", fmtInt(years))
62 if years < 1 || years > maxMaxTermYears {
63 panic("nslogic: term cap must be 1-100 years")
64 }
65 v := strconv.FormatInt(years, 10)
66 if domainLabel == "" {
67 setGlobal(cur, keyMaxTerm, v)
68 return
69 }
70 if !nsdata.DomainExists(domainLabel) {
71 panic("nslogic: unknown domain")
72 }
73 if err := nsdata.SetDomainExtraRecord(cross(cur), domainLabel, keyMaxTerm, v); err != nil {
74 panic(err)
75 }
76}
77
78// RenewalsLeft is how many more years may be added to a name right now.
79// Zero means the renew button should be off, and the site reads this
80// rather than doing the arithmetic itself — the cap can be per domain,
81// and a page that guesses it will guess wrong on exactly the domains
82// where it matters.
83//
84// An unregistered name reports the full allowance: nothing is holding
85// runway yet.
86func RenewalsLeft(label, domainLabel string) int64 {
87 maxYears := MaxTermYears(domainLabel)
88 if !nsdata.NameExists(label, domainLabel) {
89 return maxYears
90 }
91 term := nsdata.GetTermLength()
92 if term <= 0 {
93 return 0
94 }
95 _, _, expires, _, _, _ := nsdata.GetNameInfo(label, domainLabel)
96 return renewalsLeft(expires, time.Now().Unix(), term, maxYears)
97}
98
99// renewalsLeft is the arithmetic on its own, so it can be tested without
100// a chain under it.
101//
102// Runway is measured from NOW, not from the registration date, so a name
103// deep in its grace period counts as zero rather than as a negative that
104// would then read as extra room. Years already bought round UP: a name
105// with thirteen months left is holding two of its five, because the
106// alternative lets somebody sit permanently at four-years-and-eleven-
107// months and renew every month forever.
108func renewalsLeft(expires, now, term, maxYears int64) int64 {
109 if term <= 0 {
110 return 0
111 }
112 runway := expires - now
113 if runway < 0 {
114 runway = 0
115 }
116 left := maxYears - (runway+term-1)/term
117 if left < 0 {
118 return 0
119 }
120 return left
121}
122
123// assertRoomToRenew is the enforcement. It runs before payment is taken,
124// so a rejected renewal costs gas and nothing else — taking the money
125// first and reverting would work too, but only because a panic reverts,
126// and depending on that for a refund is a habit that eventually meets a
127// path where it is not true.
128func assertRoomToRenew(label, domainLabel string) {
129 if RenewalsLeft(label, domainLabel) > 0 {
130 return
131 }
132 panic(ufmt.Sprintf(
133 "nslogic: %s*%s is already paid %d years ahead, which is the limit. Renew again when it has run down.",
134 label, domainLabel, MaxTermYears(domainLabel)))
135}