Skip to content

Commit

Permalink
feat: add realm r/demo/keystore (#958)
Browse files Browse the repository at this point in the history
This realm implements the `avl.Tree` as a user-addressable key-store. I
wanted to have this realm to possibly use `gno.land` as a database where
writes may cost gnots but reads are free because they can be performed
through `Render` (related discussion:
#947). As a nice additional feature
it provides a UI for the database (through `gno.land`).

Data can be set/removed if the caller is the same as the owner of the
key-store (i.e. only owner's have write-access). For example:

```bash
# Set key,value for user at YOURKEY (write-protected)
gnokey maketx call --pkgpath "gno.land/r/demo/keystore" \
--func "Set" --args "hello" --args "world" \
--gas-fee "1000000ugnot" --gas-wanted "8000000" \
--broadcast --chainid dev --remote localhost:26657 YOURKEY
```

```bash
# Remove key for user at YOURKEY (write-protected)
gnokey maketx call --pkgpath "gno.land/r/demo/keystore" \
--func "Remove" --args "hello"  \
--gas-fee "1000000ugnot" --gas-wanted "8000000" \
--broadcast --chainid dev --remote localhost:26657 YOURKEY
```

```bash
# Get key
gnokey maketx call --pkgpath "gno.land/r/demo/keystore" \
--func "Get" --args "hello"  \
--gas-fee "1000000ugnot" --gas-wanted "8000000" \
--broadcast --chainid dev --remote localhost:26657 YOURKEY
```

All data is public and accessible as read-only from any user, as well as
from gno.land.

The main page lists all the available databases by owner
(`/r/demo/keystore`):


![image](https://github.com/gnolang/gno/assets/6550035/3e21827c-0a22-47eb-bf42-53f6ee005a7e)

Clicking on a user will show all the keys in their key-store
(`/r/demo/keystore:USER`):


![image](https://github.com/gnolang/gno/assets/6550035/7d4b3d58-20ad-47fc-8e41-095e111f5f30)

And the key values are accessible as well
(`r/demo/keystore:USER:get:KEY`)


![image](https://github.com/gnolang/gno/assets/6550035/888a8ddf-6044-42d4-aed6-ffc3989def35)





## Contributors Checklist

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
- [x] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](../.benchmarks/README.md).

## Maintainers Checklist

- [ ] Checked that the author followed the guidelines in
`CONTRIBUTING.md`
- [ ] Checked the conventional-commit (especially PR title and verb,
presence of `BREAKING CHANGE:` in the body)
- [ ] Ensured that this PR is not a significant change or confirmed that
the review/consideration process was appropriate for the change

---------

Co-authored-by: Manfred Touron <[email protected]>
  • Loading branch information
schollz and moul authored Sep 16, 2023
1 parent d5f14ff commit ae577ec
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 0 deletions.
6 changes: 6 additions & 0 deletions examples/gno.land/r/demo/keystore/gno.mod
Original file line number Diff line number Diff line change
@@ -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
)
184 changes: 184 additions & 0 deletions examples/gno.land/r/demo/keystore/keystore.gno
Original file line number Diff line number Diff line change
@@ -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
}
78 changes: 78 additions & 0 deletions examples/gno.land/r/demo/keystore/keystore_test.gno
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit ae577ec

Please sign in to comment.