diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index c37eda80bff..d99a161bcdf 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -16,15 +16,13 @@ var ( ) func init() { - var ( - // Example initial member set (just test addresses) - set = []membstore.Member{ - { - Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - VotingPower: 10, - }, - } - ) + // Example initial member set (just test addresses) + set := []membstore.Member{ + { + Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + VotingPower: 10, + }, + } // Set the member store members = membstore.NewMembStore(membstore.WithInitialMembers(set)) diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index 07d06bfe74d..e889dde4f48 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -126,3 +126,85 @@ func main() { // - #123: g12345678 (10) // - #123: g000000000 (10) // - #123: g000000000 (0) + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ValidatorAdded", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "addValidator" +// }, +// { +// "type": "ValidatorAdded", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "addValidator" +// }, +// { +// "type": "ValidatorRemoved", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "removeValidator" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 76e744a6fc8..bfd3f44f6dd 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -108,3 +108,67 @@ func main() { // ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao) // 13 Feb 2009 // + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 0cd3bce6f83..546213431e4 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -5,6 +5,7 @@ import ( "gno.land/p/demo/dao" "gno.land/p/demo/membstore" + "gno.land/r/gov/dao/bridge" govdao "gno.land/r/gov/dao/v2" ) @@ -34,7 +35,7 @@ func init() { Executor: govdao.NewMemberPropExecutor(memberFn), } - govdao.Propose(prop) + bridge.GovDAO().Propose(prop) } func main() { @@ -118,3 +119,67 @@ func main() { // // -- // 4 + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno new file mode 100644 index 00000000000..c90e76727da --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -0,0 +1,157 @@ +package main + +import ( + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" + govdaov2 "gno.land/r/gov/dao/v2" + "gno.land/r/sys/params" +) + +func init() { + mExec := params.NewStringPropExecutor("prop1.string", "value1") + comment := "setting prop1.string param" + prop := dao.ProposalRequest{ + Description: comment, + Executor: mExec, + } + id := bridge.GovDAO().Propose(prop) + println("new prop", id) +} + +func main() { + println("--") + println(govdaov2.Render("")) + println("--") + println(govdaov2.Render("0")) + println("--") + bridge.GovDAO().VoteOnProposal(0, "YES") + println("--") + println(govdaov2.Render("0")) + println("--") + bridge.GovDAO().ExecuteProposal(0) + println("--") + println(govdaov2.Render("0")) +} + +// Output: +// new prop 0 +// -- +// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// setting prop1.string param +// +// Status: active +// +// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%) +// +// Threshold met: false +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// setting prop1.string param +// +// Status: accepted +// +// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// +// Threshold met: true +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// setting prop1.string param +// +// Status: execution successful +// +// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%) +// +// Threshold met: true + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "set", +// "attrs": [ +// { +// "key": "k", +// "value": "prop1.string" +// } +// ], +// "pkg_path": "gno.land/r/sys/params", +// "func": "" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/sys/params/gno.mod b/examples/gno.land/r/sys/params/gno.mod new file mode 100644 index 00000000000..4b4c2bf790f --- /dev/null +++ b/examples/gno.land/r/sys/params/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/sys/params + +require ( + gno.land/p/demo/dao v0.0.0-latest + gno.land/r/gov/dao/bridge v0.0.0-latest +) diff --git a/examples/gno.land/r/sys/params/params.gno b/examples/gno.land/r/sys/params/params.gno new file mode 100644 index 00000000000..fa04c90de3f --- /dev/null +++ b/examples/gno.land/r/sys/params/params.gno @@ -0,0 +1,54 @@ +// Package params provides functions for creating parameter executors that +// interface with the Params Keeper. +// +// This package enables setting various parameter types (such as strings, +// integers, booleans, and byte slices) through the GovDAO proposal mechanism. +// Each function returns an executor that, when called, sets the specified +// parameter in the Params Keeper. +// +// The executors are designed to be used within governance proposals to modify +// parameters dynamically. The integration with the GovDAO allows for parameter +// changes to be proposed and executed in a controlled manner, ensuring that +// modifications are subject to governance processes. +// +// Example usage: +// +// executor := params.NewStringPropExecutor("exampleKey", "exampleValue") +// // This executor can be used in a governance proposal to set the parameter. +package params + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" +) + +func NewStringPropExecutor(key string, value string) dao.Executor { + return newPropExecutor(key, func() { std.SetParamString(key, value) }) +} + +func NewInt64PropExecutor(key string, value int64) dao.Executor { + return newPropExecutor(key, func() { std.SetParamInt64(key, value) }) +} + +func NewUint64PropExecutor(key string, value uint64) dao.Executor { + return newPropExecutor(key, func() { std.SetParamUint64(key, value) }) +} + +func NewBoolPropExecutor(key string, value bool) dao.Executor { + return newPropExecutor(key, func() { std.SetParamBool(key, value) }) +} + +func NewBytesPropExecutor(key string, value []byte) dao.Executor { + return newPropExecutor(key, func() { std.SetParamBytes(key, value) }) +} + +func newPropExecutor(key string, fn func()) dao.Executor { + callback := func() error { + fn() + std.Emit("set", "k", key) + return nil + } + return bridge.GovDAO().NewGovDAOExecutor(callback) +} diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno new file mode 100644 index 00000000000..eaa1ad039d3 --- /dev/null +++ b/examples/gno.land/r/sys/params/params_test.gno @@ -0,0 +1,15 @@ +package params + +import "testing" + +// Testing this package is limited because it only contains an `std.Set` method +// without a corresponding `std.Get` method. For comprehensive testing, refer to +// the tests located in the r/gov/dao/ directory, specifically in one of the +// propX_filetest.gno files. + +func TestNewStringPropExecutor(t *testing.T) { + executor := NewStringPropExecutor("foo", "bar") + if executor == nil { + t.Errorf("executor shouldn't be nil") + } +} diff --git a/gno.land/cmd/gnoland/testdata/genesis_params.txtar b/gno.land/cmd/gnoland/testdata/genesis_params.txtar new file mode 100644 index 00000000000..43ecd8ccacb --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/genesis_params.txtar @@ -0,0 +1,14 @@ +# test for https://github.com/gnolang/gno/pull/3003 + +gnoland start + +gnokey query params/vm/gno.land/r/sys/params.test.foo.string +stdout 'data: "bar"$' +gnokey query params/vm/gno.land/r/sys/params.test.foo.int64 +stdout 'data: "-1337"' +gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64 +stdout 'data: "42"' +gnokey query params/vm/gno.land/r/sys/params.test.foo.bool +stdout 'data: true' +# XXX: bytes + diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml new file mode 100644 index 00000000000..5f4d9c5015c --- /dev/null +++ b/gno.land/genesis/genesis_params.toml @@ -0,0 +1,29 @@ + +## gno.land +["gno.land/r/sys/params.sys"] + users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support. + # TODO: validators_pkgpath.string = "gno.land/r/sys/validators" + # TODO: rewards_pkgpath.string = "gno.land/r/sys/rewards" + # TODO: token_lock.bool = true + +## gnovm +["gno.land/r/sys/params.vm"] + # TODO: chain_domain.string = "gno.land" + # TODO: max_gas.int64 = 100_000_000 + # TODO: chain_tz.string = "UTC" + # TODO: default_storage_allowance.string = "" + +## tm2 +["gno.land/r/sys/params.tm2"] + +## misc +["gno.land/r/sys/params.misc"] + +## testing +# do not remove these lines. they are needed for a txtar integration test. +["gno.land/r/sys/params.test"] + foo.string = "bar" + foo.int64 = -1337 + foo.uint64 = 42 + foo.bool = true + #foo.bytes = todo diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index d29ae3fd181..e0c93f6194f 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -294,7 +294,7 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci return nil, fmt.Errorf("invalid AppState of type %T", appState) } - // Parse and set genesis state balances + // Apply genesis balances. for _, bal := range state.Balances { acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) cfg.acctKpr.SetAccount(ctx, acc) @@ -304,6 +304,12 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci } } + // Apply genesis params. + for _, param := range state.Params { + param.register(ctx, cfg.paramsKpr) + } + + // Replay genesis txs. txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) // Run genesis txs diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 5e4dcb2b449..999e04b2c4b 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -66,6 +66,13 @@ func TestNewAppWithOptions(t *testing.T) { }, }, }, + Params: []Param{ + {key: "foo", kind: "string", value: "hello"}, + {key: "foo", kind: "int64", value: int64(-42)}, + {key: "foo", kind: "uint64", value: uint64(1337)}, + {key: "foo", kind: "bool", value: true}, + {key: "foo", kind: "bytes", value: []byte{0x48, 0x69, 0x21}}, + }, }, }) require.True(t, resp.IsOK(), "InitChain response: %v", resp) @@ -87,6 +94,27 @@ func TestNewAppWithOptions(t *testing.T) { Tx: tx, }) require.True(t, dtxResp.IsOK(), "DeliverTx response: %v", dtxResp) + + cres := bapp.Commit() + require.NotNil(t, cres) + + tcs := []struct { + path string + expectedVal string + }{ + {"params/vm/foo.string", `"hello"`}, + {"params/vm/foo.int64", `"-42"`}, + {"params/vm/foo.uint64", `"1337"`}, + {"params/vm/foo.bool", `true`}, + {"params/vm/foo.bytes", `"SGkh"`}, // XXX: make this test more readable + } + for _, tc := range tcs { + qres := bapp.Query(abci.RequestQuery{ + Path: tc.path, + }) + require.True(t, qres.IsOK()) + assert.Equal(t, qres.Data, []byte(tc.expectedVal)) + } } func TestNewAppWithOptions_ErrNoDB(t *testing.T) { diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index 613fba51c37..ea692bcaf0d 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -13,12 +13,16 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/pelletier/go-toml" ) // LoadGenesisBalancesFile loads genesis balances from the provided file path. func LoadGenesisBalancesFile(path string) ([]Balance, error) { // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot - content := osm.MustReadFile(path) + content, err := osm.ReadFile(path) + if err != nil { + return nil, err + } lines := strings.Split(string(content), "\n") balances := make([]Balance, 0, len(lines)) @@ -58,12 +62,54 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { return balances, nil } +// LoadGenesisParamsFile loads genesis params from the provided file path. +func LoadGenesisParamsFile(path string) ([]Param, error) { + // each param is in the form: key.kind=value + content, err := osm.ReadFile(path) + if err != nil { + return nil, err + } + + m := map[string] /*category*/ map[string] /*key*/ map[string] /*kind*/ interface{} /*value*/ {} + err = toml.Unmarshal(content, &m) + if err != nil { + return nil, err + } + + params := make([]Param, 0) + for category, keys := range m { + for key, kinds := range keys { + for kind, val := range kinds { + param := Param{ + key: category + "." + key, + kind: kind, + } + switch kind { + case "uint64": // toml + param.value = uint64(val.(int64)) + default: + param.value = val + } + if err := param.Verify(); err != nil { + return nil, err + } + params = append(params, param) + } + } + } + + return params, nil +} + // LoadGenesisTxsFile loads genesis transactions from the provided file path. // XXX: Improve the way we generate and load this file func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]TxWithMetadata, error) { txs := make([]TxWithMetadata, 0) - txsBz := osm.MustReadFile(path) + txsBz, err := osm.ReadFile(path) + if err != nil { + return nil, err + } txsLines := strings.Split(string(txsBz), "\n") for _, txLine := range txsLines { if txLine == "" { diff --git a/gno.land/pkg/gnoland/param.go b/gno.land/pkg/gnoland/param.go new file mode 100644 index 00000000000..4c1e1190751 --- /dev/null +++ b/gno.land/pkg/gnoland/param.go @@ -0,0 +1,121 @@ +package gnoland + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/params" +) + +type Param struct { + key string + kind string + value interface{} +} + +func (p Param) Verify() error { + // XXX: validate + return nil +} + +const ( + ParamKindString = "string" + ParamKindInt64 = "int64" + ParamKindUint64 = "uint64" + ParamKindBool = "bool" + ParamKindBytes = "bytes" +) + +func (p *Param) Parse(entry string) error { + parts := strings.SplitN(strings.TrimSpace(entry), "=", 2) // .= + if len(parts) != 2 { + return fmt.Errorf("malformed entry: %q", entry) + } + + keyWithKind := parts[0] + rawValue := parts[1] + p.kind = keyWithKind[strings.LastIndex(keyWithKind, ".")+1:] + p.key = strings.TrimSuffix(keyWithKind, "."+p.kind) + switch p.kind { + case ParamKindString: + p.value = rawValue + case ParamKindInt64: + v, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + return err + } + p.value = v + case ParamKindBool: + v, err := strconv.ParseBool(rawValue) + if err != nil { + return err + } + p.value = v + case ParamKindUint64: + v, err := strconv.ParseUint(rawValue, 10, 64) + if err != nil { + return err + } + p.value = v + case ParamKindBytes: + v, err := hex.DecodeString(rawValue) + if err != nil { + return err + } + p.value = v + default: + return errors.New("unsupported param kind: " + p.kind + " (" + entry + ")") + } + + return p.Verify() +} + +func (p Param) String() string { + typedKey := p.key + "." + p.kind + switch p.kind { + case ParamKindString: + return fmt.Sprintf("%s=%s", typedKey, p.value) + case ParamKindInt64: + return fmt.Sprintf("%s=%d", typedKey, p.value) + case ParamKindUint64: + return fmt.Sprintf("%s=%d", typedKey, p.value) + case ParamKindBool: + if p.value.(bool) { + return fmt.Sprintf("%s=true", typedKey) + } + return fmt.Sprintf("%s=false", typedKey) + case ParamKindBytes: + return fmt.Sprintf("%s=%x", typedKey, p.value) + } + panic("invalid param kind:" + p.kind) +} + +func (p *Param) UnmarshalAmino(rep string) error { + return p.Parse(rep) +} + +func (p Param) MarshalAmino() (string, error) { + return p.String(), nil +} + +func (p Param) register(ctx sdk.Context, prk params.ParamsKeeperI) { + key := p.key + "." + p.kind + switch p.kind { + case ParamKindString: + prk.SetString(ctx, key, p.value.(string)) + case ParamKindInt64: + prk.SetInt64(ctx, key, p.value.(int64)) + case ParamKindUint64: + prk.SetUint64(ctx, key, p.value.(uint64)) + case ParamKindBool: + prk.SetBool(ctx, key, p.value.(bool)) + case ParamKindBytes: + prk.SetBytes(ctx, key, p.value.([]byte)) + default: + panic("invalid param kind: " + p.kind) + } +} diff --git a/gno.land/pkg/gnoland/param_test.go b/gno.land/pkg/gnoland/param_test.go new file mode 100644 index 00000000000..5d17aab40da --- /dev/null +++ b/gno.land/pkg/gnoland/param_test.go @@ -0,0 +1,41 @@ +package gnoland + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParam_Parse(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry string + expected Param + expectErr bool + }{ + {"valid string", "foo.string=hello", Param{key: "foo", kind: "string", value: "hello"}, false}, + {"valid int64", "foo.int64=-1337", Param{key: "foo", kind: "int64", value: int64(-1337)}, false}, + {"valid uint64", "foo.uint64=42", Param{key: "foo", kind: "uint64", value: uint64(42)}, false}, + {"valid bool", "foo.bool=true", Param{key: "foo", kind: "bool", value: true}, false}, + {"valid bytes", "foo.bytes=AAAA", Param{key: "foo", kind: "bytes", value: []byte{0xaa, 0xaa}}, false}, + {"invalid key", "invalidkey=foo", Param{}, true}, + {"invalid kind", "invalid.kind=foo", Param{}, true}, + {"invalid int64", "invalid.int64=foobar", Param{}, true}, + {"invalid uint64", "invalid.uint64=-42", Param{}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + param := Param{} + err := param.Parse(tc.entry) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, param) + } + }) + } +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 6bc4417d8e0..a5f76fdcef7 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -27,6 +27,7 @@ func ProtoGnoAccount() std.Account { type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []TxWithMetadata `json:"txs"` + Params []Param `json:"params"` } type TxWithMetadata struct { diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 56b298e4541..235b9581ae0 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -134,6 +134,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // This genesis will be use when node is started. genesis := &gnoland.GnoGenesisState{ Balances: LoadDefaultGenesisBalanceFile(t, gnoRootDir), + Params: LoadDefaultGenesisParamFile(t, gnoRootDir), Txs: []gnoland.TxWithMetadata{}, } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index ef7e05d0787..7e34049d352 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -59,6 +59,7 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + params := LoadDefaultGenesisParamFile(t, gnoroot) balances := LoadDefaultGenesisBalanceFile(t, gnoroot) txs := make([]gnoland.TxWithMetadata, 0) txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) @@ -67,6 +68,7 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW cfg.Genesis.AppState = gnoland.GnoGenesisState{ Balances: balances, Txs: txs, + Params: params, } return cfg, creator @@ -118,10 +120,11 @@ func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey Balances: []gnoland.Balance{ { Address: crypto.MustAddressFromString(DefaultAccount_Address), - Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), + Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), }, }, - Txs: []gnoland.TxWithMetadata{}, + Txs: []gnoland.TxWithMetadata{}, + Params: []gnoland.Param{}, }, } } @@ -147,6 +150,16 @@ func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balanc return genesisBalances } +// LoadDefaultGenesisParamFile loads the default genesis balance file for testing. +func LoadDefaultGenesisParamFile(t TestingTS, gnoroot string) []gnoland.Param { + paramFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_params.toml") + + genesisParams, err := gnoland.LoadGenesisParamsFile(paramFile) + require.NoError(t, err) + + return genesisParams +} + // LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing. func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []gnoland.TxWithMetadata { txsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl") diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index ff924610627..3a11d97c235 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -75,7 +75,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(93825), gasDeliver) + assert.Equal(t, int64(92825), gasDeliver) } // Enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 5921e3eb3bb..5fa2075b8f7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -228,15 +228,15 @@ func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore // Namespace can be either a user or crypto address. var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) -const ( - sysUsersPkgParamKey = "vm/gno.land/r/sys/params.string" - sysUsersPkgDefault = "gno.land/r/sys/users" -) +const sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { - sysUsersPkg := sysUsersPkgDefault - vm.prmk.GetString(ctx, sysUsersPkgParamKey, &sysUsersPkg) + var sysUsersPkg string + vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) + if sysUsersPkg == "" { + return nil + } store := vm.getGnoTransactionStore(ctx) diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 98e54114af9..98dbab6ac0e 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -58,6 +58,7 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(send) // >= send. banker := newTestBanker(pkgAddr.Bech32(), pkgCoins) + params := newTestParams() ctx := stdlibs.ExecContext{ ChainID: "dev", Height: 123, @@ -68,6 +69,7 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { OrigSend: send, OrigSendSpent: new(std.Coins), Banker: banker, + Params: params, EventLogger: sdk.NewEventLogger(), } return &teststd.TestExecContext{ @@ -632,6 +634,20 @@ func trimTrailingSpaces(result string) string { return strings.Join(lines, "\n") } +// ---------------------------------------- +// testParams +type testParams struct{} + +func newTestParams() *testParams { + return &testParams{} +} + +func (tp *testParams) SetBool(key string, val bool) { /* noop */ } +func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ } +func (tp *testParams) SetInt64(key string, val int64) { /* noop */ } +func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ } +func (tp *testParams) SetString(key string, val string) { /* noop */ } + // ---------------------------------------- // testBanker diff --git a/gnovm/tests/files/std12_stdlibs.gno b/gnovm/tests/files/std12_stdlibs.gno new file mode 100644 index 00000000000..a06fd841f91 --- /dev/null +++ b/gnovm/tests/files/std12_stdlibs.gno @@ -0,0 +1,15 @@ +package main + +import "std" + +func main() { + std.SetParamString("foo.string", "hello") + std.SetParamInt64("bar.int64", -12345) + std.SetParamUint64("baz.uint64", 12345) + std.SetParamBool("oof.bool", true) + std.SetParamBytes("rab.bytes", []byte("world")) + println("done") +} + +// Output: +// done diff --git a/tm2/pkg/sdk/params/handler_test.go b/tm2/pkg/sdk/params/handler_test.go index 1fff5d007d3..071eb12b52b 100644 --- a/tm2/pkg/sdk/params/handler_test.go +++ b/tm2/pkg/sdk/params/handler_test.go @@ -4,12 +4,12 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/sdk" tu "github.com/gnolang/gno/tm2/pkg/sdk/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestInvalidMsg(t *testing.T) { @@ -27,21 +27,42 @@ func TestQuery(t *testing.T) { env := setupTestEnv() h := NewHandler(env.keeper) - req := abci.RequestQuery{ - Path: "params/params_test/foo/bar.string", + tcs := []struct { + path string + expected string + }{ + {path: "params/params_test/foo/bar.string", expected: `"baz"`}, + {path: "params/params_test/foo/bar.int64", expected: `"-12345"`}, + {path: "params/params_test/foo/bar.uint64", expected: `"4242"`}, + {path: "params/params_test/foo/bar.bool", expected: "true"}, + {path: "params/params_test/foo/bar.bytes", expected: `"YmF6"`}, } - res := h.Query(env.ctx, req) - require.Nil(t, res.Error) - require.NotNil(t, res) - require.Nil(t, res.Data) + for _, tc := range tcs { + req := abci.RequestQuery{ + Path: tc.path, + } + res := h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + require.Nil(t, res.Data) + } env.keeper.SetString(env.ctx, "foo/bar.string", "baz") + env.keeper.SetInt64(env.ctx, "foo/bar.int64", -12345) + env.keeper.SetUint64(env.ctx, "foo/bar.uint64", 4242) + env.keeper.SetBool(env.ctx, "foo/bar.bool", true) + env.keeper.SetBytes(env.ctx, "foo/bar.bytes", []byte("baz")) - res = h.Query(env.ctx, req) - require.Nil(t, res.Error) - require.NotNil(t, res) - require.Equal(t, string(res.Data), `"baz"`) + for _, tc := range tcs { + req := abci.RequestQuery{ + Path: tc.path, + } + res := h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + assert.Equal(t, string(res.Data), tc.expected) + } } func TestQuerierRouteNotFound(t *testing.T) { diff --git a/tm2/pkg/sdk/params/keeper.go b/tm2/pkg/sdk/params/keeper.go index ffeb1775acb..523e8d54f69 100644 --- a/tm2/pkg/sdk/params/keeper.go +++ b/tm2/pkg/sdk/params/keeper.go @@ -152,6 +152,6 @@ func checkSuffix(key, expectedSuffix string) { // XXX: additional sanity checks? ) if noSuffix || noName { - panic(`key should be like "` + expectedSuffix + `"`) + panic(`key should be like "` + expectedSuffix + `" (` + key + `)`) } } diff --git a/tm2/pkg/sdk/params/keeper_test.go b/tm2/pkg/sdk/params/keeper_test.go index 45a97ae44ad..832d16229ee 100644 --- a/tm2/pkg/sdk/params/keeper_test.go +++ b/tm2/pkg/sdk/params/keeper_test.go @@ -78,11 +78,11 @@ func TestKeeper(t *testing.T) { require.Equal(t, param5, []byte("bye")) // invalid sets - require.PanicsWithValue(t, `key should be like ".string"`, func() { keeper.SetString(ctx, "invalid.int64", "hello") }) - require.PanicsWithValue(t, `key should be like ".int64"`, func() { keeper.SetInt64(ctx, "invalid.string", int64(42)) }) - require.PanicsWithValue(t, `key should be like ".uint64"`, func() { keeper.SetUint64(ctx, "invalid.int64", uint64(42)) }) - require.PanicsWithValue(t, `key should be like ".bool"`, func() { keeper.SetBool(ctx, "invalid.int64", true) }) - require.PanicsWithValue(t, `key should be like ".bytes"`, func() { keeper.SetBytes(ctx, "invalid.int64", []byte("hello")) }) + require.PanicsWithValue(t, `key should be like ".string" (invalid.int64)`, func() { keeper.SetString(ctx, "invalid.int64", "hello") }) + require.PanicsWithValue(t, `key should be like ".int64" (invalid.string)`, func() { keeper.SetInt64(ctx, "invalid.string", int64(42)) }) + require.PanicsWithValue(t, `key should be like ".uint64" (invalid.int64)`, func() { keeper.SetUint64(ctx, "invalid.int64", uint64(42)) }) + require.PanicsWithValue(t, `key should be like ".bool" (invalid.int64)`, func() { keeper.SetBool(ctx, "invalid.int64", true) }) + require.PanicsWithValue(t, `key should be like ".bytes" (invalid.int64)`, func() { keeper.SetBytes(ctx, "invalid.int64", []byte("hello")) }) } // adapted from TestKeeperSubspace from Cosmos SDK, but adapted to a subspace-less Keeper.