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