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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}