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

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