diff --git a/examples/gno.land/r/demo/keystore/gno.mod b/examples/gno.land/r/demo/keystore/gno.mod new file mode 100644 index 00000000000..88d59c9ccd6 --- /dev/null +++ b/examples/gno.land/r/demo/keystore/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/keystore + +require ( + "gno.land/p/demo/ufmt" v0.0.0-latest + "gno.land/p/demo/avl" v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/keystore/keystore.gno b/examples/gno.land/r/demo/keystore/keystore.gno new file mode 100644 index 00000000000..5c76ccd90f8 --- /dev/null +++ b/examples/gno.land/r/demo/keystore/keystore.gno @@ -0,0 +1,184 @@ +package keystore + +import ( + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +var data avl.Tree + +const ( + BaseURL = "/r/demo/keystore" + StatusOK = "ok" + StatusNoUser = "user not found" + StatusNotFound = "key not found" + StatusNoWriteAccess = "no write access" + StatusCouldNotExecute = "could not execute" + StatusNoDatabases = "no databases" +) + +func init() { + data = avl.Tree{} // user -> avl.Tree +} + +// KeyStore stores the owner-specific avl.Tree +type KeyStore struct { + Owner std.Address + Data avl.Tree +} + +// Set will set a value to a key +// requires write-access (original caller must be caller) +func Set(k, v string) string { + origOwner := std.GetOrigCaller() + return set(origOwner.String(), k, v) +} + +// set (private) will set a key to value +// requires write-access (original caller must be caller) +func set(owner, k, v string) string { + origOwner := std.GetOrigCaller() + if origOwner.String() != owner { + return StatusNoWriteAccess + } + var keystore *KeyStore + keystoreInterface, exists := data.Get(owner) + if !exists { + keystore = &KeyStore{ + Owner: origOwner, + Data: avl.Tree{}, + } + data.Set(owner, keystore) + } else { + keystore = keystoreInterface.(*KeyStore) + } + keystore.Data.Set(k, v) + return StatusOK +} + +// Remove removes a key +// requires write-access (original owner must be caller) +func Remove(k string) string { + origOwner := std.GetOrigCaller() + return remove(origOwner.String(), k) +} + +// remove (private) removes a key +// requires write-access (original owner must be caller) +func remove(owner, k string) string { + origOwner := std.GetOrigCaller() + if origOwner.String() != owner { + return StatusNoWriteAccess + } + var keystore *KeyStore + keystoreInterface, exists := data.Get(owner) + if !exists { + keystore = &KeyStore{ + Owner: origOwner, + Data: avl.Tree{}, + } + data.Set(owner, keystore) + } else { + keystore = keystoreInterface.(*KeyStore) + } + _, removed := keystore.Data.Remove(k) + if !removed { + return StatusCouldNotExecute + } + return StatusOK +} + +// Get returns a value for a key +// read-only +func Get(k string) string { + origOwner := std.GetOrigCaller() + return remove(origOwner.String(), k) +} + +// get (private) returns a value for a key +// read-only +func get(owner, k string) string { + keystoreInterface, exists := data.Get(owner) + if !exists { + return StatusNoUser + } + keystore := keystoreInterface.(*KeyStore) + val, found := keystore.Data.Get(k) + if !found { + return StatusNotFound + } + return val.(string) +} + +// Size returns size of database +// read-only +func Size() string { + origOwner := std.GetOrigCaller() + return size(origOwner.String()) +} + +func size(owner string) string { + keystoreInterface, exists := data.Get(owner) + if !exists { + return StatusNoUser + } + keystore := keystoreInterface.(*KeyStore) + return ufmt.Sprintf("%d", keystore.Data.Size()) +} + +// Render provides read-only url access to the functions of the keystore +// "" -> show all keystores listed by owner +// "owner" -> show all keys for that owner's keystore +// "owner:size" -> returns size of owner's keystore +// "owner:get:key" -> show value for that key in owner's keystore +func Render(p string) string { + var response string + args := strings.Split(p, ":") + numArgs := len(args) + if p == "" { + numArgs = 0 + } + switch numArgs { + case 0: + if data.Size() == 0 { + return StatusNoDatabases + } + data.Iterate("", "", func(key string, value interface{}) bool { + ks := value.(*KeyStore) + response += ufmt.Sprintf("- [%s](%s:%s) (%d keys)\n", ks.Owner, BaseURL, ks.Owner, ks.Data.Size()) + return false + }) + case 1: + owner := args[0] + keystoreInterface, exists := data.Get(owner) + if !exists { + return StatusNoUser + } + ks := keystoreInterface.(*KeyStore) + i := 0 + response += ufmt.Sprintf("# %s database\n\n", ks.Owner) + ks.Data.Iterate("", "", func(key string, value interface{}) bool { + response += ufmt.Sprintf("- %d [%s](%s:%s:get:%s)\n", i, key, BaseURL, ks.Owner, key) + i++ + return false + }) + case 2: + owner := args[0] + cmd := args[1] + if cmd == "size" { + return size(owner) + } + case 3: + owner := args[0] + cmd := args[1] + key := args[2] + if cmd == "get" { + return get(owner, key) + } + } + + return response +} diff --git a/examples/gno.land/r/demo/keystore/keystore_test.gno b/examples/gno.land/r/demo/keystore/keystore_test.gno new file mode 100644 index 00000000000..998c8457751 --- /dev/null +++ b/examples/gno.land/r/demo/keystore/keystore_test.gno @@ -0,0 +1,78 @@ +package keystore + +import ( + "fmt" + "std" + "strings" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestRender(t *testing.T) { + const ( + author1 std.Address = testutils.TestAddress("author1") + author2 std.Address = testutils.TestAddress("author2") + ) + + tt := []struct { + caller std.Address + owner std.Address + ps []string + exp string + }{ + // can set database if the owner is the caller + {author1, author1, []string{"set", "hello", "gno"}, StatusOK}, + {author1, author1, []string{"size"}, "1"}, + {author1, author1, []string{"set", "hello", "world"}, StatusOK}, + {author1, author1, []string{"size"}, "1"}, + {author1, author1, []string{"set", "hi", "gno"}, StatusOK}, + {author1, author1, []string{"size"}, "2"}, + // only owner can remove + {author1, author1, []string{"remove", "hi"}, StatusOK}, + {author1, author1, []string{"get", "hi"}, StatusNotFound}, + {author1, author1, []string{"size"}, "1"}, + // add back + {author1, author1, []string{"set", "hi", "gno"}, StatusOK}, + {author1, author1, []string{"size"}, "2"}, + + // different owner has different database + {author2, author2, []string{"set", "hello", "universe"}, StatusOK}, + // either author can get the other info + {author1, author2, []string{"get", "hello"}, "universe"}, + // either author can get the other info + {author2, author1, []string{"get", "hello"}, "world"}, + {author1, author2, []string{"get", "hello"}, "universe"}, + // anyone can view the databases + {author1, author2, []string{}, `- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/keystore:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6) (2 keys) +- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00) (1 keys)`}, + // anyone can view the keys in a database + {author1, author2, []string{""}, `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00 database + +- 0 [hello](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00:get:hello)`}, + } + for _, tc := range tt { + p := "" + if len(tc.ps) > 0 { + p = tc.owner.String() + for i, psv := range tc.ps { + p += ":" + psv + } + } + p = strings.TrimSuffix(p, ":") + t.Run(p, func(t *testing.T) { + std.TestSetOrigCaller(tc.caller) + var act string + if len(tc.ps) > 0 && tc.ps[0] == "set" { + act = strings.TrimSpace(Set(tc.ps[1], tc.ps[2])) + } else if len(tc.ps) > 0 && tc.ps[0] == "remove" { + act = strings.TrimSpace(Remove(tc.ps[1])) + } else { + act = strings.TrimSpace(Render(p)) + } + if act != tc.exp { + t.Errorf("%v -> '%s', got '%s', wanted '%s'", tc.ps, p, act, tc.exp) + } + }) + } +}