agentid.gno
6.58 Kb · 260 lines
1// Package agentid binds one short name to one wallet.
2//
3// Claim writes the binding. Pay forwards the ugnot attached to that
4// call to the bound wallet. A binding cannot be renamed, transferred,
5// or deleted. The name does not grant a gno.land deploy namespace.
6package agentbook
7
8import (
9 "chain"
10 "chain/banker"
11 "chain/runtime/unsafe"
12 "strconv"
13
14 std "gno.land/p/g1y0n2geu0rmdrm9u30c5fmk3ykkl2enw9n9yr2k/agentid"
15 "gno.land/p/nt/avl/v0"
16)
17
18const (
19 maxRecent = 20
20 denomUgnot = "ugnot"
21)
22
23// record is the binding stored under both indexes.
24type record struct {
25 name string
26 owner address
27}
28
29var (
30 byName avl.Tree // name -> *record
31 byAddr avl.Tree // owner.String() -> *record
32 recent []string // newest claim first, capped at maxRecent
33)
34
35// Claim binds name to the calling wallet. This is the registration call.
36//
37// The name follows gno.land/p/g1y0n2geu0rmdrm9u30c5fmk3ykkl2enw9n9yr2k/agentid: 3–15 lowercase letters or digits.
38// One wallet can hold one name. Direct wallet calls only. Coins attached
39// to Claim are rejected. A failed claim panics so the transaction reverts.
40func Claim(cur realm, name string) {
41 if !cur.Previous().IsUserCall() {
42 panic("direct wallet call required")
43 }
44 if len(unsafe.OriginSend()) != 0 {
45 panic("claim takes no payment")
46 }
47 if !std.Valid(name) {
48 panic("invalid name")
49 }
50 if std.Reserved(name) {
51 panic("reserved name")
52 }
53 caller := cur.Previous().Address()
54 if !caller.IsValid() {
55 panic("invalid caller")
56 }
57 if byAddr.Has(caller.String()) {
58 panic("already claimed")
59 }
60 if byName.Has(name) {
61 panic("name taken")
62 }
63
64 rec := &record{name: name, owner: caller}
65 byName.Set(name, rec)
66 byAddr.Set(caller.String(), rec)
67 pushRecent(name)
68 chain.Emit("Claim", "name", name, "owner", caller.String())
69}
70
71// Resolve returns the wallet bound to name.
72// The second result is false when no binding exists.
73func Resolve(name string) (address, bool) {
74 rec, ok := lookup(name)
75 if !ok {
76 return address(""), false
77 }
78 return rec.owner, true
79}
80
81// Reverse returns the name bound to addr.
82// The second result is false when addr is not a valid address or has no name.
83func Reverse(addr address) (string, bool) {
84 if !addr.IsValid() {
85 return "", false
86 }
87 raw := byAddr.Get(addr.String())
88 if raw == nil {
89 return "", false
90 }
91 return raw.(*record).name, true
92}
93
94// NameOf returns the name bound to addr, or "" when addr has none.
95func NameOf(addr address) string {
96 name, ok := Reverse(addr)
97 if !ok {
98 return ""
99 }
100 return name
101}
102
103// IsAvailable reports whether a wallet may claim name.
104// Invalid, reserved, and taken names are false. The bare name is required.
105func IsAvailable(name string) bool {
106 if !std.Valid(name) || std.Reserved(name) {
107 return false
108 }
109 return !byName.Has(name)
110}
111
112// Count returns the number of bindings.
113func Count() int {
114 return byName.Size()
115}
116
117// Pay forwards the ugnot attached to this call to the wallet bound to name.
118//
119// Direct wallet calls only. The call must attach ugnot and nothing else.
120// An unknown name, a non-wallet caller, or a missing receipt aborts the
121// transaction, so the coins do not stay on this realm.
122func Pay(cur realm, name string) {
123 if !cur.Previous().IsUserCall() {
124 panic("direct wallet call required")
125 }
126 sent := unsafe.OriginSend()
127 if len(sent) != 1 || sent[0].Denom != denomUgnot || sent[0].Amount <= 0 {
128 panic("send ugnot")
129 }
130 amt := sent[0].Amount
131 rec, ok := lookup(name)
132 if !ok {
133 panic("unknown name")
134 }
135
136 banker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)
137 realmAddr := cur.Address()
138 // IsUserCall means this amount landed here. Sending exactly that
139 // amount leaves any earlier balance untouched.
140 if banker_.GetCoins(realmAddr).AmountOf(denomUgnot) < amt {
141 panic("ugnot not received")
142 }
143 banker_.SendCoins(realmAddr, rec.owner, chain.NewCoins(chain.NewCoin(denomUgnot, amt)))
144 chain.Emit("Pay",
145 "name", name,
146 "to", rec.owner.String(),
147 "ugnot", strconv.FormatInt(amt, 10),
148 )
149}
150
151// Render shows the registry, one name, or the name bound to an address.
152//
153// path may be a bare name or the same name plus the .agent suffix.
154// A valid address with no binding gets a static page. Any other path
155// gets a static not-found page. Render does not write state and does
156// not echo the raw path.
157func Render(path string) string {
158 key := renderKey(path)
159 if key == "" {
160 return renderHome()
161 }
162 if std.Valid(key) {
163 rec, ok := lookup(key)
164 if !ok {
165 return notFound()
166 }
167 return renderCard(rec)
168 }
169 addr := address(key)
170 if addr.IsValid() {
171 name := NameOf(addr)
172 rec, ok := lookup(name)
173 if !ok {
174 return "# Agent ID\n\nThis address has no Agent ID.\n"
175 }
176 return renderCard(rec)
177 }
178 return notFound()
179}
180
181func renderCard(rec *record) string {
182 out := "# " + rec.name + std.Suffix + "\n\n"
183 out += "Address: " + rec.owner.String() + "\n\n"
184 out += "Pay forwards the ugnot attached to that call to this address. "
185 out += "The call uses the bare name `" + rec.name + "`.\n\n"
186 out += "This name is not an r/sys/namereg username and does not open a deploy namespace. "
187 out += "It cannot be renamed, transferred, or deleted.\n"
188 return out
189}
190
191func renderHome() string {
192 out := "# Agent ID\n\n"
193 out += "A short name for a gno.land address. "
194 out += "Claim binds it to your wallet. Pay forwards ugnot to that wallet.\n\n"
195 out += "Names are 3–15 lowercase letters or digits. "
196 out += "One name per address. A claim cannot be renamed, transferred, or deleted.\n\n"
197 out += "This book is not r/sys/namereg. A name here does not open a deploy namespace. "
198 out += "Claim takes no payment.\n\n"
199 out += "Registered: " + strconv.Itoa(byName.Size()) + "\n\n"
200 out += "## Recent\n\n"
201 if len(recent) == 0 {
202 out += "No names yet.\n"
203 return out
204 }
205 for _, name := range recent {
206 rec, ok := lookup(name)
207 if !ok {
208 continue
209 }
210 out += "- [" + rec.name + std.Suffix + "](:" + rec.name + ")\n"
211 }
212 return out
213}
214
215func notFound() string {
216 return "# Agent ID\n\nName not found.\n"
217}
218
219func renderKey(path string) string {
220 for len(path) > 0 && path[0] == '/' {
221 path = path[1:]
222 }
223 if i := indexByte(path, '/'); i >= 0 {
224 path = path[:i]
225 }
226 suffix := std.Suffix
227 if len(path) > len(suffix) && path[len(path)-len(suffix):] == suffix {
228 return path[:len(path)-len(suffix)]
229 }
230 return path
231}
232
233func lookup(name string) (*record, bool) {
234 raw := byName.Get(name)
235 if raw == nil {
236 return nil, false
237 }
238 return raw.(*record), true
239}
240
241func pushRecent(name string) {
242 next := make([]string, 0, maxRecent)
243 next = append(next, name)
244 for _, n := range recent {
245 if len(next) == maxRecent {
246 break
247 }
248 next = append(next, n)
249 }
250 recent = next
251}
252
253func indexByte(s string, c byte) int {
254 for i := 0; i < len(s); i++ {
255 if s[i] == c {
256 return i
257 }
258 }
259 return -1
260}