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

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}