pairreg.gno
6.07 Kb · 191 lines
1// Package pairreg is the registry every AMM pair instance announces itself to,
2// and the unified frontend over all of them.
3//
4// It is the third artifact of the instance-per-realm pattern: one shared
5// p/moul/x/pair/v0 holding the logic, N tiny realms holding one pair each, and
6// this realm knowing all of them.
7//
8// # Why registration cannot be faked, and code cannot be trusted
9//
10// Register keys an instance by cur.Previous().PkgPath(), which the runtime
11// supplies, so an instance cannot claim a path that is not its own. That is
12// the whole on-chain guarantee. It is NOT a guarantee about the code at that
13// path: gno has no way to read another package's source from inside a realm,
14// so nothing here can check that an instance really runs the shared template.
15// The mirror image of Ethereum's CREATE2, which proves the code and says
16// nothing legible about who deployed it.
17//
18// Consequences a reader of the index must keep in mind:
19//
20// - Reserves shown here are CLAIMED. They are read live from the instance's
21// own struct, which is honest for an instance running the template and
22// arbitrary for one that does not.
23// - Several instances may exist for the same token couple. That is allowed
24// on purpose; there is no canonical pair and no factory to enforce one.
25// - Approving tokens to an instance risks exactly what was approved. Read
26// the instance's source (vm/qfile on its path) before funding it.
27//
28// # Why the registry can read live state at all
29//
30// Instances register a *pair.Pair pointer, not an address, the way grc20reg
31// registers a *grc20.Token. One vm/qrender here therefore renders the real
32// state of every instance, with no indexer and no multicall. It is safe
33// because pair.Pair holds only concrete types: an interface or a func field
34// would let a hostile instance hand foreign code to this realm's frame.
35package pairreg
36
37import (
38 "chain"
39 "chain/runtime"
40 "strings"
41
42 "gno.land/p/moul/x/pair/v0"
43 "gno.land/p/nt/avl/v0"
44 "gno.land/p/nt/ufmt/v0"
45)
46
47// Entry is one registered instance.
48type Entry struct {
49 Path string // the instance realm's package path, supplied by the runtime
50 Pair *pair.Pair // live pointer into the instance's own storage
51 Height int64 // chain height at registration
52}
53
54// entries maps instance path -> *Entry. couples maps a pair ID
55// ("keyA~keyB") -> int, how many instances claim that couple.
56var (
57 entries avl.Tree
58 couples avl.Tree
59)
60
61// Register records the calling realm as an instance. Call it from the
62// instance's init, so that deploying and listing are one transaction:
63//
64// func init(cur realm) {
65// p = pair.New(keyA, keyB, grc20reg.MustGet(keyA), grc20reg.MustGet(keyB))
66// pairreg.Register(cross(cur), p)
67// }
68//
69// Permissionless by design: anyone may deploy an instance under their own
70// address namespace and land here. Spam is self funded, since the registering
71// transaction pays for the storage it adds.
72func Register(cur realm, p *pair.Pair) {
73 caller := cur.Previous()
74 path := caller.PkgPath()
75 if path == "" {
76 panic("pairreg: only a realm can register")
77 }
78 if p == nil {
79 panic("pairreg: nil pair")
80 }
81 if entries.Has(path) {
82 panic("pairreg: already registered: " + path)
83 }
84
85 entries.Set(path, &Entry{Path: path, Pair: p, Height: runtime.ChainHeight()})
86 couples.Set(p.ID(), countFor(p.ID())+1)
87
88 chain.Emit("RegisterPair",
89 "path", path,
90 "pair", p.ID(),
91 )
92}
93
94// Get returns the entry for an instance path, or nil.
95func Get(path string) *Entry {
96 v := entries.Get(path)
97 if v == nil {
98 return nil
99 }
100 return v.(*Entry)
101}
102
103// Size returns how many instances are registered.
104func Size() int { return entries.Size() }
105
106// Couples returns how many distinct token couples are represented.
107func Couples() int { return couples.Size() }
108
109// InstancesFor returns the instance paths claiming a given couple id.
110func InstancesFor(id string) []string {
111 out := []string{}
112 entries.Iterate("", "", func(_ string, value any) bool {
113 e := value.(*Entry)
114 if e.Pair.ID() == id {
115 out = append(out, e.Path)
116 }
117 return false
118 })
119 return out
120}
121
122// Render is the unified frontend: the index of every instance with its live
123// state, or one instance's own page when path is an instance path.
124func Render(path string) string {
125 if path != "" {
126 e := Get(path)
127 if e == nil {
128 return "# 404\n\nNo instance registered at `" + path + "`.\n"
129 }
130 out := e.Pair.Render("")
131 out += "\n---\n\n"
132 out += ufmt.Sprintf("Instance: [`%s`](%s) · registered at height %d · %d instance(s) claim this couple.\n",
133 e.Path, webPath(e.Path), e.Height, countFor(e.Pair.ID()))
134 return out
135 }
136
137 out := "# AMM pairs\n\n"
138 out += "One realm per token couple, all running [p/moul/x/pair/v0](/p/moul/x/pair/v0). "
139 out += "Anyone can deploy one under their own namespace and it lands here.\n\n"
140
141 if entries.Size() == 0 {
142 out += "_No instance yet._\n"
143 return out
144 }
145
146 out += ufmt.Sprintf("%d instance(s), %d couple(s).\n\n", entries.Size(), couples.Size())
147 out += "**Reserves below are claimed by each instance, not verified.** "
148 out += "Nothing on chain can check that an instance runs the shared template; read its source before funding it.\n\n"
149 out += "| pair | reserves | LP shares | providers | instance |\n"
150 out += "|---|---|---|---|---|\n"
151 entries.Iterate("", "", func(key string, value any) bool {
152 e := value.(*Entry)
153 symA, symB := e.Pair.Symbols()
154 resA, resB := e.Pair.Reserves()
155 out += ufmt.Sprintf("| [%s/%s](/r/moul/x/pairreg/v0:%s) | %d %s / %d %s | %d | %d | [`%s`](%s) |\n",
156 symA, symB, key,
157 resA, symA, resB, symB,
158 e.Pair.TotalShares(), e.Pair.Providers(),
159 shortPath(e.Path), webPath(e.Path))
160 return false
161 })
162 return out
163}
164
165//
166// Internals.
167//
168
169func countFor(id string) int {
170 v := couples.Get(id)
171 if v == nil {
172 return 0
173 }
174 return v.(int)
175}
176
177// webPath turns a package path into a gnoweb link.
178func webPath(pkgPath string) string {
179 if i := strings.Index(pkgPath, "/"); i >= 0 {
180 return pkgPath[i:]
181 }
182 return "/" + pkgPath
183}
184
185// shortPath drops the chain domain, which is the same on every row.
186func shortPath(pkgPath string) string {
187 if i := strings.Index(pkgPath, "/"); i >= 0 {
188 return pkgPath[i+1:]
189 }
190 return pkgPath
191}