home.gno
12.52 Kb · 242 lines
1// Package home is 67's site on gno.land: what the exchange is, the open
2// markets read live from the markets realm, how a market works and what the
3// contract does. It holds no funds and no keys. It is private, so the same
4// deployer can publish a new version at the same address.
5package home
6
7import (
8 "chain/runtime"
9 "chain/runtime/unsafe"
10 "strings"
11 "time"
12
13 "gno.land/r/nym-sixseven067/markets"
14)
15
16// appURL is the exchange interface, where Adena signs purchases and claims.
17const appURL = "https://the67.click"
18
19const tick = "`"
20
21// homePath and marketsPath are gnoweb paths, captured at deployment.
22var (
23 homePath = strings.TrimPrefix(unsafe.CurrentRealm().PkgPath(), "gno.land")
24 marketsPath = strings.TrimSuffix(homePath, "/home") + "/markets"
25)
26
27// Render serves three pages: the home page, ":how" and ":contract".
28func Render(path string) string {
29 switch path {
30 case "":
31 return renderHome()
32 case "how":
33 return renderHow()
34 case "contract":
35 return renderContract()
36 }
37 return "# Page not found\n\nThere is nothing at this address.\n\n[Home](" + homePath + ")\n"
38}
39
40func renderHome() string {
41 var out strings.Builder
42 out.WriteString("# 67\n\n")
43 out.WriteString("Questions about the world, priced in GNOT. A share in the outcome that happens pays 1 GNOT.\n\n")
44 out.WriteString("[Open the exchange](" + appURL + "/#/markets)\n\n")
45
46 out.WriteString("## Markets\n\n")
47 list := markets.ListMarkets()
48 if len(list) == 0 {
49 out.WriteString("No markets are listed yet.\n\n")
50 } else {
51 out.WriteString("| Question | Yes, GNOT | No, GNOT | Status |\n")
52 out.WriteString("| :--- | ---: | ---: | :--- |\n")
53 for _, m := range list {
54 out.WriteString("| [" + escapeText(m.Question) + "](" + marketsPath + ":" + m.ID + ") | ")
55 if m.Finalized {
56 out.WriteString("– | – | " + status(m) + " |\n")
57 } else {
58 out.WriteString(priceText(m.YesPricePpm) + " | " + priceText(1000000-m.YesPricePpm) + " | " + status(m) + " |\n")
59 }
60 }
61 out.WriteString("\n")
62 }
63
64 out.WriteString("## Four calls settle a market\n\n")
65 out.WriteString("<gno-columns>\n")
66 out.WriteString("**CreateMarket**\n\nThe question, its rules and the oracle are fixed in the realm. Nothing edits them later.\n\n<gno-columns-sep>\n\n")
67 out.WriteString("**Buy**\n\nYou pay the exact quoted price. The realm refuses a sale it could not pay out.\n\n<gno-columns-sep>\n\n")
68 out.WriteString("**FinalizeByOracle**\n\nWhen Polymarket's result is final, the market's oracle records it, once.\n\n<gno-columns-sep>\n\n")
69 out.WriteString("**Claim**\n\nEach winning share pays 1 GNOT from the market's collateral, straight to your address.\n")
70 out.WriteString("</gno-columns>\n\n")
71
72 out.WriteString("[How it works](" + homePath + ":how) and [what the contract does](" + homePath + ":contract), in full.\n\n")
73 out.WriteString(footer())
74 return out.String()
75}
76
77func renderHow() string {
78 var out strings.Builder
79 out.WriteString("# A share pays 1 GNOT if it's right\n\n")
80 out.WriteString("Everything else on 67 follows from that sentence: what a price means, why every market holds collateral, and when you get paid.\n\n")
81
82 out.WriteString("## The question\n\n")
83 out.WriteString("Every question is copied word for word from a Polymarket market, rules included. When it is listed, the realm stores the question, the rules, their hash, the Polymarket condition and both outcome tokens. None of it can be edited later.\n\n")
84 out.WriteString("Copying the words is not a link to Polymarket's money: there is no shared order book and no shared liquidity.\n\n")
85
86 out.WriteString("## The price\n\n")
87 out.WriteString("Prices are 67's own fixed quotes. The operator follows Polymarket's midpoint for the Yes token and publishes a new quote when it moves. Each quote is numbered and kept in the market's history.\n\n")
88 out.WriteString("Yes and No always add up to 1 GNOT, so a Yes price of 0.42 is also a 42% quote. It is a price, not a forecast.\n\n")
89 out.WriteString("```\nshares = floor(spend × 1,000,000 ÷ price)\ncost = ceil(shares × price ÷ 1,000,000)\n```\n\n")
90
91 out.WriteString("## The review\n\n")
92 out.WriteString("A purchase carries the price the buyer reviewed and pays the exact cost. If the quote moved in the meantime, the realm refuses and only the network fee is spent.\n\n")
93
94 out.WriteString("## The collateral\n\n")
95 out.WriteString("Each market holds its own collateral: reserve funding first, then the cost of every purchase. The realm refuses any purchase after which the larger side could not be paid in full.\n\n")
96 out.WriteString("Only winning claims take collateral out. There is no withdrawal function, not even for the administrator.\n\n")
97 out.WriteString("```\nmax(Yes shares, No shares) ≤ collateral (all in ugnot)\n```\n\n")
98
99 out.WriteString("## The result\n\n")
100 out.WriteString("Each market names its oracle when it is listed. Only that address can finalize it, once, and only by repeating the market's full source binding with a hash of the evidence. Trading stops in the same transaction.\n\n")
101 out.WriteString("A Polymarket price near 100% is not a result. Until the oracle finalizes, nothing pays.\n\n")
102
103 out.WriteString("## The claim\n\n")
104 out.WriteString("When a market is final, each winning share pays 1 GNOT from that market's collateral, straight to your address. A losing position closes with zero, and a second claim is refused.\n\n")
105
106 out.WriteString("## Where to trade\n\n")
107 out.WriteString("In [the app](" + appURL + "/#/markets), where the Adena wallet signs each purchase after you review it. ")
108 out.WriteString("Or directly with gnokey: ask the realm for the exact cost with " + tick + "Quote(id, outcome, microShares)" + tick + ", then call [Buy](" + marketsPath + "$help&func=Buy) with that amount.\n\n")
109 out.WriteString("There is no selling yet: a position is held until its market is final.\n\n")
110
111 out.WriteString("[Home](" + homePath + ")\n")
112 return out.String()
113}
114
115type call struct {
116 name, who, signature, text string
117 refusals []string
118}
119
120var calls = []call{
121 {"CreateMarket", "Administrator", "CreateMarket(cur, id, question, rules, conditionID, yesTokenID, noTokenID string, closesAt, yesPricePpm int64, oracle address)",
122 "Lists a market and freezes every field used to match the external result: the question, the rules and their hash, the Polymarket condition, both outcome tokens, the closing time and the oracle. There is no function that edits any of them afterwards.",
123 []string{"administrator only", "market already exists", "closing time must be in the future", "invalid fixed price"}},
124 {"Fund", "Anyone", "Fund(cur, id string)",
125 "Adds reserve collateral to one market. The money belongs to that market only and nothing withdraws it, not even the administrator.",
126 []string{"market funding closed"}},
127 {"SetQuote", "Administrator", "SetQuote(cur, id string, yesPricePpm, expectedVersion int64)",
128 "Publishes a new fixed Yes price while trading is open and adds it to the quote history. The expected version stops an operator from overwriting a quote they have not seen.",
129 []string{"trading closed", "quote version changed", "quote unchanged"}},
130 {"Buy", "Any account", "Buy(cur, id, outcome string, microShares, expectedPricePpm int64)",
131 "Buys Yes or No shares at the price the buyer reviewed. The payment must be the exact cost, and the market's collateral must cover the larger side if it wins.",
132 []string{"trading closed", "price does not match review", "send the exact quoted ugnot amount", "insufficient market collateral"}},
133 {"FinalizeByOracle", "The market's oracle", "FinalizeByOracle(cur, id, conditionID, rulesHash, yesTokenID, noTokenID, winner, evidenceHash string)",
134 "Records the final outcome once, with a hash of the evidence. The call must repeat the market's whole source binding, so a result for another question cannot land here. Trading stops in the same transaction.",
135 []string{"oracle only", "outcome already final", "source binding mismatch"}},
136 {"Claim", "A position holder", "Claim(cur, id string) int64",
137 "Pays the caller's winning shares, 1 GNOT each, from that market's collateral. A losing position closes with zero; a second claim is refused.",
138 []string{"outcome is not final", "no position", "position already claimed"}},
139 {"ProposeAdministrator", "Administrator", "ProposeAdministrator(cur, next address)",
140 "Starts a two-step handover. Nothing changes until the proposed address accepts from its own account, so a mistyped address cannot take over. Markets keep the oracle they were created with.",
141 []string{"administrator only", "invalid administrator"}},
142 {"AcceptAdministrator", "The proposed address", "AcceptAdministrator(cur)",
143 "Completes the handover.",
144 []string{"pending administrator only"}},
145}
146
147func renderContract() string {
148 var out strings.Builder
149 out.WriteString("# One realm, every rule\n\n")
150 out.WriteString("67 is one Gno package, [" + tick + "gno.land" + marketsPath + tick + "](" + marketsPath + "). It holds each market's collateral, the published quotes, every position and the payouts. The app only reads it and asks Adena to sign calls to it.\n\n")
151 out.WriteString("Administrator " + string(markets.Administrator()) + "\n\n")
152
153 out.WriteString("## Units\n\n")
154 out.WriteString("All amounts are whole " + tick + "ugnot" + tick + ": one GNOT is 1,000,000 ugnot. A share is 1,000,000 micro-shares, and a winning micro-share pays 1 ugnot, so a winning share pays exactly 1 GNOT.\n\n")
155 out.WriteString("Prices are parts per million of that payout. A Yes price of " + tick + "420000" + tick + " means 0.42 GNOT per share; the No price is always the rest, " + tick + "580000" + tick + ".\n\n")
156
157 out.WriteString("## What each call does\n\n")
158 out.WriteString("Each function checks its own conditions and stops the whole transaction when one fails. The refusals are its exact messages.\n\n")
159 for _, c := range calls {
160 out.WriteString("### [" + c.name + "](" + marketsPath + "$help&func=" + c.name + ")\n\n")
161 out.WriteString(c.who + ". " + c.text + "\n\n")
162 out.WriteString(tick + c.signature + tick + "\n\n")
163 out.WriteString("Refuses with " + tick + strings.Join(c.refusals, tick+", "+tick) + tick + ".\n\n")
164 }
165
166 out.WriteString("## Read without signing\n\n")
167 for _, fn := range []string{"MarketsJSON()", "MarketJSON(id)", "PositionsJSON(owner)", "QuoteHistory(id)", "Quote(id, outcome, microShares)", "Render(path)"} {
168 out.WriteString("- " + tick + fn + tick + "\n")
169 }
170 out.WriteString("\nEvents for indexers: " + tick + "MarketCreated" + tick + ", " + tick + "QuoteUpdated" + tick + ", " + tick + "Funded" + tick + ", " + tick + "Bought" + tick + ", " + tick + "Finalized" + tick + ", " + tick + "Claimed" + tick + ", " + tick + "AdministratorProposed" + tick + ", " + tick + "AdministratorAccepted" + tick + ".\n\n")
171
172 out.WriteString("## What it cannot do\n\n")
173 out.WriteString("- Edit a question, its rules, its source or its oracle\n")
174 out.WriteString("- Withdraw collateral, for anyone\n")
175 out.WriteString("- Sell or cancel a position\n")
176 out.WriteString("- Learn a result by itself: only the pinned oracle can finalize\n\n")
177
178 out.WriteString("[Source of the realm](" + marketsPath + "$source)\n\n")
179 out.WriteString("[Home](" + homePath + ")\n")
180 return out.String()
181}
182
183func footer() string {
184 var out strings.Builder
185 if runtime.ChainID() != "gnoland-1" {
186 out.WriteString("> [!NOTE] Testnet\n")
187 out.WriteString("> This is " + runtime.ChainID() + ", a test network: test GNOT has no value.\n\n")
188 }
189 out.WriteString("Questions, rules and results come from Polymarket. 67 is independent and not affiliated with Polymarket.\n")
190 return out.String()
191}
192
193func status(m markets.Market) string {
194 if m.Finalized {
195 return m.Winner + " won"
196 }
197 if time.Now().Unix() >= m.ClosesAt {
198 return "Awaiting the result"
199 }
200 return "Open until " + time.Unix(m.ClosesAt, 0).UTC().Format("2 January 2006")
201}
202
203// priceText formats millionths like the app: at least two decimals, no
204// trailing zeros beyond them (420000 is "0.42", 425000 is "0.425").
205func priceText(value int64) string {
206 whole := value / 1000000
207 fraction := value % 1000000
208 text := ""
209 for i := 0; i < 6; i++ {
210 text = string(rune('0'+fraction%10)) + text
211 fraction /= 10
212 }
213 for len(text) > 2 && strings.HasSuffix(text, "0") {
214 text = text[:len(text)-1]
215 }
216 return itoa(whole) + "." + text
217}
218
219func itoa(n int64) string {
220 if n == 0 {
221 return "0"
222 }
223 text := ""
224 for n > 0 {
225 text = string(rune('0'+n%10)) + text
226 n /= 10
227 }
228 return text
229}
230
231// escapeText keeps question text literal in Markdown, tables included.
232func escapeText(s string) string {
233 var out strings.Builder
234 for _, c := range s {
235 switch c {
236 case '\\', '`', '*', '_', '[', ']', '<', '>', '|', '#', '~', '!':
237 out.WriteByte('\\')
238 }
239 out.WriteRune(c)
240 }
241 return out.String()
242}