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

handles.gno

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