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

render.gno

6.38 Kb · 187 lines
  1package faucet
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/nt/markdown/sanitize/v0"
  8)
  9
 10// Rows is how many requests each table on the board shows. The rest are
 11// counted, not rendered: the per-request page is always reachable by id.
 12const Rows = 20
 13
 14// Render shows the float, the open asks and everything already decided.
 15//
 16// Two paths: "" is the whole board, "req/<id>" is one request.
 17//
 18// Every string on this page that a caller typed goes through sanitize before
 19// it is concatenated. A reason is arbitrary text from an arbitrary account and
 20// Render output is markdown that gnoweb parses, so an unescaped one can plant
 21// a link, an image beacon or gnoweb chrome on a page the realm is signing for.
 22// This realm's path is permanent, so that bug could only ever be fixed at a
 23// new path.
 24func Render(path string) string {
 25	path = strings.TrimPrefix(path, "/")
 26	if id, ok := reqPath(path); ok {
 27		return renderOne(id)
 28	}
 29	return renderBoard()
 30}
 31
 32func renderBoard() string {
 33	var b strings.Builder
 34
 35	b.WriteString("# GNOT faucet\n\n")
 36	b.WriteString(intro)
 37	b.WriteString("\n\n")
 38
 39	b.WriteString("| | |\n| --- | ---: |\n")
 40	b.WriteString("| Float available | **" + gnot(Balance()) + " GNOT** |\n")
 41	b.WriteString("| Paid out | " + gnot(totalSent) + " GNOT over " +
 42		strconv.FormatInt(sentCount, 10) + " request(s) |\n")
 43	b.WriteString("| Open requests | " + strconv.FormatInt(pendingN, 10) + " |\n")
 44	b.WriteString("| Refused | " + strconv.FormatInt(deniedCount, 10) + " |\n")
 45	b.WriteString("| Cap per request | " + gnot(maxPerRequest) + " GNOT |\n")
 46	b.WriteString("| Float address | `" + Address().String() + "` |\n\n")
 47
 48	b.WriteString("## Open\n\n")
 49	b.WriteString(table(true, pendingN))
 50	b.WriteString("\n## Decided\n\n")
 51	b.WriteString(table(false, sentCount+deniedCount))
 52
 53	b.WriteString("\n## Approvers\n\n")
 54	approvers.Iterate("", "", func(k string, _ any) bool {
 55		b.WriteString("- `" + k + "`\n")
 56		return false
 57	})
 58
 59	b.WriteString("\n" + howto + "\n")
 60	return b.String()
 61}
 62
 63// table lists at most Rows requests: the open ones oldest first, because a
 64// queue is worked from the front, and the decided ones newest first, because
 65// nobody scrolls to the bottom of a ledger.
 66//
 67// It takes the total from the realm's counters rather than counting rows, so
 68// it can STOP iterating once the page is full. A Render that walks every
 69// request ever filed is a page that grows without bound and a gas cost that
 70// grows with it, and this realm's path is permanent, so that would only ever
 71// be fixable at a new path.
 72func table(open bool, total int64) string {
 73	if total == 0 {
 74		return "_Nothing here yet._\n"
 75	}
 76	var rows strings.Builder
 77	shown := int64(0)
 78	visit := func(_ string, v any) bool {
 79		r := v.(*request)
 80		if (r.Status == StatusPending) != open {
 81			return false
 82		}
 83		rows.WriteString("| [" + strconv.FormatInt(r.ID, 10) + "](" + Link + ":req/" +
 84			strconv.FormatInt(r.ID, 10) + ") | `" + r.To.String() + "` | " +
 85			gnot(r.Amount) + " | " + sanitize.TableCell(excerpt(r.Reason, 60)) + " | " +
 86			r.Status + " |\n")
 87		shown++
 88		return shown >= Rows // a true return stops the walk
 89	}
 90	if open {
 91		requests.Iterate("", "", visit)
 92	} else {
 93		requests.ReverseIterate("", "", visit)
 94	}
 95
 96	out := "| # | To | GNOT | Why | State |\n| ---: | --- | ---: | --- | --- |\n" + rows.String()
 97	if rest := total - shown; rest > 0 {
 98		out += "\n_" + strconv.FormatInt(rest, 10) + " more not shown._\n"
 99	}
100	return out
101}
102
103func renderOne(id int64) string {
104	r := find(id)
105	if r == nil {
106		return "# Request " + strconv.FormatInt(id, 10) + "\n\n_No such request._\n"
107	}
108	var b strings.Builder
109	b.WriteString("# Request " + strconv.FormatInt(r.ID, 10) + "\n\n")
110	b.WriteString("| | |\n| --- | --- |\n")
111	b.WriteString("| To | `" + r.To.String() + "` |\n")
112	b.WriteString("| Amount | " + gnot(r.Amount) + " GNOT |\n")
113	b.WriteString("| Filed by | `" + r.By.String() + "` |\n")
114	b.WriteString("| Filed at height | " + strconv.FormatInt(r.Asked, 10) + " |\n")
115	b.WriteString("| State | " + r.Status + " |\n")
116	if r.Status != StatusPending {
117		b.WriteString("| Decided by | `" + r.Judge.String() + "` |\n")
118		b.WriteString("| Decided at height | " + strconv.FormatInt(r.Decided, 10) + " |\n")
119	}
120	b.WriteString("\n**Why:** " + sanitize.InlineText(r.Reason) + "\n")
121	if r.Note != "" {
122		b.WriteString("\n**Refused because:** " + sanitize.InlineText(r.Note) + "\n")
123	}
124	b.WriteString("\n[Back to the faucet](" + Link + ")\n")
125	return b.String()
126}
127
128// reqPath parses "req/<id>".
129func reqPath(path string) (int64, bool) {
130	rest, ok := strings.CutPrefix(path, "req/")
131	if !ok {
132		return 0, false
133	}
134	id, err := strconv.ParseInt(rest, 10, 64)
135	if err != nil || id <= 0 {
136		return 0, false
137	}
138	return id, true
139}
140
141// excerpt cuts to width RUNES, never bytes: slicing a multi-byte character in
142// half puts invalid UTF-8 on the page, and one emoji in a reason is enough.
143// It cuts before escaping, because escaping inserts backslashes and cutting an
144// already-escaped string can strand a lone one that escapes the chrome after
145// it.
146func excerpt(s string, width int) string {
147	r := []rune(s)
148	if len(r) <= width+1 {
149		return s
150	}
151	return string(r[:width]) + "…"
152}
153
154// gnot renders ugnot as GNOT with the trailing zeros of the fraction trimmed,
155// so 100000000 reads as 100 and 1500000 as 1.5.
156func gnot(ugnot int64) string {
157	neg := ""
158	if ugnot < 0 {
159		neg, ugnot = "-", -ugnot
160	}
161	whole := strconv.FormatInt(ugnot/1_000_000, 10)
162	frac := strconv.FormatInt(ugnot%1_000_000, 10)
163	if frac == "0" {
164		return neg + whole
165	}
166	for len(frac) < 6 {
167		frac = "0" + frac
168	}
169	return neg + whole + "." + strings.TrimRight(frac, "0")
170}
171
172const (
173	intro = "Somebody you trust has no GNOT and cannot use the public faucet. File a\n" +
174		"request on their behalf, an approver releases it, and both halves stay on\n" +
175		"this page with a reason attached. The faucet only ever spends what has been\n" +
176		"sent to its own address."
177
178	howto = "## How\n\n" +
179		"`Request(to, amount, reason)` is open to anyone and moves no money.\n" +
180		"`Approve(id, to, amount)` pays it, and only an approver can call it.\n\n" +
181		"Send both in **one transaction**: a tm2 transaction stops at the first\n" +
182		"message that fails and writes none of their state, so the ask and its\n" +
183		"answer land together or not at all. Read `NextID` to address the approval,\n" +
184		"and pass the recipient and the amount into it as well: if another request\n" +
185		"takes that id first, the mismatch fails the transaction instead of paying\n" +
186		"the wrong account."
187)