handles.gno
5.63 Kb · 218 lines
1// Package handles is a tiny on-chain nickname registry.
2//
3// Any address may claim one unique handle (3-20 lowercase letters/digits,
4// starting with a letter), attach a short bio, transfer the handle to
5// another address, or release it back to the pool. It is deliberately
6// small: two AVL indexes (handle -> record, address -> record) and a
7// handful of guarded mutators.
8package handles
9
10import (
11 "strconv"
12 "strings"
13
14 "chain"
15 "chain/runtime"
16
17 "gno.land/p/nt/avl/v0"
18)
19
20const maxBioLen = 140
21
22// record is the persisted state for one claimed handle.
23type record struct {
24 handle string
25 owner address
26 bio string
27 sinceBlk int64
28}
29
30var (
31 byHandle avl.Tree // handle (string) -> *record
32 byOwner avl.Tree // owner address.String() -> *record
33)
34
35// validHandle reports whether h is 3-20 chars, starts with a lowercase
36// letter, and contains only lowercase letters and digits thereafter.
37func validHandle(h string) bool {
38 if len(h) < 3 || len(h) > 20 {
39 return false
40 }
41 for i := 0; i < len(h); i++ {
42 c := h[i]
43 switch {
44 case c >= 'a' && c <= 'z':
45 case c >= '0' && c <= '9' && i > 0:
46 default:
47 return false
48 }
49 }
50 return true
51}
52
53// Register claims handle for the caller. Aborts if the handle is invalid,
54// already taken, or the caller already owns a handle (release it first).
55func Register(cur realm, handle string) {
56 if !cur.IsCurrent() {
57 panic("invalid realm")
58 }
59 if !cur.Previous().IsUserCall() {
60 panic("only a direct user call can register a handle")
61 }
62 if !validHandle(handle) {
63 panic("handle must be 3-20 lowercase letters/digits, starting with a letter")
64 }
65 caller := cur.Previous().Address()
66 if byOwner.Has(caller.String()) {
67 panic("this address already owns a handle; release it first")
68 }
69 if byHandle.Has(handle) {
70 panic("handle already taken")
71 }
72
73 r := &record{
74 handle: handle,
75 owner: caller,
76 sinceBlk: runtime.ChainHeight(),
77 }
78 byHandle.Set(handle, r)
79 byOwner.Set(caller.String(), r)
80 chain.Emit("HandleRegistered", "handle", handle, "owner", caller.String())
81}
82
83// SetBio updates the bio of the handle owned by the caller.
84func SetBio(cur realm, bio string) {
85 if !cur.IsCurrent() {
86 panic("invalid realm")
87 }
88 if len(bio) > maxBioLen {
89 panic("bio too long (max " + strconv.Itoa(maxBioLen) + " chars)")
90 }
91 r := ownRecord(cur)
92 r.bio = bio
93 chain.Emit("BioUpdated", "handle", r.handle)
94}
95
96// Release frees the handle owned by the caller, making it claimable again.
97func Release(cur realm) {
98 if !cur.IsCurrent() {
99 panic("invalid realm")
100 }
101 r := ownRecord(cur)
102 byHandle.Remove(r.handle)
103 byOwner.Remove(r.owner.String())
104 chain.Emit("HandleReleased", "handle", r.handle)
105}
106
107// Transfer moves the caller's handle to another address that does not
108// already own one.
109func Transfer(cur realm, to address) {
110 if !cur.IsCurrent() {
111 panic("invalid realm")
112 }
113 if !to.IsValid() {
114 panic("invalid recipient address")
115 }
116 r := ownRecord(cur)
117 if byOwner.Has(to.String()) {
118 panic("recipient already owns a handle")
119 }
120 byOwner.Remove(r.owner.String())
121 r.owner = to
122 byOwner.Set(to.String(), r)
123 chain.Emit("HandleTransferred", "handle", r.handle, "to", to.String())
124}
125
126// ownRecord resolves the record for the current caller, panicking if the
127// caller does not own a handle.
128func ownRecord(cur realm) *record {
129 caller := cur.Previous().Address()
130 v, ok := byOwner.Get(caller.String())
131 if !ok {
132 panic("this address does not own a handle")
133 }
134 return v.(*record)
135}
136
137// OwnerOf returns the owner of handle and whether it exists.
138func OwnerOf(handle string) (address, bool) {
139 v, ok := byHandle.Get(handle)
140 if !ok {
141 return address(""), false
142 }
143 return v.(*record).owner, true
144}
145
146// HandleOf returns the handle owned by addr, if any.
147func HandleOf(addr address) (string, bool) {
148 v, ok := byOwner.Get(addr.String())
149 if !ok {
150 return "", false
151 }
152 return v.(*record).handle, true
153}
154
155// Count returns the number of currently registered handles.
156func Count() int {
157 return byHandle.Size()
158}
159
160// Render renders Markdown. "/" lists every handle; "/<handle>" shows a
161// single record's detail.
162func Render(path string) string {
163 path = strings.TrimPrefix(path, "/")
164 if path == "" {
165 return renderIndex()
166 }
167 return renderHandle(path)
168}
169
170func renderIndex() string {
171 var b strings.Builder
172 b.WriteString("# 🪪 Handles\n\n")
173 b.WriteString("A tiny on-chain nickname registry. Claim a short handle, ")
174 b.WriteString("attach a bio, transfer it, or release it.\n\n")
175
176 if byHandle.Size() == 0 {
177 b.WriteString("_No handles registered yet._\n\n")
178 b.WriteString("Call `Register(handle)` to claim the first one.\n")
179 return b.String()
180 }
181
182 b.WriteString("| Handle | Owner | Since block |\n")
183 b.WriteString("|--------|-------|-------------|\n")
184 byHandle.Iterate("", "", func(key string, value interface{}) bool {
185 r := value.(*record)
186 b.WriteString("| [" + key + "](/" + key + ") | " + short(r.owner.String()) +
187 " | " + strconv.Itoa(int(r.sinceBlk)) + " |\n")
188 return false
189 })
190 b.WriteString("\nTotal handles: " + strconv.Itoa(byHandle.Size()) + "\n")
191 return b.String()
192}
193
194func renderHandle(handle string) string {
195 v, ok := byHandle.Get(handle)
196 if !ok {
197 return "# Handles\n\nNo handle named `" + handle + "`.\n\n[← all handles](/)\n"
198 }
199 r := v.(*record)
200
201 var b strings.Builder
202 b.WriteString("# 🪪 @" + r.handle + "\n\n")
203 b.WriteString("**Owner:** " + r.owner.String() + "\n\n")
204 if r.bio != "" {
205 b.WriteString("**Bio:** " + r.bio + "\n\n")
206 }
207 b.WriteString("**Registered at block:** " + strconv.Itoa(int(r.sinceBlk)) + "\n\n")
208 b.WriteString("[← all handles](/)\n")
209 return b.String()
210}
211
212// short truncates a long address for the index table.
213func short(s string) string {
214 if len(s) <= 12 {
215 return s
216 }
217 return s[:6] + "…" + s[len(s)-4:]
218}