Skip to content

feat(gno.land): add read-only metadata support to packages #3740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
96de36e
feat: add new `m_setmeta` message type
jeronimoalbi Sep 3, 2024
75bcf57
feat: add `setmeta` sub command to `gnokey maketx`
jeronimoalbi Sep 3, 2024
927a7ff
chore: remove redundant package validation
jeronimoalbi Sep 4, 2024
e3bac0b
feat: implement saving of metadata fields in a keeper store
jeronimoalbi Sep 5, 2024
a4a4d01
feat: add package metadata field query
jeronimoalbi Sep 6, 2024
68407e2
docs: update `gnokey` docs
jeronimoalbi Sep 6, 2024
c72c3a9
chore: remove redundant comments
jeronimoalbi Sep 9, 2024
2e2c2c9
Merge branch 'master' into devx/package-metadata
jeronimoalbi Feb 12, 2025
2531dc5
chore: remove integration tests for `maketx setmeta` command
jeronimoalbi Feb 12, 2025
04ef767
chore: remove `gnokey maketx setmeta` command
jeronimoalbi Feb 12, 2025
c7857ab
chore: remove support for handling `MsgSetMeta` messages
jeronimoalbi Feb 12, 2025
cb981a1
feat: add metadata field to `addpkg` message
jeronimoalbi Feb 12, 2025
9716907
test: fix broken unit test
jeronimoalbi Feb 12, 2025
8e7db8e
feat: change `maketex addpkg` command to support metadata fields
jeronimoalbi Feb 12, 2025
5d05415
chore: fix typo
jeronimoalbi Feb 12, 2025
ec2eedc
chore: fix CI issues
jeronimoalbi Feb 12, 2025
7f6078b
Merge branch 'master' into devx/package-metadata
jeronimoalbi Feb 12, 2025
6162dd9
chore: fix CI linting error
jeronimoalbi Feb 13, 2025
5d8758e
Merge branch 'master' into devx/package-metadata
jeronimoalbi Feb 13, 2025
7a03845
Merge branch 'master' into devx/package-metadata
jeronimoalbi Feb 17, 2025
773020b
Merge branch 'master' into devx/package-metadata
jeronimoalbi Mar 25, 2025
fc83728
chore: move metadata CLI parsing to a separate function
jeronimoalbi Mar 25, 2025
65d489e
feat: move metadata storage logic into a separate function
jeronimoalbi Mar 25, 2025
fb464eb
test: add txtar integration tests for gnokey metadata
jeronimoalbi Mar 25, 2025
6d1c7b2
test: add unit test for meta query
jeronimoalbi Mar 25, 2025
8f62f18
test: add metadata and tools unit tests to keeper add package tests
jeronimoalbi Mar 25, 2025
ffa9726
Merge branch 'master' into devx/package-metadata
jeronimoalbi Mar 25, 2025
bfb618b
test: add metadata unit tests for addpkg `ValidateBasic()`
jeronimoalbi Mar 25, 2025
cb4c02a
Merge branch 'master' into devx/package-metadata
jeronimoalbi Mar 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/users/interact-with-gnokey.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ Below is a list of queries a user can make with `gnokey`:
- `vm/qdoc` - Returns the JSON of the doc for a given pkgpath, suitable for printing
- `vm/qeval` - evaluates an expression in read-only mode on and returns the results
- `vm/qrender` - shorthand for evaluating `vm/qeval Render("")` for a given pkgpath
- `vm/qmeta` - returns the value for a package metadata field

Let's see how we can use them.

Expand Down Expand Up @@ -936,6 +937,16 @@ gnokey query vm/qrender --data "gno.land/r/demo/wugnot:balance/g125em6arxsnj49vx
To see how this was achieved, check out `wugnot`'s `Render()` function.
:::

## `vm/qmeta`

`vm/qmeta` allows us to read the metadata of a realm or package. For example:

```bash
gnokey query vm/qmeta --data "gno.land/r/demo/boards:FIELD_NAME"`
```

This command will return the value of the given metadata field encoded as base64.

### Gas parameters

When using `gnokey` to send transactions, you'll need to specify gas parameters:
Expand Down
27 changes: 27 additions & 0 deletions gno.land/pkg/integration/testdata/addpkg_metadata.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Start a new node
gnoland start

# Fail deploy when using an invalid metadata argument
! gnokey maketx addpkg -meta invalid -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stderr 'invalid metadata field format, expected field=value'

# Fail deploy when using an empty metadata field name value
! gnokey maketx addpkg -meta="=" -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stderr 'empty metadata field name'

# Successfully deploy a package with metadata
gnokey maketx addpkg -meta foo=bar -meta other=42 -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stdout 'OK!'

# Check "foo" metadata field
gnokey query vm/qmeta --data "gno.land/r/$test1_user_addr/realm:foo"
stdout 'data: YmFy'

# Check "baz" metadata field
gnokey query vm/qmeta --data "gno.land/r/$test1_user_addr/realm:other"
stdout 'data: NDI='

-- realm.gno --
package realm

func Render(string) string { return "" }
28 changes: 28 additions & 0 deletions gno.land/pkg/integration/testdata/addpkg_metadata_tools.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Start a new node
gnoland start

# Fail deploy when using invalid JSON for metadata tools field argument
! gnokey maketx addpkg -meta "tools=" -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stderr 'invalid tools field format'

# Fail deploy when using empty tools value for metadata tools field argument
! gnokey maketx addpkg -meta "tools={}" -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stderr 'tools list is empty'

# Fail deploy when using invalid tool name value for metadata tools field argument
# To pass tool name must have minimum 6 chars, lowercase alphanumeric with underscore
! gnokey maketx addpkg -meta "tools={\"tools\":[{\"name\":\"foo\",\"weight\":1}]}" -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stderr 'invalid tool name'

# Successfully deploy a package with tools metadata
gnokey maketx addpkg -meta "tools={\"tools\":[{\"name\":\"foobar\",\"weight\":1}]}" -pkgdir $WORK -pkgpath gno.land/r/$test1_user_addr/realm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1
stdout 'OK!'

# Check "tools" metadata field
gnokey query vm/qmeta --data "gno.land/r/$test1_user_addr/realm:tools"
stdout 'data: eyJ0b29scyI6W3sibmFtZSI6ImZvb2JhciIsIndlaWdodCI6MX1dfQ=='

-- realm.gno --
package realm

func Render(string) string { return "" }
51 changes: 48 additions & 3 deletions gno.land/pkg/keyscli/addpkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"strings"

"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
Expand All @@ -21,6 +22,7 @@ type MakeAddPkgCfg struct {
PkgPath string
PkgDir string
Deposit string
Meta commands.StringArr
}

func NewMakeAddPkgCmd(rootCfg *client.MakeTxCfg, io commands.IO) *commands.Command {
Expand Down Expand Up @@ -62,6 +64,12 @@ func (c *MakeAddPkgCfg) RegisterFlags(fs *flag.FlagSet) {
"",
"deposit coins",
)

fs.Var(
&c.Meta,
"meta",
"metadata fields (format: field=value)",
)
}

func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error {
Expand All @@ -82,6 +90,12 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error {
return flag.ErrHelp
}

// parse metadata fields
metadata, err := parseMetadataFields(cfg.Meta)
if err != nil {
return err
}

// read account pubkey.
nameOrBech32 := args[0]
kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.RootCfg.Home)
Expand Down Expand Up @@ -115,9 +129,10 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error {
}
// construct msg & tx and marshal.
msg := vm.MsgAddPackage{
Creator: creator,
Package: memPkg,
Deposit: deposit,
Creator: creator,
Package: memPkg,
Deposit: deposit,
Metadata: metadata,
}
tx := std.Tx{
Msgs: []std.Msg{msg},
Expand All @@ -136,3 +151,33 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error {
}
return nil
}

func parseMetadataFields(meta commands.StringArr) ([]*vm.MetaField, error) {
if len(meta) == 0 {
return nil, nil
}

metadata := make([]*vm.MetaField, len(meta))
for i, v := range meta {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 {
return nil, errors.New("invalid metadata field format, expected field=value")
}

name := strings.TrimSpace(parts[0])
if name == "" {
return nil, errors.New("empty metadata field name")
}

var value []byte
if strings.TrimSpace(parts[1]) != "" {
value = []byte(parts[1])
}

metadata[i] = &vm.MetaField{
Name: name,
Value: value,
}
}
return metadata, nil
}
6 changes: 6 additions & 0 deletions gno.land/pkg/sdk/vm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func (abciError) AssertABCIError() {}
// NOTE: these are meant to be used in conjunction with pkgs/errors.
type (
InvalidPkgPathError struct{ abciError }
InvalidPkgMetaError struct{ abciError }
NoRenderDeclError struct{ abciError }
PkgExistError struct{ abciError }
InvalidStmtError struct{ abciError }
Expand All @@ -28,6 +29,7 @@ type (
)

func (e InvalidPkgPathError) Error() string { return "invalid package path" }
func (e InvalidPkgMetaError) Error() string { return "invalid package metadata" }
func (e NoRenderDeclError) Error() string { return "render function not declared" }
func (e PkgExistError) Error() string { return "package already exists" }
func (e InvalidStmtError) Error() string { return "invalid statement" }
Expand All @@ -52,6 +54,10 @@ func ErrInvalidPkgPath(msg string) error {
return errors.Wrap(InvalidPkgPathError{}, msg)
}

func ErrInvalidPkgMeta(msg string) error {
return errors.Wrap(InvalidPkgMetaError{}, msg)
}

func ErrInvalidStmt(msg string) error {
return errors.Wrap(InvalidStmtError{}, msg)
}
Expand Down
20 changes: 20 additions & 0 deletions gno.land/pkg/sdk/vm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const (
QueryEval = "qeval"
QueryFile = "qfile"
QueryDoc = "qdoc"
QueryMeta = "qmeta"
)

func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery {
Expand All @@ -93,6 +94,8 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQ
res = vh.queryFile(ctx, req)
case QueryDoc:
res = vh.queryDoc(ctx, req)
case QueryMeta:
res = vh.queryMeta(ctx, req)
default:
return sdk.ABCIResponseQueryFromError(
std.ErrUnknownRequest(fmt.Sprintf(
Expand Down Expand Up @@ -199,6 +202,23 @@ func (vh vmHandler) queryDoc(ctx sdk.Context, req abci.RequestQuery) (res abci.R
return
}

// queryMeta returns the value for a package metadata field.
func (vh vmHandler) queryMeta(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) {
parts := strings.SplitN(string(req.Data), ":", 2)
if len(parts) != 2 {
panic("expected <pkgpath>:<field_name> syntax in query input data")
}

pkgPath, name := parts[0], parts[1]
result, err := vh.vm.QueryMeta(ctx, pkgPath, name)
if err != nil {
res = sdk.ABCIResponseQueryFromError(err)
return
}
res.Data = result
return
}

// ----------------------------------------
// misc

Expand Down
90 changes: 90 additions & 0 deletions gno.land/pkg/sdk/vm/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,93 @@ func Hello(msg string) (res string) { res = prefix+" "+msg; return }
})
}
}

func TestVmHandlerQuery_Meta(t *testing.T) {
tt := []struct {
name string
input []byte
meta []*MetaField
expectedResult string
expectedResultMatch string
expectedErrorMatch string
expectedPanicMatch string
}{
{
name: "success",
input: []byte("gno.land/r/foo:field"),
meta: []*MetaField{
{Name: "field", Value: []byte("bar")},
},
expectedResult: "YmFy",
},
{
name: "invalid query data format",
input: []byte("gno.land/r/foo"),
expectedPanicMatch: "expected <pkgpath>:<field_name> syntax in query input data",
},
{
name: "metadata field not found",
input: []byte("gno.land/r/foo:foobar"),
expectedErrorMatch: "metadata field for package gno.land/r/foo not found: foobar",
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
env := setupTestEnv()
ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
vmHandler := env.vmh

// Give "addr1" some gnots.
addr := crypto.AddressFromPreimage([]byte("addr1"))
acc := env.acck.NewAccountWithAddress(ctx, addr)
env.acck.SetAccount(ctx, acc)
env.bankk.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot"))
assert.True(t, env.bankk.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot")))

// Prepare add package message
files := []*gnovm.MemFile{
{Name: "foo.gno", Body: "package foo\nfunc Render(string) string{ return \"\"}\n"},
}
pkgPath := "gno.land/r/foo"
msg := NewMsgAddPackage(addr, pkgPath, files)
msg.Metadata = tc.meta

// Create test package.
err := env.vmk.AddPackage(ctx, msg)
assert.NoError(t, err)
env.vmk.CommitGnoTransactionStore(ctx)

// Prepare ABCI request
req := abci.RequestQuery{
Path: "vm/qmeta",
Data: tc.input,
}

defer func() {
if r := recover(); r != nil {
output := fmt.Sprintf("%v", r)
assert.Regexp(t, tc.expectedPanicMatch, output)
} else {
assert.Equal(t, tc.expectedPanicMatch, "", "should not panic")
}
}()

res := vmHandler.Query(env.ctx, req)

if tc.expectedPanicMatch == "" {
if tc.expectedErrorMatch == "" {
assert.True(t, res.IsOK(), "should not have error")

if tc.expectedResult != "" {
assert.Equal(t, tc.expectedResult, string(res.Data))
}
} else {
assert.False(t, res.IsOK(), "should have an error")
errmsg := res.Error.Error()
assert.Regexp(t, tc.expectedErrorMatch, errmsg)
}
}
})
}
}
Loading
Loading