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.

vesting.gno

10.15 Kb · 315 lines
  1// Realm vesting answers one question for a gno.land account: of the coins it
  2// holds, how many can actually move right now.
  3//
  4// # Read this before you trust a number here
  5//
  6// A realm CANNOT read an account's vesting schedule. The VM's whole view of an
  7// account is banker.GetCoins, which returns the TOTAL balance with the locked
  8// part included, and no native exposes std.VestingSchedule. So this realm
  9// reads two things from the chain and is told the third:
 10//
 11//	balance     read from the chain, banker.GetCoins   always true
 12//	now         read from the chain, block time        always true
 13//	schedule    supplied, see below                    only as good as its source
 14//
 15// Every rendered figure says which of the two it rests on. Nothing here is
 16// presented as verified when it is not.
 17//
 18// # Where a schedule comes from
 19//
 20// Either the query string, for a one-off calculation that stores nothing, or
 21// the registry. [Declare] writes a schedule for the CALLER'S OWN address and
 22// no other, which is the whole trust model: an address can only misdescribe
 23// itself, and a wrong entry misleads nobody but its author.
 24//
 25// Anyone can read their real schedule in one command and declare it:
 26//
 27//	gnokey query auth/accounts/g1youraddress -remote https://rpc.gno.land:443
 28//
 29// # Why the schedule can be cached at all
 30//
 31// Because it can never change. std.SetVesting has exactly one caller in the
 32// monorepo, gno.land/pkg/gnoland/app.go, during genesis balance loading, and
 33// no message type creates or modifies a schedule. A schedule declared once is
 34// correct forever, which is what makes a registry honest rather than stale.
 35package vesting
 36
 37import (
 38	"strconv"
 39	"strings"
 40	"time"
 41
 42	"chain"
 43	"chain/banker"
 44	"chain/runtime"
 45	"chain/runtime/unsafe"
 46
 47	"gno.land/p/nt/avl/v0"
 48	"gno.land/p/nt/ufmt/v0"
 49
 50	"gno.land/p/nym-moultest123/vesting/v0"
 51)
 52
 53const (
 54	// Path is this realm's package path.
 55	Path = "gno.land/r/nym-moultest123/vesting/v0"
 56	// Link is Path as a gnoweb route.
 57	Link = "/r/nym-moultest123/vesting/v0"
 58	// Denom is the only coin this realm reports on.
 59	Denom = "ugnot"
 60)
 61
 62// seeded are the schedules this realm ships with, restored by init() after
 63// every redeploy.
 64//
 65// They are source constants and not registry rows on purpose: the realm is
 66// private = true, so a redeploy wipes realm state, and the one schedule the
 67// page exists to show should not need a transaction to come back. Each is
 68// copied from the genesis allocation sheet that built mainnet, which is the
 69// public, pinned input named in misc/deployments/mainnet.gno.land/gen-genesis.sh.
 70//
 71// An address is seeded here only when its LABEL is public too. The addresses
 72// are all on chain, but naming one is a claim about who holds it, and this
 73// realm is public and permanent. gen-genesis.sh names the investors-vesting
 74// multisig outright, so that one ships; the treasuries and the other buckets
 75// are identified nowhere public, so they do not, and the URL form serves
 76// anyone who wants to check them anyway.
 77var seeded = []struct {
 78	addr     address
 79	label    string
 80	original int64
 81	start    int64
 82	end      int64
 83	delayed  bool
 84}{
 85	{
 86		addr:     "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
 87		label:    "moul",
 88		original: 106560000000,
 89		start:    1789225200,
 90		end:      1852383600,
 91	},
 92	{
 93		// "including the investors-vesting multisig
 94		// g1x7tm26g9wj84cmg3cs74uwf3g9lqj4mjp6gax3, 150,000,000 GNOT total,
 95		// 144,000,000 of it vesting continuously" -- gen-genesis.sh, and the
 96		// sheet row it describes:
 97		//   g1x7tm26…=150000000000000ugnot;vesting=144000000000000ugnot,1789225200,1852383600
 98		addr:     "g1x7tm26g9wj84cmg3cs74uwf3g9lqj4mjp6gax3",
 99		label:    "investors-vesting multisig",
100		original: 144000000000000,
101		start:    1789225200,
102		end:      1852383600,
103	},
104}
105
106// declared maps an address to the schedule that address declared for itself.
107// Seeded schedules are NOT in here: they are read from the seeded slice, which
108// is what lets the index render them in full while paging everything else.
109// Anyone may Declare, so this tree is unbounded and the page that shows it
110// cannot be.
111var declared avl.Tree
112
113// seedIndex resolves a seeded address without walking the slice twice.
114var seedIndex avl.Tree
115
116// entry is one row of the registry.
117type entry struct {
118	schedule vesting.Schedule
119	label    string // non-empty only for a seeded row
120	seeded   bool
121	at       int64 // block height the row was written at; 0 for a seeded row
122}
123
124func init() { reset() }
125
126// reset restores the registry to exactly what the source declares. init() is
127// its only caller on chain; a test calls it to render a page that does not
128// depend on what the test before it wrote, since an Example runs after every
129// Test in the package and sees the state they left.
130func reset() {
131	declared = avl.Tree{}
132	seedIndex = avl.Tree{}
133	for _, s := range seeded {
134		typ := vesting.Continuous
135		if s.delayed {
136			typ = vesting.Delayed
137		}
138		sch, err := vesting.New(s.original, s.start, s.end, typ)
139		if err != nil {
140			panic("vesting: seeded schedule for " + s.label + ": " + err.Error())
141		}
142		seedIndex.Set(s.addr.String(), &entry{schedule: sch, label: s.label, seeded: true})
143	}
144}
145
146// Declare records the caller's own vesting schedule, replacing any previous
147// one. It cannot write a row for anybody else, which is what keeps the
148// registry honest without the realm being able to verify a thing.
149//
150// Pass original = 0 to declare that the address has no schedule at all.
151func Declare(cur realm, original, start, end int64, delayed bool) {
152	typ := vesting.Continuous
153	if delayed {
154		typ = vesting.Delayed
155	}
156	sch, err := vesting.New(original, start, end, typ)
157	if err != nil {
158		panic(err.Error())
159	}
160	who := unsafe.PreviousRealm().Address()
161	if e, ok := lookup(who); ok && e.seeded {
162		panic("vesting: " + who.String() + " is seeded in the source; edit the realm instead")
163	}
164	declared.Set(who.String(), &entry{schedule: sch, at: chainHeight()})
165	chain.Emit("declared", "addr", who.String(),
166		"original", strconv.FormatInt(original, 10),
167		"start", strconv.FormatInt(start, 10),
168		"end", strconv.FormatInt(end, 10),
169		"type", typ.String())
170}
171
172// Forget removes the caller's own declaration.
173func Forget(cur realm) {
174	who := unsafe.PreviousRealm().Address()
175	e, ok := lookup(who)
176	if !ok {
177		panic("vesting: nothing declared for " + who.String())
178	}
179	if e.seeded {
180		panic("vesting: " + who.String() + " is seeded in the source; edit the realm instead")
181	}
182	declared.Remove(who.String())
183	chain.Emit("forgot", "addr", who.String())
184}
185
186// Count returns how many schedules addresses have declared for themselves.
187// Seeded schedules are not included; SeededCount reports those.
188func Count() int { return declared.Size() }
189
190// SeededCount returns how many schedules this realm ships in its source.
191func SeededCount() int { return seedIndex.Size() }
192
193// ScheduleOf returns the declared schedule for addr, and whether there is one.
194func ScheduleOf(addr address) (original, start, end int64, delayed, ok bool) {
195	e, found := lookup(addr)
196	if !found {
197		return 0, 0, 0, false, false
198	}
199	s := e.schedule
200	return s.Original, s.Start, s.End, s.Type == vesting.Delayed, true
201}
202
203// SpendableOf returns what addr can move right now, and whether the answer
204// rests on a declared schedule. With ok false the figure is the whole balance,
205// which is correct only if the address really has no schedule.
206func SpendableOf(addr address) (spendable int64, ok bool) {
207	bal := balanceOf(addr)
208	e, found := lookup(addr)
209	if !found {
210		return bal, false
211	}
212	return e.schedule.Spendable(bal, now()), true
213}
214
215// LockedOf returns what addr cannot move right now, and whether the answer
216// rests on a declared schedule.
217func LockedOf(addr address) (locked int64, ok bool) {
218	e, found := lookup(addr)
219	if !found {
220		return 0, false
221	}
222	bal := balanceOf(addr)
223	l := e.schedule.Locked(now())
224	if l > bal {
225		l = bal // cannot lock more than is there
226	}
227	return l, true
228}
229
230// lookup reads a row, seeded first so a seeded address can never be shadowed.
231// avl's Get returns ONE value and nil for a miss, unlike Remove which returns
232// two; that asymmetry is a documented gno-vs-Go trap.
233func lookup(addr address) (*entry, bool) {
234	if v := seedIndex.Get(addr.String()); v != nil {
235		return v.(*entry), true
236	}
237	v := declared.Get(addr.String())
238	if v == nil {
239		return nil, false
240	}
241	return v.(*entry), true
242}
243
244// balanceOf reads the address's TOTAL ugnot, locked included. That is what
245// banker.GetCoins reports and the only balance figure a realm can obtain.
246// A readonly banker takes no realm, so this works from inside Render.
247func balanceOf(addr address) int64 {
248	return banker.NewReadonlyBanker().GetCoins(addr).AmountOf(Denom)
249}
250
251// gnot renders a ugnot amount as GNOT with six decimals and no float.
252func gnot(u int64) string {
253	neg := u < 0
254	if neg {
255		u = -u
256	}
257	whole, frac := u/1_000_000, u%1_000_000
258	s := ufmt.Sprintf("%d.%s", whole, pad6(frac))
259	if neg {
260		s = "-" + s
261	}
262	return s + " GNOT"
263}
264
265// pad6 left-pads to six digits. ufmt has no width flags, so this is by hand.
266func pad6(n int64) string {
267	s := strconv.FormatInt(n, 10)
268	for len(s) < 6 {
269		s = "0" + s
270	}
271	return s
272}
273
274// permille renders tenths of a percent as a percentage with one decimal.
275func permille(p int64) string {
276	return ufmt.Sprintf("%d.%d%%", p/10, p%10)
277}
278
279// duration renders a span of seconds as days and hours, which is the only
280// resolution worth reading on a two-year schedule.
281func duration(sec int64) string {
282	if sec <= 0 {
283		return "complete"
284	}
285	d, h := sec/86400, (sec%86400)/3600
286	var b strings.Builder
287	if d > 0 {
288		b.WriteString(strconv.FormatInt(d, 10))
289		b.WriteString("d ")
290	}
291	b.WriteString(strconv.FormatInt(h, 10))
292	b.WriteString("h")
293	return b.String()
294}
295
296// isAddr is a cheap shape check, so a typo renders as "not an address" rather
297// than as an account with a zero balance.
298func isAddr(s string) bool {
299	if len(s) != 40 || !strings.HasPrefix(s, "g1") {
300		return false
301	}
302	for _, r := range s {
303		if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') {
304			return false
305		}
306	}
307	return true
308}
309
310// now is the chain's own clock, in unix seconds. time.Now() in a realm returns
311// the BLOCK time, not a wall clock, which is exactly the value the ante
312// handler compares a vesting schedule against.
313func now() int64 { return time.Now().Unix() }
314
315func chainHeight() int64 { return runtime.ChainHeight() }