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.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}