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}