render.gno
8.80 Kb · 227 lines
1package markets
2
3import (
4 "chain/runtime"
5 "chain/runtime/unsafe"
6 "strconv"
7 "strings"
8 "time"
9)
10
11// appURL is the exchange interface; the realm page links to it for trading.
12const appURL = "https://the67.click"
13
14// pagePath is this realm's gnoweb path, captured at deployment so links between
15// its pages work on any chain and namespace.
16var pagePath = strings.TrimPrefix(unsafe.CurrentRealm().PkgPath(), "gno.land")
17
18const historyRows = 10
19
20// Render draws the realm's public page on gnoweb: the market list at the root
21// and one page per market at ":<id>". It is informational; the app reads JSON.
22func Render(path string) string {
23 if path == "" {
24 return renderIndex()
25 }
26 r := records[path]
27 if r == nil {
28 return "# Market not found\n\nThere is no market `" + escapeCode(path) + "` in this realm.\n\n[All markets](" + pagePath + ")\n"
29 }
30 return renderMarket(r)
31}
32
33func renderIndex() string {
34 var out strings.Builder
35 out.WriteString("# 67\n\n")
36 out.WriteString("Questions about the world, priced in GNOT. A share in the outcome that happens pays 1 GNOT.\n\n")
37 out.WriteString("[Trade on 67 with the Adena wallet](" + appURL + "/#/markets)\n\n")
38 if realmPaused {
39 out.WriteString("> [!WARNING] Paused\n> New purchases are paused in every market. Claims are never paused.\n\n")
40 }
41 if len(marketIDs) == 0 {
42 out.WriteString("No markets are listed yet.\n\n")
43 } else {
44 out.WriteString("| Question | Yes, GNOT | No, GNOT | Status |\n")
45 out.WriteString("| :--- | ---: | ---: | :--- |\n")
46 for _, id := range marketIDs {
47 m := records[id].market
48 yes, no := priceText(m.YesPricePpm), priceText(m.NoPricePpm)
49 if m.State == StateFinal || m.State == StateProposed {
50 yes, no = "–", "–"
51 }
52 out.WriteString("| [" + escapeText(m.Question) + "](" + pagePath + ":" + id + ") | " + yes + " | " + no + " | " + status(m) + " |\n")
53 }
54 out.WriteString("\n")
55 }
56 if runtime.ChainID() != "gnoland-1" {
57 out.WriteString("> [!NOTE] Testnet\n")
58 out.WriteString("> This is " + runtime.ChainID() + ", a test network: test GNOT has no value.\n\n")
59 }
60 out.WriteString("Questions, rules and results come from Polymarket. 67 is independent and not affiliated with Polymarket.\n\n")
61 out.WriteString("## How a market settles\n\n")
62 out.WriteString("The question, its rules, the fee rate and the oracle are fixed when a market is listed. ")
63 out.WriteString("Every purchase is fully collateralized: the realm refuses a sale it could not pay out. ")
64 out.WriteString("When Polymarket's result is final, the market's oracle proposes it here and trading stops. ")
65 out.WriteString("After 24 hours anyone can make it final, and holders of the winning side claim 1 GNOT per share. ")
66 out.WriteString("A 50-50 result is void: every holder takes back what they paid, the fee included. ")
67 out.WriteString("If nothing is final 60 days after trading ends, anyone can void the market.\n\n")
68 out.WriteString("## The fee\n\n")
69 out.WriteString("Each purchase pays a service fee of θ × shares × price × (1 − price) GNOT, rounded up to the ugnot. ")
70 out.WriteString("θ is fixed per market and can never exceed 0.07. The fee is held until the market is final and returned on void.\n\n")
71 out.WriteString("[Functions](" + pagePath + "$help) and [source](" + pagePath + "$source) of this realm.\n")
72 return out.String()
73}
74
75func renderMarket(r *marketRecord) string {
76 m := r.market
77 var out strings.Builder
78 out.WriteString("# " + escapeText(m.Question) + "\n\n")
79
80 if m.State == StateFinal {
81 out.WriteString("<gno-columns>\n")
82 out.WriteString("**Result**\n\n" + outcomeName(m, m.Outcome) + "\n\n<gno-columns-sep>\n\n")
83 out.WriteString("**Final on**\n\n" + dayText(m.FinalizedAt) + "\n")
84 out.WriteString("</gno-columns>\n\n")
85 } else {
86 out.WriteString("<gno-columns>\n")
87 out.WriteString("**" + m.YesLabel + "**\n\n" + priceText(m.YesPricePpm) + " GNOT\n\n<gno-columns-sep>\n\n")
88 out.WriteString("**" + m.NoLabel + "**\n\n" + priceText(m.NoPricePpm) + " GNOT\n\n<gno-columns-sep>\n\n")
89 out.WriteString("**Status**\n\n" + status(m) + "\n")
90 out.WriteString("</gno-columns>\n\n")
91 }
92
93 switch m.State {
94 case StateFinal:
95 out.WriteString("## Result\n\n")
96 if m.Outcome == "Void" {
97 out.WriteString("The market is void. Every holder takes back what they paid, the fee included, in [the app](" + appURL + "/#/portfolio).\n\n")
98 } else {
99 out.WriteString(outcomeName(m, m.Outcome) + " won. Holders of " + outcomeName(m, m.Outcome) + " claim 1 GNOT per share in [the app](" + appURL + "/#/portfolio); " + priceText(m.PaidUgnot) + " GNOT has been paid so far.\n\n")
100 }
101 if m.EvidenceHash != "" {
102 out.WriteString("Evidence hash `" + m.EvidenceHash + "`\n\n")
103 }
104 case StateProposed:
105 out.WriteString("## Proposed result\n\n")
106 out.WriteString("The oracle proposed " + outcomeName(m, m.Outcome) + " on " + timeText(m.ProposedAt) + " UTC. ")
107 out.WriteString("Anyone can make it final from " + timeText(m.ProposedAt+DisputeWindow) + " UTC; until then the guardian can cancel it. Trading has stopped.\n\n")
108 out.WriteString("Evidence hash `" + m.EvidenceHash + "`\n\n")
109 default:
110 out.WriteString("[Buy " + m.YesLabel + " or " + m.NoLabel + " on 67](" + appURL + "/#/market/" + m.ID + ")\n\n")
111 }
112
113 out.WriteString("## Rules\n\n")
114 for _, line := range strings.Split(m.Rules, "\n") {
115 line = strings.TrimSpace(line)
116 if line != "" {
117 out.WriteString(escapeText(line) + "\n\n")
118 }
119 }
120
121 out.WriteString("## Terms fixed at listing\n\n")
122 out.WriteString("- Polymarket condition `" + m.ConditionID + "`\n")
123 out.WriteString("- Rules hash `" + m.RulesHash + "`, the SHA-256 of the question and the rules above\n")
124 out.WriteString("- Oracle " + string(m.Oracle) + "\n")
125 out.WriteString("- Fee rate θ = " + priceText(m.FeePpm) + "\n")
126 out.WriteString("- Trading ends " + timeText(m.TradingEndsAt) + " UTC; void if nothing is final by " + dayText(m.AbandonAt) + "\n")
127 out.WriteString("- Surplus and fees go to " + string(m.Treasury) + ", only after the market is final\n\n")
128
129 out.WriteString("## Record\n\n")
130 out.WriteString("- " + countText(m.Trades, "trade", "trades") + ", " + priceText(m.VolumeUgnot) + " GNOT bought, " + priceText(m.FeesCollectedUgnot) + " GNOT in fees\n")
131 out.WriteString("- Collateral " + priceText(m.EscrowUgnot) + " GNOT, of which " + priceText(m.ReserveUgnot) + " GNOT was the reserve\n")
132 out.WriteString("- Listed on " + dayText(m.CreatedAt) + "\n\n")
133
134 out.WriteString("## Quotes\n\n")
135 out.WriteString("| Published, UTC | Yes, GNOT | No, GNOT |\n")
136 out.WriteString("| :--- | ---: | ---: |\n")
137 from := len(r.history) - historyRows
138 if from < 0 {
139 from = 0
140 }
141 for i := len(r.history) - 1; i >= from; i-- {
142 point := r.history[i]
143 out.WriteString("| " + timeText(point.At) + " | " + priceText(point.YesPricePpm) + " | " + priceText(point.NoPricePpm) + " |\n")
144 }
145 out.WriteString("\n[All markets](" + pagePath + ")\n")
146 return out.String()
147}
148
149func status(m Market) string {
150 switch m.State {
151 case StateFinal:
152 if m.Outcome == "Void" {
153 return "Void, refunds open"
154 }
155 return m.Outcome + " won"
156 case StateProposed:
157 return outcomeName(m, m.Outcome) + " proposed, final from " + dayText(m.ProposedAt+DisputeWindow)
158 }
159 if time.Now().Unix() >= m.TradingEndsAt {
160 return "Awaiting the result"
161 }
162 if m.State == StatePaused || realmPaused {
163 return "Paused"
164 }
165 return "Open until " + dayText(m.TradingEndsAt)
166}
167
168// outcomeName is the outcome as the market names it: Yes and No are the first
169// and second token, shown by their names fixed at listing.
170func outcomeName(m Market, outcome string) string {
171 switch outcome {
172 case "Yes":
173 return m.YesLabel
174 case "No":
175 return m.NoLabel
176 }
177 return outcome
178}
179
180// priceText formats millionths like the app: at least two decimals, no
181// trailing zeros beyond them (420000 is "0.42", 425000 is "0.425").
182func priceText(value int64) string {
183 whole := value / scale
184 fraction := strconv.FormatInt(value%scale, 10)
185 for len(fraction) < 6 {
186 fraction = "0" + fraction
187 }
188 text := strconv.FormatInt(whole, 10) + "." + fraction
189 for strings.HasSuffix(text, "0") && len(text)-strings.Index(text, ".") > 3 {
190 text = text[:len(text)-1]
191 }
192 return text
193}
194
195func dayText(unix int64) string { return time.Unix(unix, 0).UTC().Format("2 January 2006") }
196func timeText(unix int64) string {
197 return time.Unix(unix, 0).UTC().Format("2 January 2006, 15:04")
198}
199
200func countText(n int64, one, many string) string {
201 if n == 1 {
202 return "1 " + one
203 }
204 return strconv.FormatInt(n, 10) + " " + many
205}
206
207// escapeText keeps question and rules text literal in Markdown, tables included.
208func escapeText(s string) string {
209 var out strings.Builder
210 for _, c := range s {
211 switch c {
212 case '\\', '`', '*', '_', '[', ']', '<', '>', '|', '#', '~', '!':
213 out.WriteByte('\\')
214 }
215 out.WriteRune(c)
216 }
217 return out.String()
218}
219
220// escapeCode keeps an untrusted path inside one inline code span.
221func escapeCode(s string) string {
222 s = strings.ReplaceAll(s, "`", "'")
223 if len(s) > 64 {
224 s = s[:64]
225 }
226 return s
227}