Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

render.gno

6.69 Kb Β· 206 lines
  1package curated
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"chain/runtime"
  8
  9	"gno.land/p/moul/kit/num/v0"
 10	"gno.land/p/moul/kit/ui/v0"
 11	"gno.land/p/moul/md/v0"
 12	"gno.land/p/moul/realmpath/v0"
 13	"gno.land/p/moul/txlink/v0"
 14	cu "gno.land/p/moul/x/social/curated/v0"
 15)
 16
 17// Render routes two views: the list itself, and one entry.
 18//
 19// A key never contains a slash, so "entry/<key>" is a prefix strip rather than
 20// a path element that would have to be rejoined.
 21func Render(path string) string {
 22	req := realmpath.Parse(path)
 23
 24	switch {
 25	case strings.HasPrefix(req.Path, "entry/"):
 26		return renderEntry(strings.TrimPrefix(req.Path, "entry/"))
 27	case req.Path == "":
 28		return renderIndex()
 29	default:
 30		return md.H1("Not found") + "No such view: " + ui.Inline(req.Path)
 31	}
 32}
 33
 34func renderIndex() string {
 35	out := md.H1("Curated")
 36	out += "A list you pay to be on, and can be paid for getting somebody off. " +
 37		"The deposit does not make an entry good: it makes a bad one expensive " +
 38		"to leave standing, because anybody who disagrees can put the same " +
 39		"amount at risk and take it.\n\n"
 40
 41	open := list.Challenged()
 42
 43	t := ui.NewTable("on the list", "open challenges", "deposit", "window", "locked", "credited")
 44	t.Row(
 45		strconv.Itoa(list.Count()),
 46		strconv.Itoa(len(open)),
 47		num.GNOTf(Deposit),
 48		strconv.FormatInt(ChallengeBlocks, 10)+" blocks",
 49		num.GNOTf(list.Locked()),
 50		num.GNOTf(list.Owed()),
 51	)
 52	out += t.String()
 53
 54	out += md.H2("The list")
 55	out += listing(list.Listed())
 56	out += "\n" + md.Link("βž• Apply for "+num.GNOTf(Deposit), applyURL()) + "\n"
 57
 58	if len(open) > 0 {
 59		out += md.H2("Open challenges")
 60		out += challengeTable(open)
 61	}
 62
 63	out += md.H2("How it works")
 64	out += md.BulletList([]string{
 65		"**Apply** locks " + num.GNOTf(Deposit) + " and lists the entry immediately. " +
 66			"There is no application period in v0.",
 67		"**Challenge** matches that entry's deposit with a bond and opens a vote " +
 68			"for " + strconv.FormatInt(ChallengeBlocks, 10) + " blocks. An owner cannot challenge their own entry.",
 69		"**Vote** is one address, one vote, unweighted. An address is free, so read " +
 70			"a resolution as nobody with a stake objecting, never as a verdict.",
 71		"**Resolve** can be called by anyone once the window closes. A majority to " +
 72			"keep credits the owner the bond; otherwise the challenger takes the bond " +
 73			"and the deposit. A tie keeps the entry, so a challenge that convinced " +
 74			"nobody loses its bond.",
 75		"**Voters are paid nothing** in v0, which is the first thing to fix: the only " +
 76			"two addresses with a reason to vote are the ones with money on the outcome.",
 77		"**Nothing is ever sent to you.** Every payout is a credit, and " +
 78			md.InlineCode("Withdraw()") + " collects it.",
 79	})
 80
 81	return out
 82}
 83
 84func renderEntry(key string) string {
 85	e, ok := list.Get(key)
 86	if !ok {
 87		return md.H1("Curated") + "No entry named " + ui.Inline(key)
 88	}
 89
 90	out := md.H1(ui.Inline(e.Key))
 91	out += md.Blockquote(ui.Inline(e.Description)) + "\n"
 92	// ShortN truncates without escaping and md.Link escapes what it is given:
 93	// ui.Inline here would escape twice and render the backslashes.
 94	out += md.Link(ui.ShortN(e.URL, 60, 0), e.URL) + "\n\n"
 95
 96	t := ui.NewTable("owner", "deposit", "listed at", "state")
 97	t.Row(
 98		ui.Addr(e.Owner),
 99		num.GNOTf(e.Deposit),
100		"block "+strconv.FormatInt(e.At, 10),
101		e.State.String(),
102	)
103	out += t.String()
104
105	c, challenged := list.ChallengeOf(e.Key)
106	switch {
107	case challenged:
108		out += md.H2("Under challenge")
109		out += challengeTable([]*cu.Entry{e})
110		if c.Open(runtime.ChainHeight()) {
111			out += "\n" + ui.Action("πŸ‘ Keep", "Vote", "key", e.Key, "keep", "true") +
112				" Β· " + ui.Action("πŸ‘Ž Remove", "Vote", "key", e.Key, "keep", "false") + "\n"
113		} else {
114			out += "\n" + ui.Action("βš–οΈ Resolve", "Resolve", "key", e.Key) +
115				" Β· the window has closed and anyone can settle it\n"
116		}
117	case e.Live():
118		out += "\n" + md.Link("🚩 Challenge for "+num.GNOTf(e.Deposit), challengeURL(e.Key, e.Deposit)) +
119			" Β· " + ui.Action("πŸ—‘ Unlist", "Unlist", "key", e.Key) + "\n"
120	default:
121		out += "\n" + ui.Empty("Off the list. The key is free to apply for again.")
122		out += md.Link("βž• Apply for "+num.GNOTf(Deposit), applyURL()) + "\n"
123	}
124
125	return out
126}
127
128// listing renders the entries as the list itself: the key links to its page,
129// the description links to the thing it describes.
130func listing(entries []*cu.Entry) string {
131	if len(entries) == 0 {
132		return ui.Empty("Nothing is on the list yet.")
133	}
134	t := ui.NewTable("key", "what", "link", "owner", "since", "state")
135	for _, e := range entries {
136		mark := ""
137		if e.State == cu.StateChallenged {
138			mark = " 🚩"
139		}
140		t.Row(
141			// A key is charset-validated, so md.Link's escaping is the whole
142			// story for it. A description is not: md.Link escapes markdown
143			// but NOT the pipe, which in a cell would open a column, so the
144			// description is never a link title here. Cut first with ShortN,
145			// which does not escape, then escape once with ui.Cell.
146			md.Link(ui.ShortN(e.Key, 32, 0), entryURL(e.Key)),
147			ui.Cell(ui.ShortN(e.Description, 48, 0)),
148			md.Link("β†—", e.URL),
149			ui.Addr(e.Owner),
150			"block "+strconv.FormatInt(e.At, 10),
151			e.State.String()+mark,
152		)
153	}
154	return t.String()
155}
156
157// challengeTable renders the open challenge against each of entries. An entry
158// with none is skipped rather than shown blank.
159func challengeTable(entries []*cu.Entry) string {
160	t := ui.NewTable("entry", "challenger", "bond", "keep", "remove", "voting closes")
161	now := runtime.ChainHeight()
162	for _, e := range entries {
163		c, ok := list.ChallengeOf(e.Key)
164		if !ok {
165			continue
166		}
167		closes := "block " + strconv.FormatInt(c.Deadline, 10)
168		if !c.Open(now) {
169			closes = "closed, awaiting `Resolve`"
170		}
171		t.Row(
172			md.Link(ui.ShortN(e.Key, 32, 0), entryURL(e.Key)),
173			ui.Addr(c.Challenger),
174			num.GNOTf(c.Bond),
175			strconv.FormatInt(c.Keep, 10),
176			strconv.FormatInt(c.Remove, 10),
177			closes,
178		)
179	}
180	return t.OrEmpty("No challenge is open.")
181}
182
183// entryURL is the gnoweb path of one entry's page.
184//
185// The chain domain is the first element of a package path and a gnoweb path is
186// the rest of it, so this is a prefix strip and not a hostname to hardcode.
187func entryURL(key string) string {
188	path := realmPath
189	if i := strings.Index(path, "/"); i >= 0 {
190		path = path[i:]
191	}
192	return path + ":entry/" + key
193}
194
195// applyURL is the Apply form with the deposit already attached, so a reader
196// never has to work out what -send should say.
197func applyURL() string {
198	return txlink.NewLink("Apply").SetSend(send(Deposit)).URL()
199}
200
201// challengeURL is the same for Challenge, carrying that entry's own bond.
202func challengeURL(key string, bond int64) string {
203	return txlink.NewLink("Challenge").AddArgs("key", key).SetSend(send(bond)).URL()
204}
205
206func send(amount int64) string { return strconv.FormatInt(amount, 10) + Denom }