vault.gno
3.82 Kb · 147 lines
1package vault
2
3import (
4 "chain"
5 "strconv"
6 "strings"
7
8 "gno.land/p/moul/kit/ui/v0"
9 "gno.land/p/nt/avl/v0"
10)
11
12// selfPath is this realm's own page. A root-relative link resolves against
13// the domain, not the realm, so every link back into it names it in full.
14const selfPath = "/r/moul/x/daily/vault/v1"
15
16// vaults maps address string -> *avl.Tree (key string -> value string).
17var vaults avl.Tree
18
19// The bounds on what a caller may store. Anyone may open a vault, the root
20// page lists every vault, and a vault's page lists every entry.
21const (
22 MaxVaults = 1000
23 MaxEntries = 100 // per vault
24 MaxKeyLen = 64
25 MaxValueLen = 512
26)
27
28// vaultFor returns the caller's namespace tree, creating it if needed.
29func vaultFor(owner address) *avl.Tree {
30 k := owner.String()
31 if v := vaults.Get(k); v != nil {
32 return v.(*avl.Tree)
33 }
34 if vaults.Size() >= MaxVaults {
35 panic("vault is full at " + strconv.Itoa(MaxVaults) + " vaults")
36 }
37 t := &avl.Tree{}
38 vaults.Set(k, t)
39 return t
40}
41
42// Set stores value under key in the caller's own namespace.
43func Set(cur realm, key string, value string) {
44 who := caller(cur)
45 if key == "" {
46 panic("key must not be empty")
47 }
48 if len(key) > MaxKeyLen || len(value) > MaxValueLen {
49 panic("key or value too long")
50 }
51 t := vaultFor(who)
52 if !t.Has(key) && t.Size() >= MaxEntries {
53 panic("a vault holds at most " + strconv.Itoa(MaxEntries) + " entries")
54 }
55 t.Set(key, value)
56 chain.Emit("VaultSet", "owner", who.String(), "key", key)
57}
58
59// Delete removes key from the caller's own namespace.
60func Delete(cur realm, key string) {
61 who := caller(cur)
62 k := who.String()
63 v := vaults.Get(k)
64 if v == nil {
65 panic("no vault for caller")
66 }
67 t := v.(*avl.Tree)
68 if _, removed := t.Remove(key); !removed {
69 panic("key not found")
70 }
71 if t.Size() == 0 {
72 vaults.Remove(k)
73 }
74 chain.Emit("VaultDelete", "owner", who.String(), "key", key)
75}
76
77// Render shows all vaults (root) or a single address's entries ("/<address>").
78func Render(path string) string {
79 p := strings.TrimPrefix(path, "/")
80 if p == "" {
81 return renderRoot()
82 }
83 return renderAddress(p)
84}
85
86func renderRoot() string {
87 var b strings.Builder
88 b.WriteString("# Key-Value Vault\n\n")
89 b.WriteString("A per-address key-value store. Each caller owns their own namespace; only the owner can mutate their entries.\n\n")
90 if vaults.Size() == 0 {
91 b.WriteString("_No vaults yet. Call `Set(cur, key, value)` to create one._\n")
92 return b.String()
93 }
94 b.WriteString("## Vaults\n\n")
95 b.WriteString("| Address | Entries |\n")
96 b.WriteString("|---|---|\n")
97 vaults.Iterate("", "", func(k string, v interface{}) bool {
98 t := v.(*avl.Tree)
99 b.WriteString("| [")
100 b.WriteString(k)
101 b.WriteString("](" + selfPath + ":")
102 // link back into this realm's render for the address subpage
103 b.WriteString(k)
104 b.WriteString(") | ")
105 b.WriteString(strconv.Itoa(t.Size()))
106 b.WriteString(" |\n")
107 return false
108 })
109 b.WriteString("\nView an address's entries at path `/<address>`.\n")
110 return b.String()
111}
112
113func renderAddress(addr string) string {
114 var b strings.Builder
115 b.WriteString("# Vault: ")
116 b.WriteString(ui.Inline(addr))
117 b.WriteString("\n\n")
118 v := vaults.Get(addr)
119 if v == nil {
120 b.WriteString("_No vault for this address._\n")
121 return b.String()
122 }
123 t := v.(*avl.Tree)
124 b.WriteString("| Key | Value |\n")
125 b.WriteString("|---|---|\n")
126 t.Iterate("", "", func(k string, val interface{}) bool {
127 b.WriteString("| ")
128 b.WriteString(ui.Cell(k))
129 b.WriteString(" | ")
130 b.WriteString(ui.Cell(val.(string)))
131 b.WriteString(" |\n")
132 return false
133 })
134 b.WriteString("\nTotal entries: ")
135 b.WriteString(strconv.Itoa(t.Size()))
136 b.WriteString("\n")
137 return b.String()
138}
139
140// caller is the address that crossed into this realm, after checking that the
141// realm token is this frame's own.
142func caller(cur realm) address {
143 if !cur.IsCurrent() {
144 panic("vault: spoofed realm")
145 }
146 return cur.Previous().Address()
147}