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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}