render.gno
6.11 Kb · 169 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://six-seven-67-phi.vercel.app"
13
14// pagePath is this realm's gnoweb path ("/r/<namespace>/markets"), captured at
15// deployment so links between its pages work on any chain and namespace.
16var pagePath = strings.TrimPrefix(unsafe.CurrentRealm().PkgPath(), "gno.land")
17
18// historyRows is how many recent quotes a market page lists.
19const historyRows = 10
20
21// Render draws the realm's public page on gnoweb: the market list at the root
22// and one page per market at ":<id>". It is informational; the app reads JSON.
23func Render(path string) string {
24 if path == "" {
25 return renderIndex()
26 }
27 r := records[path]
28 if r == nil {
29 return "# Market not found\n\nThere is no market `" + escapeCode(path) + "` in this realm.\n\n[All markets](" + pagePath + ")\n"
30 }
31 return renderMarket(r)
32}
33
34func renderIndex() string {
35 var out strings.Builder
36 out.WriteString("# 67\n\n")
37 out.WriteString("Questions about the world, priced in GNOT. A share in the outcome that happens pays 1 GNOT.\n\n")
38 out.WriteString("[Trade on 67 with the Adena wallet](" + appURL + "/#/markets)\n\n")
39 if len(marketIDs) == 0 {
40 out.WriteString("No markets are listed yet.\n\n")
41 } else {
42 out.WriteString("| Question | Yes, GNOT | No, GNOT | Status |\n")
43 out.WriteString("| :--- | ---: | ---: | :--- |\n")
44 for _, id := range marketIDs {
45 m := records[id].market
46 out.WriteString("| [" + escapeText(m.Question) + "](" + pagePath + ":" + id + ") | ")
47 out.WriteString(priceText(m.YesPricePpm) + " | " + priceText(scale-m.YesPricePpm) + " | " + status(m) + " |\n")
48 }
49 out.WriteString("\n")
50 }
51 if runtime.ChainID() != "gnoland-1" {
52 out.WriteString("> [!NOTE] Testnet\n")
53 out.WriteString("> This is " + runtime.ChainID() + ", a test network: test GNOT has no value.\n\n")
54 }
55 out.WriteString("Questions, rules and results come from Polymarket. 67 is independent and not affiliated with Polymarket.\n\n")
56 out.WriteString("## How a market settles\n\n")
57 out.WriteString("The question and its rules are fixed in this realm when a market is listed. ")
58 out.WriteString("Every purchase is fully collateralized: the realm refuses a sale it could not pay out, and nobody can withdraw the collateral. ")
59 out.WriteString("When Polymarket's result is final, the market's oracle records it here, and holders of the winning side claim 1 GNOT per share.\n\n")
60 out.WriteString("[Functions](" + pagePath + "$help) and [source](" + pagePath + "$source) of this realm.\n")
61 return out.String()
62}
63
64func renderMarket(r *marketRecord) string {
65 m := r.market
66 var out strings.Builder
67 out.WriteString("# " + escapeText(m.Question) + "\n\n")
68
69 out.WriteString("<gno-columns>\n")
70 out.WriteString("**Yes**\n\n" + priceText(m.YesPricePpm) + " GNOT\n\n<gno-columns-sep>\n\n")
71 out.WriteString("**No**\n\n" + priceText(scale-m.YesPricePpm) + " GNOT\n\n<gno-columns-sep>\n\n")
72 out.WriteString("**Status**\n\n" + status(m) + "\n")
73 out.WriteString("</gno-columns>\n\n")
74
75 if m.Finalized {
76 out.WriteString("## Result\n\n")
77 out.WriteString(m.Winner + " won. The oracle recorded it on " + dayText(m.FinalizedAt) + ". ")
78 out.WriteString("Holders of " + m.Winner + " claim 1 GNOT per share in [the app](" + appURL + "/#/portfolio); " + priceText(m.PaidUgnot) + " GNOT has been paid so far.\n\n")
79 out.WriteString("Evidence hash `" + m.EvidenceHash + "`\n\n")
80 } else {
81 out.WriteString("[Buy Yes or No on 67](" + appURL + "/#/market/" + m.ID + ")\n\n")
82 }
83
84 out.WriteString("## Rules\n\n")
85 for _, line := range strings.Split(m.Rules, "\n") {
86 line = strings.TrimSpace(line)
87 if line != "" {
88 out.WriteString(escapeText(line) + "\n\n")
89 }
90 }
91
92 out.WriteString("## Source\n\n")
93 out.WriteString("- Polymarket condition `" + m.ConditionID + "`\n")
94 out.WriteString("- Rules hash `" + m.RulesHash + "`, the SHA-256 of the question and the rules above\n")
95 out.WriteString("- Oracle, fixed for this market: " + string(m.Oracle) + "\n\n")
96
97 out.WriteString("## Record\n\n")
98 out.WriteString("- " + countText(m.Trades, "trade", "trades") + ", " + priceText(m.VolumeUgnot) + " GNOT bought\n")
99 out.WriteString("- Collateral " + priceText(m.EscrowUgnot) + " GNOT\n")
100 out.WriteString("- Listed on " + dayText(m.CreatedAt) + "\n\n")
101
102 out.WriteString("## Quotes\n\n")
103 out.WriteString("| Published, UTC | Yes, GNOT | No, GNOT |\n")
104 out.WriteString("| :--- | ---: | ---: |\n")
105 from := len(r.history) - historyRows
106 if from < 0 {
107 from = 0
108 }
109 for i := len(r.history) - 1; i >= from; i-- {
110 point := r.history[i]
111 out.WriteString("| " + timeText(point.At) + " | " + priceText(point.YesPricePpm) + " | " + priceText(scale-point.YesPricePpm) + " |\n")
112 }
113 out.WriteString("\n[All markets](" + pagePath + ")\n")
114 return out.String()
115}
116
117func status(m Market) string {
118 if m.Finalized {
119 return m.Winner + " won"
120 }
121 if time.Now().Unix() >= m.ClosesAt {
122 return "Awaiting the result"
123 }
124 return "Open until " + dayText(m.ClosesAt)
125}
126
127// priceText formats millionths like the app: at least two decimals, no
128// trailing zeros beyond them (420000 is "0.42", 425000 is "0.425").
129func priceText(value int64) string {
130 text := ppmPrice(value)
131 for strings.HasSuffix(text, "0") && len(text)-strings.Index(text, ".") > 3 {
132 text = text[:len(text)-1]
133 }
134 return text
135}
136
137func dayText(unix int64) string { return time.Unix(unix, 0).UTC().Format("2 January 2006") }
138func timeText(unix int64) string {
139 return time.Unix(unix, 0).UTC().Format("2 January 2006, 15:04")
140}
141
142func countText(n int64, one, many string) string {
143 if n == 1 {
144 return "1 " + one
145 }
146 return strconv.FormatInt(n, 10) + " " + many
147}
148
149// escapeText keeps question and rules text literal in Markdown, tables included.
150func escapeText(s string) string {
151 var out strings.Builder
152 for _, c := range s {
153 switch c {
154 case '\\', '`', '*', '_', '[', ']', '<', '>', '|', '#', '~', '!':
155 out.WriteByte('\\')
156 }
157 out.WriteRune(c)
158 }
159 return out.String()
160}
161
162// escapeCode keeps an untrusted path inside one inline code span.
163func escapeCode(s string) string {
164 s = strings.ReplaceAll(s, "`", "'")
165 if len(s) > 64 {
166 s = s[:64]
167 }
168 return s
169}