Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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 := byOwner.Get(caller.String())
131	if v == nil {
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 := byHandle.Get(handle)
140	if v == nil {
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 := byOwner.Get(addr.String())
149	if v == nil {
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 := byHandle.Get(handle)
196	if v == nil {
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}