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

render.gno

9.12 Kb · 302 lines
  1package otc
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"time"
  7
  8	"gno.land/p/nt/avl/v0"
  9	"gno.land/r/nt/grc20reg/v0"
 10)
 11
 12// Reads do not refuse coins: the envelope belongs to the whole message, so a
 13// guard here would break any realm that reads an offer inside a paid call of
 14// its own. Only writes are guarded.
 15//
 16// Amounts in JSON are strings: int64 does not fit a JavaScript number.
 17
 18func RealmAddress() address { return realmAddr }
 19
 20// GetOffer returns one offer as JSON, public or private.
 21func GetOffer(id uint64) string { return offerJSON(mustOffer(id)) }
 22
 23// ListOpen returns open public offers, newest first (S9).
 24func ListOpen(offset, limit int) string {
 25	return listJSON(&openPublic, offset, limit)
 26}
 27
 28// ListByMaker returns every offer of one maker, private and closed ones
 29// included, newest first.
 30func ListByMaker(addr string, offset, limit int) string {
 31	v := byMaker.Get(addr)
 32	if v == nil {
 33		return `{"total":0,"offers":[]}`
 34	}
 35	return listJSON(v.(*avl.Tree), offset, limit)
 36}
 37
 38// GetConfig returns the parameters and the whitelist as JSON.
 39func GetConfig() string {
 40	var b strings.Builder
 41	b.WriteString(`{"realm":` + jstr(realmAddr.String()))
 42	b.WriteString(`,"admin":` + jstr(admin.String()))
 43	b.WriteString(`,"fee_recipient":` + jstr(feeRecipient.String()))
 44	b.WriteString(`,"fee_bps":` + num(feeBps))
 45	b.WriteString(`,"fee_cap_bps":` + num(feeCapBps))
 46	b.WriteString(`,"gnomic_key":` + jstr(gnomicKey))
 47	b.WriteString(`,"gnomic_fee_enabled":` + strconv.FormatBool(gnomicFeeOn))
 48	b.WriteString(`,"gnomic_fee_bps":` + num(gnomicFee))
 49	b.WriteString(`,"paused":` + strconv.FormatBool(paused))
 50	b.WriteString(`,"max_page_size":` + strconv.Itoa(maxPageSize))
 51	b.WriteString(`,"tokens":[`)
 52	i := 0
 53	tokens.Iterate("", "", func(key string, v any) bool { // bounded by maxTokens
 54		if i > 0 {
 55			b.WriteString(",")
 56		}
 57		i++
 58		sym, dec := tokenInfo(key)
 59		b.WriteString(`{"key":` + jstr(key) + `,"symbol":` + jstr(sym) +
 60			`,"decimals":` + strconv.Itoa(dec) + `,"enabled":` + strconv.FormatBool(v.(bool)) + `}`)
 61		return false
 62	})
 63	b.WriteString("]}")
 64	return b.String()
 65}
 66
 67// GetStats returns the running totals as JSON (F7).
 68func GetStats() string {
 69	return `{"total_fees_ugnot":` + jstr(num(totalFees)) +
 70		`,"total_volume_ugnot":` + jstr(num(totalVolume)) +
 71		`,"filled":` + num(filledCount) +
 72		`,"offers":` + strconv.FormatUint(lastID, 10) +
 73		`,"open_public":` + strconv.Itoa(openPublic.Size()) + `}`
 74}
 75
 76// ---------------------------------------------------------------------------
 77// Render
 78// ---------------------------------------------------------------------------
 79
 80// Render serves "" (stats and the first page), "page/N" and "offer/ID".
 81func Render(path string) string {
 82	switch {
 83	case path == "":
 84		return renderHome(1)
 85	case strings.HasPrefix(path, "page/"):
 86		n, err := strconv.Atoi(path[len("page/"):])
 87		if err != nil || n < 1 {
 88			return "# Not found\n"
 89		}
 90		return renderHome(n)
 91	case strings.HasPrefix(path, "offer/"):
 92		id, err := strconv.ParseUint(path[len("offer/"):], 10, 64)
 93		if err != nil || !offers.Has(padID(id)) {
 94			return "# Not found\n"
 95		}
 96		return renderOffer(mustOffer(id))
 97	}
 98	return "# Not found\n"
 99}
100
101func renderHome(page int) string {
102	var b strings.Builder
103	b.WriteString("# Gnomic OTC\n\n")
104	b.WriteString("Escrowed GRC20 ⇄ GNOT offers, filled in full and atomically.\n\n")
105	if paused {
106		b.WriteString("**Paused:** no new offers or fills. Makers can still cancel.\n\n")
107	}
108	b.WriteString("| Volume | Fees | Filled | Open (public) | Fee |\n|---|---|---|---|---|\n")
109	b.WriteString("| " + units(totalVolume, 6) + " GNOT | " + units(totalFees, 6) + " GNOT | " +
110		num(filledCount) + " | " + strconv.Itoa(openPublic.Size()) + " | " + bps(feeBps))
111	if gnomicFeeOn {
112		b.WriteString(" (GNOMIC " + bps(gnomicFee) + ")")
113	}
114	b.WriteString(" |\n\n")
115
116	b.WriteString("## Open offers\n\n")
117	size := openPublic.Size()
118	// Compared as page numbers, not as an offset: (page-1)*pageSize overflows
119	// for a large enough page.
120	if page > 1 && page-1 > (size-1)/defaultPageSize {
121		return "# Not found\n"
122	}
123	if size == 0 {
124		b.WriteString("No open offers.\n")
125		return b.String()
126	}
127	offset := (page - 1) * defaultPageSize
128	b.WriteString("| ID | Side | Token | Amount | GNOT | Maker | Expires |\n|---|---|---|---|---|---|---|\n")
129	openPublic.ReverseIterateByOffset(offset, defaultPageSize, func(key string, _ any) bool {
130		o := offers.Get(key).(*Offer)
131		sym, dec := tokenInfo(o.TokenKey)
132		b.WriteString("| [" + num64(o.ID) + "](" + link("offer/"+num64(o.ID)) + ") | " + o.Side.String() +
133			" | " + sym + " | " + units(o.TokenAmt, dec) + " | " + units(o.GnotAmt, 6) +
134			" | " + short(o.Maker) + " | " + expiry(o) + " |\n")
135		return false
136	})
137	if page > 1 {
138		b.WriteString("\n[← newer](" + link("page/"+strconv.Itoa(page-1)) + ")")
139	}
140	if offset+defaultPageSize < size {
141		b.WriteString("\n[older →](" + link("page/"+strconv.Itoa(page+1)) + ")")
142	}
143	b.WriteString("\n")
144	return b.String()
145}
146
147func renderOffer(o *Offer) string {
148	sym, dec := tokenInfo(o.TokenKey)
149	fee := feeOf(o.GnotAmt, o.FeeBps)
150	var b strings.Builder
151	b.WriteString("# Offer " + num64(o.ID) + "\n\n")
152	b.WriteString("| | |\n|---|---|\n")
153	if o.Status == Open && expired(o) {
154		b.WriteString("| Status | Open, expired: it can no longer be filled, only cancelled by its maker |\n")
155	} else {
156		b.WriteString("| Status | " + o.Status.String() + " |\n")
157	}
158	b.WriteString("| Side | " + o.Side.String() + " |\n")
159	b.WriteString("| Token | " + sym + " (`" + o.TokenKey + "`) |\n")
160	b.WriteString("| Token amount | " + units(o.TokenAmt, dec) + " " + sym + " |\n")
161	b.WriteString("| GNOT amount | " + units(o.GnotAmt, 6) + " GNOT |\n")
162	b.WriteString("| Fee | " + bps(o.FeeBps) + " = " + units(fee, 6) + " GNOT, paid by whoever receives the GNOT |\n")
163	b.WriteString("| Maker | `" + o.Maker.String() + "` |\n")
164	if o.Taker == "" {
165		b.WriteString("| Taker | anyone |\n")
166	} else {
167		b.WriteString("| Taker | `" + o.Taker.String() + "` (not listed) |\n")
168	}
169	b.WriteString("| Created | " + when(o.CreatedAt) + " |\n")
170	b.WriteString("| Expires | " + expiry(o) + " |\n")
171	b.WriteString("\n[← all offers](" + link("") + ")\n")
172	return b.String()
173}
174
175// ---------------------------------------------------------------------------
176// Helpers
177// ---------------------------------------------------------------------------
178
179func listJSON(t *avl.Tree, offset, limit int) string {
180	if offset < 0 {
181		offset = 0
182	}
183	if limit <= 0 {
184		limit = defaultPageSize
185	}
186	if limit > maxPageSize {
187		limit = maxPageSize
188	}
189	var b strings.Builder
190	b.WriteString(`{"total":` + strconv.Itoa(t.Size()) + `,"offers":[`)
191	i := 0
192	t.ReverseIterateByOffset(offset, limit, func(key string, _ any) bool {
193		if i > 0 {
194			b.WriteString(",")
195		}
196		i++
197		b.WriteString(offerJSON(offers.Get(key).(*Offer)))
198		return false
199	})
200	b.WriteString("]}")
201	return b.String()
202}
203
204func offerJSON(o *Offer) string {
205	sym, dec := tokenInfo(o.TokenKey)
206	return `{"id":` + jstr(num64(o.ID)) +
207		`,"maker":` + jstr(o.Maker.String()) +
208		`,"taker":` + jstr(o.Taker.String()) +
209		`,"side":` + jstr(o.Side.String()) +
210		`,"token_key":` + jstr(o.TokenKey) +
211		`,"token_symbol":` + jstr(sym) +
212		`,"token_decimals":` + strconv.Itoa(dec) +
213		`,"token_amt":` + jstr(num(o.TokenAmt)) +
214		`,"gnot_amt":` + jstr(num(o.GnotAmt)) +
215		`,"fee_bps":` + num(o.FeeBps) +
216		`,"fee_ugnot":` + jstr(num(feeOf(o.GnotAmt, o.FeeBps))) +
217		`,"expires_at":` + num(o.ExpiresAt) +
218		`,"expired":` + strconv.FormatBool(expired(o)) +
219		`,"created_at":` + num(o.CreatedAt) +
220		`,"status":` + jstr(o.Status.String()) + `}`
221}
222
223func tokenInfo(key string) (string, int) {
224	t := grc20reg.Get(key)
225	if t == nil {
226		return key, 0
227	}
228	return t.GetSymbol(), t.GetDecimals()
229}
230
231// jstr quotes a string for JSON.
232func jstr(s string) string {
233	var b strings.Builder
234	b.WriteByte('"')
235	for _, r := range s {
236		switch {
237		case r == '"' || r == '\\':
238			b.WriteByte('\\')
239			b.WriteRune(r)
240		case r < 0x20:
241			h := strconv.FormatInt(int64(r), 16)
242			b.WriteString(`\u` + strings.Repeat("0", 4-len(h)) + h)
243		default:
244			b.WriteRune(r)
245		}
246	}
247	b.WriteByte('"')
248	return b.String()
249}
250
251func num(v int64) string    { return strconv.FormatInt(v, 10) }
252func num64(v uint64) string { return strconv.FormatUint(v, 10) }
253
254// units formats base units with dec decimals, trailing zeros dropped.
255func units(v int64, dec int) string {
256	s := strconv.FormatInt(v, 10)
257	if dec <= 0 {
258		return s
259	}
260	if len(s) <= dec {
261		s = strings.Repeat("0", dec-len(s)+1) + s
262	}
263	whole, frac := s[:len(s)-dec], strings.TrimRight(s[len(s)-dec:], "0")
264	if frac == "" {
265		return whole
266	}
267	return whole + "." + frac
268}
269
270func bps(v int64) string { return units(v, 2) + "%" }
271
272func short(a address) string {
273	s := a.String()
274	if len(s) <= 12 {
275		return s
276	}
277	return s[:8] + "…" + s[len(s)-4:]
278}
279
280// link is an absolute gnoweb link to one of this realm's Render paths.
281// gnoweb rejects a bare ":path" destination and prints it as plain text.
282func link(path string) string {
283	if path == "" {
284		return webPath
285	}
286	return webPath + ":" + path
287}
288
289// expiry is the Expires cell: the date, marked once it has passed.
290func expiry(o *Offer) string {
291	if expired(o) {
292		return "expired " + when(o.ExpiresAt)
293	}
294	return when(o.ExpiresAt)
295}
296
297func when(unix int64) string {
298	if unix == 0 {
299		return "never"
300	}
301	return time.Unix(unix, 0).UTC().Format("2006-01-02 15:04 UTC")
302}