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 }