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.

agentid.gno

6.58 Kb · 260 lines
  1// Package agentid binds one short name to one wallet.
  2//
  3// Claim writes the binding. Pay forwards the ugnot attached to that
  4// call to the bound wallet. A binding cannot be renamed, transferred,
  5// or deleted. The name does not grant a gno.land deploy namespace.
  6package agentbook
  7
  8import (
  9	"chain"
 10	"chain/banker"
 11	"chain/runtime/unsafe"
 12	"strconv"
 13
 14	std "gno.land/p/g1y0n2geu0rmdrm9u30c5fmk3ykkl2enw9n9yr2k/agentid"
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18const (
 19	maxRecent  = 20
 20	denomUgnot = "ugnot"
 21)
 22
 23// record is the binding stored under both indexes.
 24type record struct {
 25	name  string
 26	owner address
 27}
 28
 29var (
 30	byName avl.Tree // name -> *record
 31	byAddr avl.Tree // owner.String() -> *record
 32	recent []string // newest claim first, capped at maxRecent
 33)
 34
 35// Claim binds name to the calling wallet. This is the registration call.
 36//
 37// The name follows gno.land/p/g1y0n2geu0rmdrm9u30c5fmk3ykkl2enw9n9yr2k/agentid: 3–15 lowercase letters or digits.
 38// One wallet can hold one name. Direct wallet calls only. Coins attached
 39// to Claim are rejected. A failed claim panics so the transaction reverts.
 40func Claim(cur realm, name string) {
 41	if !cur.Previous().IsUserCall() {
 42		panic("direct wallet call required")
 43	}
 44	if len(unsafe.OriginSend()) != 0 {
 45		panic("claim takes no payment")
 46	}
 47	if !std.Valid(name) {
 48		panic("invalid name")
 49	}
 50	if std.Reserved(name) {
 51		panic("reserved name")
 52	}
 53	caller := cur.Previous().Address()
 54	if !caller.IsValid() {
 55		panic("invalid caller")
 56	}
 57	if byAddr.Has(caller.String()) {
 58		panic("already claimed")
 59	}
 60	if byName.Has(name) {
 61		panic("name taken")
 62	}
 63
 64	rec := &record{name: name, owner: caller}
 65	byName.Set(name, rec)
 66	byAddr.Set(caller.String(), rec)
 67	pushRecent(name)
 68	chain.Emit("Claim", "name", name, "owner", caller.String())
 69}
 70
 71// Resolve returns the wallet bound to name.
 72// The second result is false when no binding exists.
 73func Resolve(name string) (address, bool) {
 74	rec, ok := lookup(name)
 75	if !ok {
 76		return address(""), false
 77	}
 78	return rec.owner, true
 79}
 80
 81// Reverse returns the name bound to addr.
 82// The second result is false when addr is not a valid address or has no name.
 83func Reverse(addr address) (string, bool) {
 84	if !addr.IsValid() {
 85		return "", false
 86	}
 87	raw := byAddr.Get(addr.String())
 88	if raw == nil {
 89		return "", false
 90	}
 91	return raw.(*record).name, true
 92}
 93
 94// NameOf returns the name bound to addr, or "" when addr has none.
 95func NameOf(addr address) string {
 96	name, ok := Reverse(addr)
 97	if !ok {
 98		return ""
 99	}
100	return name
101}
102
103// IsAvailable reports whether a wallet may claim name.
104// Invalid, reserved, and taken names are false. The bare name is required.
105func IsAvailable(name string) bool {
106	if !std.Valid(name) || std.Reserved(name) {
107		return false
108	}
109	return !byName.Has(name)
110}
111
112// Count returns the number of bindings.
113func Count() int {
114	return byName.Size()
115}
116
117// Pay forwards the ugnot attached to this call to the wallet bound to name.
118//
119// Direct wallet calls only. The call must attach ugnot and nothing else.
120// An unknown name, a non-wallet caller, or a missing receipt aborts the
121// transaction, so the coins do not stay on this realm.
122func Pay(cur realm, name string) {
123	if !cur.Previous().IsUserCall() {
124		panic("direct wallet call required")
125	}
126	sent := unsafe.OriginSend()
127	if len(sent) != 1 || sent[0].Denom != denomUgnot || sent[0].Amount <= 0 {
128		panic("send ugnot")
129	}
130	amt := sent[0].Amount
131	rec, ok := lookup(name)
132	if !ok {
133		panic("unknown name")
134	}
135
136	banker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)
137	realmAddr := cur.Address()
138	// IsUserCall means this amount landed here. Sending exactly that
139	// amount leaves any earlier balance untouched.
140	if banker_.GetCoins(realmAddr).AmountOf(denomUgnot) < amt {
141		panic("ugnot not received")
142	}
143	banker_.SendCoins(realmAddr, rec.owner, chain.NewCoins(chain.NewCoin(denomUgnot, amt)))
144	chain.Emit("Pay",
145		"name", name,
146		"to", rec.owner.String(),
147		"ugnot", strconv.FormatInt(amt, 10),
148	)
149}
150
151// Render shows the registry, one name, or the name bound to an address.
152//
153// path may be a bare name or the same name plus the .agent suffix.
154// A valid address with no binding gets a static page. Any other path
155// gets a static not-found page. Render does not write state and does
156// not echo the raw path.
157func Render(path string) string {
158	key := renderKey(path)
159	if key == "" {
160		return renderHome()
161	}
162	if std.Valid(key) {
163		rec, ok := lookup(key)
164		if !ok {
165			return notFound()
166		}
167		return renderCard(rec)
168	}
169	addr := address(key)
170	if addr.IsValid() {
171		name := NameOf(addr)
172		rec, ok := lookup(name)
173		if !ok {
174			return "# Agent ID\n\nThis address has no Agent ID.\n"
175		}
176		return renderCard(rec)
177	}
178	return notFound()
179}
180
181func renderCard(rec *record) string {
182	out := "# " + rec.name + std.Suffix + "\n\n"
183	out += "Address: " + rec.owner.String() + "\n\n"
184	out += "Pay forwards the ugnot attached to that call to this address. "
185	out += "The call uses the bare name `" + rec.name + "`.\n\n"
186	out += "This name is not an r/sys/namereg username and does not open a deploy namespace. "
187	out += "It cannot be renamed, transferred, or deleted.\n"
188	return out
189}
190
191func renderHome() string {
192	out := "# Agent ID\n\n"
193	out += "A short name for a gno.land address. "
194	out += "Claim binds it to your wallet. Pay forwards ugnot to that wallet.\n\n"
195	out += "Names are 3–15 lowercase letters or digits. "
196	out += "One name per address. A claim cannot be renamed, transferred, or deleted.\n\n"
197	out += "This book is not r/sys/namereg. A name here does not open a deploy namespace. "
198	out += "Claim takes no payment.\n\n"
199	out += "Registered: " + strconv.Itoa(byName.Size()) + "\n\n"
200	out += "## Recent\n\n"
201	if len(recent) == 0 {
202		out += "No names yet.\n"
203		return out
204	}
205	for _, name := range recent {
206		rec, ok := lookup(name)
207		if !ok {
208			continue
209		}
210		out += "- [" + rec.name + std.Suffix + "](:" + rec.name + ")\n"
211	}
212	return out
213}
214
215func notFound() string {
216	return "# Agent ID\n\nName not found.\n"
217}
218
219func renderKey(path string) string {
220	for len(path) > 0 && path[0] == '/' {
221		path = path[1:]
222	}
223	if i := indexByte(path, '/'); i >= 0 {
224		path = path[:i]
225	}
226	suffix := std.Suffix
227	if len(path) > len(suffix) && path[len(path)-len(suffix):] == suffix {
228		return path[:len(path)-len(suffix)]
229	}
230	return path
231}
232
233func lookup(name string) (*record, bool) {
234	raw := byName.Get(name)
235	if raw == nil {
236		return nil, false
237	}
238	return raw.(*record), true
239}
240
241func pushRecent(name string) {
242	next := make([]string, 0, maxRecent)
243	next = append(next, name)
244	for _, n := range recent {
245		if len(next) == maxRecent {
246			break
247		}
248		next = append(next, n)
249	}
250	recent = next
251}
252
253func indexByte(s string, c byte) int {
254	for i := 0; i < len(s); i++ {
255		if s[i] == c {
256			return i
257		}
258	}
259	return -1
260}