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.

render.gno

11.46 Kb · 363 lines
  1package vesting
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/nt/ufmt/v0"
  8
  9	"gno.land/p/nym-moultest123/kit/ui/v0"
 10	"gno.land/p/nym-moultest123/realmpath/v0"
 11	"gno.land/p/nym-moultest123/vesting/v0"
 12)
 13
 14// Render serves three shapes:
 15//
 16//	/r/nym-moultest123/vesting/v0                     the seeded schedules
 17//	/r/nym-moultest123/vesting/v0:g1...               one address, its declared schedule
 18//	/r/nym-moultest123/vesting/v0:g1...?o=&s=&e=&d=   one address, a schedule you supply
 19//
 20// The query form stores nothing. It exists so an address that has not declared
 21// anything can still get a real answer: paste the schedule out of
 22// `gnokey query auth/accounts/<addr>` and the balance is still read from chain.
 23func Render(path string) string {
 24	req := realmpath.Parse(path)
 25	target := strings.TrimSpace(req.PathPart(0))
 26
 27	if target == "" {
 28		curPage = 1
 29		if p, err := strconv.Atoi(req.Query.Get("page")); err == nil && p > 1 {
 30			curPage = p
 31		}
 32		return renderIndex()
 33	}
 34	if !isAddr(target) {
 35		return header() + ui.Empty("`"+ui.Inline(target)+"` is not a gno.land address.") +
 36			"\n" + lookupForm()
 37	}
 38	addr := address(target)
 39
 40	// An ad-hoc schedule in the query beats a declared one, and is labelled as
 41	// coming from the caller.
 42	if raw := req.Query.Get("o"); raw != "" {
 43		sch, err := scheduleFromQuery(req)
 44		if err != "" {
 45			return header() + ui.Empty(err) + "\n" + lookupForm()
 46		}
 47		return header() + renderAccount(addr, sch, sourceQuery, "") + "\n" + lookupForm()
 48	}
 49
 50	if e, ok := lookup(addr); ok {
 51		src := sourceDeclared
 52		if e.seeded {
 53			src = sourceSeeded
 54		}
 55		return header() + renderAccount(addr, e.schedule, src, e.label) + "\n" + lookupForm()
 56	}
 57	return header() + renderUnknown(addr) + "\n" + lookupForm()
 58}
 59
 60// Where a schedule came from. This is the distinction the whole page turns on,
 61// so it is a value and not a boolean buried in a branch.
 62type source int
 63
 64const (
 65	sourceSeeded   source = iota // a source constant in this realm
 66	sourceDeclared               // written by the address itself
 67	sourceQuery                  // typed into the URL by whoever is looking
 68	sourceNone                   // there is none
 69)
 70
 71func (s source) label() string {
 72	switch s {
 73	case sourceSeeded:
 74		return "seeded in this realm's source"
 75	case sourceDeclared:
 76		return "declared by the address itself"
 77	case sourceQuery:
 78		return "supplied in the URL by you"
 79	}
 80	return "unknown"
 81}
 82
 83func header() string {
 84	return "# Vesting\n\n" +
 85		"How much of an account's balance can actually move right now.\n\n"
 86}
 87
 88// pageSize bounds the declared table. vm/qrender runs on a gas meter and a
 89// large return value fails with OutOfGasError{Descriptor:"CPUCycles"} rather
 90// than answering slowly, so an unbounded index is a page that eventually stops
 91// rendering for everyone. Anyone may Declare, so "eventually" is somebody
 92// else's choice unless it is capped here.
 93const pageSize = 25
 94
 95func renderIndex() string {
 96	var b strings.Builder
 97	b.WriteString(header())
 98
 99	// Seeded rows always render in full: there are as many as this realm's
100	// source says and no caller can add one.
101	t := ui.NewTable("account", "balance", "spendable", "locked", "vested")
102	seedIndex.Iterate("", "", func(key string, v any) bool {
103		t.Row(rowFor(address(key), v.(*entry))...)
104		return false
105	})
106	b.WriteString(t.String())
107
108	if n := declared.Size(); n > 0 {
109		b.WriteString("\n## Declared\n\n")
110		b.WriteString(ufmt.Sprintf("%d address(es) have put their own schedule on file.\n\n", n))
111
112		d := ui.NewTable("account", "balance", "spendable", "locked", "vested")
113		declared.IterateByOffset(pageOffset(curPage)*pageSize, pageSize, func(key string, v any) bool {
114			d.Row(rowFor(address(key), v.(*entry))...)
115			return false
116		})
117		b.WriteString(d.String())
118		b.WriteString("\n")
119		b.WriteString(pager(n))
120	}
121
122	b.WriteString("\n")
123	b.WriteString(lookupForm())
124	b.WriteString("\n## What is real here, and what is not\n\n")
125	b.WriteString("| figure | where it comes from |\n|---|---|\n")
126	b.WriteString("| balance | read from the chain every render, `banker.GetCoins` |\n")
127	b.WriteString("| the clock | read from the chain every render, the block time |\n")
128	b.WriteString("| the schedule | **supplied**, because no realm can read one |\n\n")
129	b.WriteString("A realm's whole view of an account is its TOTAL balance, locked coins ")
130	b.WriteString("included. `std.VestingSchedule` lives in the auth store and no native ")
131	b.WriteString("reaches it, so the curve has to be told to this realm rather than found.\n\n")
132	b.WriteString("That is safe to cache because a schedule can never change: it is set ")
133	b.WriteString("once, at genesis, and no message type creates or modifies one. A ")
134	b.WriteString("schedule declared correctly today is correct forever.\n")
135	return b.String()
136}
137
138// curPage is the page renderIndex is currently building, set by Render before
139// it delegates. A parameter would be cleaner; Render's signature is fixed by
140// the VM, and threading it through every helper for one value is worse.
141var curPage = 1
142
143func pageOffset(page int) int {
144	if page < 1 {
145		return 0
146	}
147	return page - 1
148}
149
150// rowFor builds one table row. Shared so the seeded and declared tables cannot
151// drift apart in what they show.
152func rowFor(addr address, e *entry) []string {
153	bal := balanceOf(addr)
154	n := now()
155	name := ui.AddrText(addr)
156	if e.label != "" {
157		name = ui.Inline(e.label) + " " + ui.Addr(addr)
158	}
159	return []string{
160		name,
161		gnot(bal),
162		gnot(e.schedule.Spendable(bal, n)),
163		gnot(lockedCapped(e.schedule, bal, n)),
164		permille(e.schedule.PermilleVested(n)),
165	}
166}
167
168// pager renders the page links, and nothing at all when everything fits.
169func pager(total int) string {
170	pages := (total + pageSize - 1) / pageSize
171	if pages <= 1 {
172		return ""
173	}
174	var b strings.Builder
175	b.WriteString(ufmt.Sprintf("Page %d of %d.", curPage, pages))
176	if curPage > 1 {
177		b.WriteString(ufmt.Sprintf(" [previous](%s?page=%d)", Link, curPage-1))
178	}
179	if curPage < pages {
180		b.WriteString(ufmt.Sprintf(" [next](%s?page=%d)", Link, curPage+1))
181	}
182	b.WriteString("\n")
183	return b.String()
184}
185
186func renderAccount(addr address, s vesting.Schedule, src source, label string) string {
187	var b strings.Builder
188	n := now()
189	bal := balanceOf(addr)
190
191	b.WriteString("## ")
192	if label != "" {
193		b.WriteString(ui.Inline(label))
194		b.WriteString(" ")
195	}
196	b.WriteString(ui.AddrText(addr))
197	b.WriteString("\n\n")
198
199	if s.IsZero() {
200		b.WriteString("No vesting schedule, so the whole balance moves.\n\n")
201		b.WriteString("| | |\n|---|---|\n")
202		b.WriteString("| balance | **" + gnot(bal) + "** |\n")
203		b.WriteString("| spendable | **" + gnot(bal) + "** |\n\n")
204		b.WriteString(sourceNote(src))
205		return b.String()
206	}
207
208	locked := lockedCapped(s, bal, n)
209	t := ui.NewTable("", "")
210	t.Row("balance", "**"+gnot(bal)+"**")
211	t.Row("spendable now", "**"+gnot(s.Spendable(bal, n))+"**")
212	t.Row("locked now", gnot(locked))
213	t.Row("granted", gnot(s.Original))
214	t.Row("vested", gnot(s.Vested(n))+" ("+permille(s.PermilleVested(n))+")")
215	t.Row("still to vest", gnot(s.Locked(n)))
216	t.Row("curve", s.Type.String())
217	t.Row("term", tstamp(s.Start)+" to "+tstamp(s.End))
218	t.Row("finishes in", duration(s.RemainingSeconds(n)))
219	b.WriteString(t.String())
220	b.WriteString("\n")
221
222	if s.Locked(n) > bal {
223		b.WriteString("\n> This account holds less than its schedule still locks, so ")
224		b.WriteString("nothing moves at all. That happens when coins were spent while ")
225		b.WriteString("they were free and the grant has since outrun the balance.\n")
226	}
227
228	b.WriteString("\n")
229	b.WriteString(sourceNote(src))
230	return b.String()
231}
232
233func renderUnknown(addr address) string {
234	var b strings.Builder
235	bal := balanceOf(addr)
236	b.WriteString("## ")
237	b.WriteString(ui.AddrText(addr))
238	b.WriteString("\n\nNo schedule on file for this address.\n\n")
239	b.WriteString("| | |\n|---|---|\n")
240	b.WriteString("| balance | **" + gnot(bal) + "** |\n")
241	b.WriteString("| spendable | unknown |\n\n")
242	b.WriteString("The balance above is real. How much of it moves is not knowable from ")
243	b.WriteString("inside a realm. Read the schedule off the chain:\n\n")
244	b.WriteString("```\ngnokey query auth/accounts/" + addr.String() +
245		" -remote https://rpc.gno.land:443\n```\n\n")
246	b.WriteString("then either append it to this URL as `?o=<original_vesting>&s=<start_time>")
247	b.WriteString("&e=<end_time>` (add `&d=1` for a `delayed` schedule), or, if this address ")
248	b.WriteString("is yours, put it on file so the link works for everyone:\n\n")
249	b.WriteString(ui.Action("Declare this schedule", "Declare",
250		"original", "", "start", "", "end", "", "delayed", "false"))
251	b.WriteString("\n")
252	return b.String()
253}
254
255func sourceNote(src source) string {
256	s := "Balance and clock are read from the chain. The schedule is " + src.label() + "."
257	if src == sourceQuery {
258		s += " Nothing was stored."
259	}
260	if src == sourceDeclared {
261		s += " Only that address can write or remove it, so it can misdescribe nobody else."
262	}
263	return "> " + s + "\n"
264}
265
266func lookupForm() string {
267	return "## Check another address\n\n" +
268		"Append it to the path: `" + Link + ":g1…`\n\n" +
269		"| | |\n|---|---|\n" +
270		"| put your own schedule on file | " +
271		ui.Action("Declare", "Declare", "original", "", "start", "", "end", "", "delayed", "false") +
272		" |\n| take it off again | " + ui.Action("Forget", "Forget") + " |\n"
273}
274
275// scheduleFromQuery reads a one-off schedule out of the URL. It returns a
276// human-readable reason rather than an error value, because every caller turns
277// it straight into page text.
278func scheduleFromQuery(req *realmpath.Request) (vesting.Schedule, string) {
279	o, err := strconv.ParseInt(req.Query.Get("o"), 10, 64)
280	if err != nil {
281		return vesting.Schedule{}, "`o` (the original vesting amount, in ugnot) is not a number."
282	}
283	s, err := strconv.ParseInt(orZero(req.Query.Get("s")), 10, 64)
284	if err != nil {
285		return vesting.Schedule{}, "`s` (the start time, unix seconds) is not a number."
286	}
287	e, err := strconv.ParseInt(orZero(req.Query.Get("e")), 10, 64)
288	if err != nil {
289		return vesting.Schedule{}, "`e` (the end time, unix seconds) is not a number."
290	}
291	typ := vesting.Continuous
292	if d := req.Query.Get("d"); d == "1" || d == "true" {
293		typ = vesting.Delayed
294	}
295	sch, verr := vesting.New(o, s, e, typ)
296	if verr != nil {
297		return vesting.Schedule{}, "That is not a valid schedule: " + verr.Error() + "."
298	}
299	return sch, ""
300}
301
302func orZero(s string) string {
303	if s == "" {
304		return "0"
305	}
306	return s
307}
308
309// lockedCapped is what the account cannot move, which is never more than it
310// holds. The schedule alone can say otherwise once coins have been spent.
311func lockedCapped(s vesting.Schedule, balance, n int64) int64 {
312	l := s.Locked(n)
313	if l > balance {
314		return balance
315	}
316	if l < 0 {
317		return 0
318	}
319	return l
320}
321
322// tstamp renders a unix second as a plain UTC date, which is all a two-year
323// term needs. gno has no time formatting worth the dependency here.
324func tstamp(sec int64) string {
325	y, m, d := civil(sec / 86400)
326	return ufmtDate(y, m, d)
327}
328
329// civil converts a day count since 1970-01-01 to a calendar date, by Howard
330// Hinnant's civil_from_days. Integer only, no time package, no locale.
331func civil(z int64) (year int64, month int64, day int64) {
332	z += 719468
333	era := z / 146097
334	if z < 0 {
335		era = (z - 146096) / 146097
336	}
337	doe := z - era*146097
338	yoe := (doe - doe/1460 + doe/36524 - doe/146096) / 365
339	y := yoe + era*400
340	doy := doe - (365*yoe + yoe/4 - yoe/100)
341	mp := (5*doy + 2) / 153
342	d := doy - (153*mp+2)/5 + 1
343	m := mp + 3
344	if mp >= 10 {
345		m = mp - 9
346	}
347	if m <= 2 {
348		y++
349	}
350	return y, m, d
351}
352
353func ufmtDate(y, m, d int64) string {
354	return strconv.FormatInt(y, 10) + "-" + pad2(m) + "-" + pad2(d)
355}
356
357func pad2(n int64) string {
358	s := strconv.FormatInt(n, 10)
359	if len(s) < 2 {
360		return "0" + s
361	}
362	return s
363}