Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}