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.

wallet.gno

10.44 Kb · 468 lines
  1// Package agentwallet is a spend-limited wallet bound to an Agent ID.
  2//
  3// The owner funds it and sets the policy. A different address, the agent,
  4// is the only caller that can move those funds. Swap, exchange, liquidity,
  5// and stake stay inside this realm. They are not a public exchange.
  6package agentwallet
  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	book "gno.land/r/g1y0n2geu0rmdrm9u30c5fmk3ykkl2enw9n9yr2k/agentbook"
 17)
 18
 19const denomUgnot = "ugnot"
 20
 21type wallet struct {
 22	owner     address
 23	agent     address
 24	name      string
 25	armed     bool
 26	txCap     int64
 27	allowance int64
 28	actions   []string
 29	names     []string
 30	free      int64
 31	exchange  int64
 32	staked    int64
 33	units     int64
 34	shares    int64
 35}
 36
 37var (
 38	wallets    avl.Tree // owner.String() -> *wallet
 39	byAgent    avl.Tree // agent.String() -> owner.String()
 40	poolUgnot  int64
 41	poolUnits  int64
 42	poolShares int64
 43)
 44
 45// Open binds a fresh agent address to the caller's Agent ID.
 46// The agent address must be a different key from the owner.
 47func Open(cur realm, agent address) {
 48	if !cur.Previous().IsUserCall() {
 49		panic("direct wallet call required")
 50	}
 51	if len(unsafe.OriginSend()) != 0 {
 52		panic("no payment")
 53	}
 54	caller := cur.Previous().Address()
 55	name := book.NameOf(caller)
 56	if name == "" {
 57		panic("claim a name")
 58	}
 59	if !agent.IsValid() {
 60		panic("invalid caller")
 61	}
 62	if agent == caller {
 63		panic("same key")
 64	}
 65	if wallets.Has(caller.String()) || byAgent.Has(agent.String()) {
 66		panic("already open")
 67	}
 68	wallets.Set(caller.String(), &wallet{owner: caller, agent: agent, name: name})
 69	byAgent.Set(agent.String(), caller.String())
 70	chain.Emit("Open", "owner", caller.String(), "agent", agent.String(), "name", name)
 71}
 72
 73// Fund adds the ugnot attached to this call to the owner's agent wallet.
 74func Fund(cur realm) {
 75	if !cur.Previous().IsUserCall() {
 76		panic("direct wallet call required")
 77	}
 78	sent := unsafe.OriginSend()
 79	if len(sent) != 1 || sent[0].Denom != denomUgnot || sent[0].Amount <= 0 {
 80		panic("send ugnot")
 81	}
 82	w := mustOwner(cur.Previous().Address())
 83	amt := sent[0].Amount
 84	banker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)
 85	if banker_.GetCoins(cur.Address()).AmountOf(denomUgnot) < amt {
 86		panic("ugnot not received")
 87	}
 88	w.free += amt
 89	chain.Emit("Fund", "owner", w.owner.String(), "ugnot", strconv.FormatInt(amt, 10))
 90}
 91
 92// SetPolicy replaces the per-action cap, the remaining allowance, and the allowlists.
 93// actions and names are comma-separated. The owner calls this.
 94func SetPolicy(cur realm, txCap int64, allowance int64, actions string, names string) {
 95	if !cur.Previous().IsUserCall() {
 96		panic("direct wallet call required")
 97	}
 98	if len(unsafe.OriginSend()) != 0 {
 99		panic("no payment")
100	}
101	if txCap < 0 {
102		panic("over cap")
103	}
104	if allowance < 0 {
105		panic("over allowance")
106	}
107	w := mustOwner(cur.Previous().Address())
108	w.txCap = txCap
109	w.allowance = allowance
110	w.actions = cleanActions(splitCSV(actions))
111	w.names = cleanNames(splitCSV(names))
112	chain.Emit("Policy", "owner", w.owner.String())
113}
114
115// Arm lets the agent address submit actions.
116func Arm(cur realm) {
117	setArmed(cur, true)
118}
119
120// Disarm stops the agent address until the owner arms it again.
121func Disarm(cur realm) {
122	setArmed(cur, false)
123}
124
125func setArmed(cur realm, armed bool) {
126	if !cur.Previous().IsUserCall() {
127		panic("direct wallet call required")
128	}
129	if len(unsafe.OriginSend()) != 0 {
130		panic("no payment")
131	}
132	w := mustOwner(cur.Previous().Address())
133	w.armed = armed
134}
135
136// Act moves funds already inside the agent wallet.
137// The caller must be the bound agent address, not the owner.
138func Act(cur realm, action string, dest string, amount int64) {
139	if !cur.Previous().IsUserCall() {
140		panic("direct wallet call required")
141	}
142	if len(unsafe.OriginSend()) != 0 {
143		panic("no payment")
144	}
145	w := walletByAgent(cur.Previous().Address())
146	if w == nil {
147		panic("agent only")
148	}
149	if !w.armed {
150		panic("disarmed")
151	}
152	if !allowed(w.actions, groupOf(action)) {
153		panic("action denied")
154	}
155	switch action {
156	case "transfer":
157		transfer(cur, w, dest, amount)
158	case "exchange-in":
159		takeFree(w, amount)
160		w.exchange += amount
161	case "exchange-out":
162		moveBack(w, &w.exchange, amount)
163	case "stake":
164		takeFree(w, amount)
165		w.staked += amount
166	case "unstake":
167		moveBack(w, &w.staked, amount)
168	case "swap-buy":
169		swapBuy(w, amount)
170	case "swap-sell":
171		swapSell(w, amount)
172	case "liquidity-add":
173		addLiquidity(w, amount)
174	case "liquidity-remove":
175		removeLiquidity(w, amount, true)
176	default:
177		panic("action denied")
178	}
179	chain.Emit("Act", "agent", w.agent.String(), "action", action, "ugnot", strconv.FormatInt(amount, 10))
180}
181
182// Recover returns the owner's agent funds to the owner and disarms the agent.
183func Recover(cur realm) {
184	if !cur.Previous().IsUserCall() {
185		panic("direct wallet call required")
186	}
187	if len(unsafe.OriginSend()) != 0 {
188		panic("no payment")
189	}
190	w := mustOwner(cur.Previous().Address())
191	w.free += w.exchange + w.staked
192	w.exchange = 0
193	w.staked = 0
194	if w.shares > 0 {
195		removeLiquidity(w, w.shares, false)
196	}
197	if w.units > 0 && poolUgnot > 0 && poolUnits > 0 {
198		out := quoteOut(poolUnits, poolUgnot, w.units)
199		if out > 0 {
200			poolUnits += w.units
201			poolUgnot -= out
202			w.free += out
203		}
204	}
205	w.units = 0
206	amt := w.free
207	w.free = 0
208	w.armed = false
209	if amt > 0 {
210		banker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)
211		if banker_.GetCoins(cur.Address()).AmountOf(denomUgnot) < amt {
212			panic("ugnot not received")
213		}
214		banker_.SendCoins(cur.Address(), w.owner, chain.NewCoins(chain.NewCoin(denomUgnot, amt)))
215	}
216	chain.Emit("Recover", "owner", w.owner.String(), "ugnot", strconv.FormatInt(amt, 10))
217}
218
219// Free returns the spendable ugnot inside the owner's agent wallet.
220func Free(owner address) int64 {
221	w := ownerWallet(owner)
222	if w == nil {
223		return 0
224	}
225	return w.free
226}
227
228func transfer(cur realm, w *wallet, dest string, amount int64) {
229	if !allowed(w.names, dest) {
230		panic("not allowed")
231	}
232	to, ok := book.Resolve(dest)
233	if !ok {
234		panic("unknown name")
235	}
236	takeFree(w, amount)
237	banker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)
238	if banker_.GetCoins(cur.Address()).AmountOf(denomUgnot) < amount {
239		panic("ugnot not received")
240	}
241	banker_.SendCoins(cur.Address(), to, chain.NewCoins(chain.NewCoin(denomUgnot, amount)))
242}
243
244func swapBuy(w *wallet, amount int64) {
245	out := quoteOut(poolUgnot, poolUnits, amount)
246	if out <= 0 {
247		panic("no output")
248	}
249	takeFree(w, amount)
250	poolUgnot += amount
251	poolUnits -= out
252	w.units += out
253}
254
255func swapSell(w *wallet, amount int64) {
256	out := quoteOut(poolUnits, poolUgnot, amount)
257	if out <= 0 {
258		panic("no output")
259	}
260	if out > w.txCap {
261		panic("over cap")
262	}
263	if w.units < amount {
264		panic("insufficient")
265	}
266	w.units -= amount
267	poolUnits += amount
268	poolUgnot -= out
269	w.free += out
270}
271
272func addLiquidity(w *wallet, amount int64) {
273	if poolShares != 0 {
274		if poolUgnot <= 0 {
275			panic("no output")
276		}
277		sharesOut := amount * poolShares / poolUgnot
278		unitsNeeded := amount * poolUnits / poolUgnot
279		if sharesOut <= 0 || unitsNeeded <= 0 {
280			panic("no output")
281		}
282		if w.units < unitsNeeded {
283			panic("insufficient")
284		}
285		takeFree(w, amount)
286		w.units -= unitsNeeded
287		w.shares += sharesOut
288		poolUgnot += amount
289		poolUnits += unitsNeeded
290		poolShares += sharesOut
291		return
292	}
293	takeFree(w, amount)
294	poolUgnot += amount
295	poolUnits += amount
296	poolShares += amount
297	w.shares += amount
298}
299
300func removeLiquidity(w *wallet, shares int64, capped bool) {
301	if shares <= 0 || shares > w.shares || poolShares <= 0 {
302		panic("insufficient")
303	}
304	if shares == poolShares {
305		if capped && poolUgnot > w.txCap {
306			panic("over cap")
307		}
308		w.free += poolUgnot
309		poolUgnot = 0
310		poolUnits = 0
311		poolShares = 0
312		w.shares = 0
313		return
314	}
315	ugnotOut := shares * poolUgnot / poolShares
316	unitsOut := shares * poolUnits / poolShares
317	if ugnotOut <= 0 {
318		panic("no output")
319	}
320	if capped && ugnotOut > w.txCap {
321		panic("over cap")
322	}
323	w.shares -= shares
324	poolShares -= shares
325	poolUgnot -= ugnotOut
326	poolUnits -= unitsOut
327	w.free += ugnotOut
328	w.units += unitsOut
329}
330
331func takeFree(w *wallet, amount int64) {
332	if amount <= 0 {
333		panic("send ugnot")
334	}
335	if amount > w.txCap {
336		panic("over cap")
337	}
338	if amount > w.allowance {
339		panic("over allowance")
340	}
341	if amount > w.free {
342		panic("insufficient")
343	}
344	w.free -= amount
345	w.allowance -= amount
346}
347
348func moveBack(w *wallet, bucket *int64, amount int64) {
349	if amount <= 0 {
350		panic("send ugnot")
351	}
352	if amount > w.txCap {
353		panic("over cap")
354	}
355	if *bucket < amount {
356		panic("insufficient")
357	}
358	*bucket -= amount
359	w.free += amount
360}
361
362func quoteOut(reserveIn int64, reserveOut int64, amountIn int64) int64 {
363	if amountIn <= 0 || reserveIn <= 0 || reserveOut <= 0 {
364		return 0
365	}
366	if reserveOut > int64(9223372036854775807)/amountIn {
367		panic("overflow")
368	}
369	return reserveOut * amountIn / (reserveIn + amountIn)
370}
371
372func mustOwner(caller address) *wallet {
373	w := ownerWallet(caller)
374	if w == nil {
375		panic("no wallet")
376	}
377	return w
378}
379
380func ownerWallet(caller address) *wallet {
381	raw := wallets.Get(caller.String())
382	if raw == nil {
383		return nil
384	}
385	return raw.(*wallet)
386}
387
388func walletByAgent(caller address) *wallet {
389	raw := byAgent.Get(caller.String())
390	if raw == nil {
391		return nil
392	}
393	return ownerWallet(address(raw.(string)))
394}
395
396func groupOf(action string) string {
397	switch action {
398	case "transfer":
399		return "transfer"
400	case "exchange-in", "exchange-out":
401		return "exchange"
402	case "swap-buy", "swap-sell":
403		return "swap"
404	case "liquidity-add", "liquidity-remove":
405		return "liquidity"
406	case "stake", "unstake":
407		return "stake"
408	default:
409		return ""
410	}
411}
412
413func allowed(list []string, want string) bool {
414	if want == "" {
415		return false
416	}
417	for _, item := range list {
418		if item == want {
419			return true
420		}
421	}
422	return false
423}
424
425func cleanActions(items []string) []string {
426	if len(items) > 8 {
427		panic("invalid action")
428	}
429	out := make([]string, 0, len(items))
430	for _, item := range items {
431		switch item {
432		case "transfer", "exchange", "swap", "liquidity", "stake":
433			out = append(out, item)
434		default:
435			panic("invalid action")
436		}
437	}
438	return out
439}
440
441func cleanNames(items []string) []string {
442	if len(items) > 16 {
443		panic("invalid name")
444	}
445	out := make([]string, 0, len(items))
446	for _, item := range items {
447		if !std.Valid(item) || std.Reserved(item) {
448			panic("invalid name")
449		}
450		out = append(out, item)
451	}
452	return out
453}
454
455func splitCSV(s string) []string {
456	if s == "" {
457		return []string{}
458	}
459	out := []string{}
460	start := 0
461	for i := 0; i < len(s); i++ {
462		if s[i] == ',' {
463			out = append(out, s[start:i])
464			start = i + 1
465		}
466	}
467	return append(out, s[start:])
468}