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

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}