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

render.gno

5.14 Kb · 168 lines
  1package position
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/moul/md/v0"
  9	"gno.land/p/moul/mdtable/v0"
 10	ufmt "gno.land/p/nt/ufmt/v0"
 11
 12	"gno.land/r/gnoswap/halt/v1"
 13	positionRoot "gno.land/r/gnoswap/position"
 14)
 15
 16func (p *positionV1) Render(path string) string {
 17	if path == "" {
 18		return p.renderSummary()
 19	}
 20
 21	positionID, ok := parsePositionRenderPath(path)
 22	if !ok {
 23		return "404\n"
 24	}
 25
 26	storedPosition, exists := p.store.GetPosition(positionID)
 27	if !exists {
 28		return "404\n"
 29	}
 30
 31	return p.renderPositionDetail(positionID, storedPosition)
 32}
 33
 34func (p *positionV1) renderSummary() string {
 35	out := md.H1("Gnoswap Position")
 36	out += md.Paragraph(
 37		"Position records remain stored after burning. The record count includes burned " +
 38			"positions and is not a count of active liquidity positions. Use " +
 39			md.InlineCode("id/<id>") + " for one stored record, for example " +
 40			md.Link("id/1", "/r/gnoswap/position:id/1") +
 41			". The detail route performs one keyed lookup and does not enumerate positions or recompute claimable values.",
 42	)
 43	runtimeTable := mdtable.Table{
 44		Headers: []string{"Field", "Value"},
 45		Rows: [][]string{
 46			{"Realm address", md.InlineCode(chain.PackageAddress("gno.land/r/gnoswap/position").String())},
 47			{"Active implementation", renderPackageLink(positionRoot.GetImplementationPackagePath())},
 48			{"Position operations halted", ufmt.Sprintf("%t", halt.IsHaltedPosition())},
 49			{"Withdrawals halted", ufmt.Sprintf("%t", halt.IsHaltedWithdraw())},
 50		},
 51	}
 52	out += md.H2("Runtime") + runtimeTable.String()
 53
 54	records := mdtable.Table{
 55		Headers: []string{"Field", "Value"},
 56		Rows: [][]string{
 57			{"Stored position records (including burned)", ufmt.Sprintf("%d", p.store.GetPositions().Size())},
 58			{"Next position ID", ufmt.Sprintf("%d", p.store.GetPositionNextID())},
 59		},
 60	}
 61	return out + md.H2("Stored records") + records.String()
 62}
 63
 64func (p *positionV1) renderPositionDetail(positionID uint64, stored positionRoot.Position) string {
 65	status := "Active"
 66	if stored.Burned() {
 67		status = "Burned (record retained)"
 68	}
 69
 70	owner := "unavailable"
 71	ownerAddress, err := p.nftAccessor.OwnerOf(positionIdFrom(positionID))
 72	if err == nil && ownerAddress != "" {
 73		owner = ownerAddress.String()
 74	}
 75
 76	token0, token1, poolFee, validPoolKey := splitPoolKeyForRender(stored.PoolKey())
 77	token0Reference := "unavailable (stored pool key is not token0:token1:fee)"
 78	token1Reference := token0Reference
 79	if validPoolKey {
 80		token0Reference = renderPackageLink(token0)
 81		token1Reference = renderPackageLink(token1)
 82	}
 83
 84	out := md.H1(ufmt.Sprintf("Gnoswap Position %d", positionID))
 85	out += md.Paragraph(
 86		"This page reads one persisted position record. Stored accounting fields are " +
 87			"shown as stored; claimable fees and current token balances are recomputed " +
 88			"values and are intentionally not queried here.",
 89	)
 90	identity := mdtable.Table{
 91		Headers: []string{"Field", "Value"},
 92		Rows: [][]string{
 93			{"Position ID", ufmt.Sprintf("%d", positionID)},
 94			{"Status", status},
 95			{"Owner", owner},
 96		},
 97	}
 98	out += md.H2("Identity") + identity.String()
 99
100	poolAndRange := mdtable.Table{
101		Headers: []string{"Field", "Value"},
102		Rows: [][]string{
103			{"Pool key", md.InlineCode(stored.PoolKey())},
104			{"Token 0 reference", token0Reference},
105			{"Token 1 reference", token1Reference},
106			{"Pool fee (from stored key)", poolFee},
107			{"Lower tick (stored)", ufmt.Sprintf("%d", stored.TickLower())},
108			{"Upper tick (stored)", ufmt.Sprintf("%d", stored.TickUpper())},
109		},
110	}
111	out += md.H2("Pool and range") + poolAndRange.String()
112
113	accounting := mdtable.Table{
114		Headers: []string{"Field", "Value"},
115		Rows: [][]string{
116			{"Liquidity (stored)", stored.Liquidity()},
117			{"Fee growth token 0 inside, last X128 (stored)", stored.FeeGrowthInside0LastX128()},
118			{"Fee growth token 1 inside, last X128 (stored)", stored.FeeGrowthInside1LastX128()},
119			{"Token 0 owed (stored accounting)", ufmt.Sprintf("%d", stored.TokensOwed0())},
120			{"Token 1 owed (stored accounting)", ufmt.Sprintf("%d", stored.TokensOwed1())},
121			{"Claimable fees / current token balances", "Not rendered; would require recomputation"},
122		},
123	}
124	return out + md.H2("Stored accounting") + accounting.String()
125}
126
127func parsePositionRenderPath(path string) (uint64, bool) {
128	const prefix = "id/"
129	if len(path) <= len(prefix) || path[:len(prefix)] != prefix {
130		return 0, false
131	}
132
133	id := path[len(prefix):]
134	for i := 0; i < len(id); i++ {
135		if id[i] < '0' || id[i] > '9' {
136			return 0, false
137		}
138	}
139
140	positionID, err := strconv.ParseUint(id, 10, 64)
141	if err != nil {
142		return 0, false
143	}
144
145	return positionID, true
146}
147
148func splitPoolKeyForRender(poolKey string) (string, string, string, bool) {
149	parts := strings.Split(poolKey, ":")
150	if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" {
151		return "", "", "", false
152	}
153
154	return parts[0], parts[1], parts[2], true
155}
156
157func renderPackageLink(path string) string {
158	if strings.HasPrefix(path, "gno.land/") {
159		destination := strings.TrimPrefix(path, "gno.land")
160		// Token references append .SYMBOL; link to the defining realm.
161		if separator := strings.Index(destination, "."); separator >= 0 {
162			destination = destination[:separator]
163		}
164		return md.Link(path, destination)
165	}
166
167	return md.InlineCode(path)
168}