diff --git a/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/gnomod.toml b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/gnomod.toml new file mode 100644 index 00000000000..f30de83a0fc --- /dev/null +++ b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft" +gno = "0.9" \ No newline at end of file diff --git a/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/svg_generator.gno b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/svg_generator.gno new file mode 100644 index 00000000000..bd591f77162 --- /dev/null +++ b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/svg_generator.gno @@ -0,0 +1,69 @@ +package tnft + +import ( + b64 "encoding/base64" + "math/rand" + "strings" + + "gno.land/p/nt/ufmt" +) + +var baseTempalte = ` + + + + + + + + + + + + + + + + + + + + + + + +` + +// charset contains valid hex digits for color generation. +const charset = "0123456789ABCDEF" + +// genImageURI generates a base64-encoded SVG image URI with random gradient colors. +func genImageURI(r *rand.Rand) string { + imageRaw := genImageRaw(r) + sEnc := b64.StdEncoding.EncodeToString([]byte(imageRaw)) + + return "data:image/svg+xml;base64," + sEnc +} + +// genImageRaw generates an SVG image with random gradient parameters. +func genImageRaw(r *rand.Rand) string { + x1 := 7 + r.Uint64N(7) + y1 := 7 + r.Uint64N(7) + + x2 := 121 + r.Uint64N(6) + y2 := 121 + r.Uint64N(6) + + var color1, color2 strings.Builder + color1.Grow(7) + color2.Grow(7) + color1.WriteByte('#') + color2.WriteByte('#') + + for i := 0; i < 6; i++ { + color1.WriteByte(charset[r.IntN(16)]) + color2.WriteByte(charset[r.IntN(16)]) + } + + randImage := ufmt.Sprintf(baseTempalte, x1, y1, x2, y2, color1.String(), color2.String()) + return randImage +} diff --git a/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/tnft.gno b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/tnft.gno new file mode 100644 index 00000000000..9b61acc3790 --- /dev/null +++ b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/tnft.gno @@ -0,0 +1,56 @@ +package tnft + +import ( + "chain/runtime" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" +) + +var ( + nft = grc721.NewBasicNFT("Test NFT", "TNFT") + owner = ownable.NewWithAddress(runtime.PreviousRealm().Address()) +) + +// BulkMint mints a bulk of tokens to the specified address. +// For testing purposes, the bulk size is set to bulkSize. +// This function is not intended to be used in production. +func BulkMint(cur realm, to address, bulkSize int64) { + for i := int64(0); i < bulkSize; i++ { + nextTokenId := nft.TokenCount() + i + 1 + tid := grc721.TokenID(ufmt.Sprintf("%d", nextTokenId)) + + err := nft.Mint(to, tid) + if err != nil { + panic(err) + } + + tokenURI := genImageURI(generateRandInstance()) + err = setTokenURI(tid, grc721.TokenURI(tokenURI)) + if err != nil { + panic(err) + } + } +} + +// BulkGenerateTokenURI generates a bulk of token URIs. +// For testing purposes, the bulk size is set to bulkSize. +// This function is not intended to be used in production. +func BulkGenerateTokenURI(bulkSize int64) bool { + for i := int64(0); i < bulkSize; i++ { + genImageURI(generateRandInstance()) + } + + return true +} + +// setTokenURI sets the metadata URI for a specific token ID. +func setTokenURI(tid grc721.TokenID, tURI grc721.TokenURI) error { + _, err := nft.SetTokenURI(tid, tURI) + if err != nil { + return ufmt.Errorf("token id (%s) || %s", tid, err.Error()) + } + + return nil +} diff --git a/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/utils.gno b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/utils.gno new file mode 100644 index 00000000000..08b9b2455f6 --- /dev/null +++ b/examples/gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/tnft/utils.gno @@ -0,0 +1,22 @@ +package tnft + +import ( + "math/rand" + "time" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ufmt" +) + +// tid converts uint64 to grc721.TokenID. +func tid(id uint64) grc721.TokenID { + return grc721.TokenID(ufmt.Sprintf("%d", id)) +} + +// generateRandInstance generates a new random instance. +func generateRandInstance() *rand.Rand { + seed1 := time.Now().Unix() + nft.TokenCount() + seed2 := time.Now().UnixNano() + nft.TokenCount() + pcg := rand.NewPCG(uint64(seed1), uint64(seed2)) + return rand.New(pcg) +} diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 00adace7435..91926e320d9 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" + apprpc "github.com/gnolang/gno/gno.land/pkg/gnoland/rpc" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gnovm/pkg/gnoenv" @@ -17,6 +18,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" signer "github.com/gnolang/gno/tm2/pkg/bft/privval/signer/local" + rpcserver "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -261,6 +263,15 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { return fmt.Errorf("unable to start the Gnoland node, %w", err) } + var ( + appRPCAddr = "tcp://0.0.0.0:26660" // TODO: make configurable + appRPC = apprpc.NewServer(cfg.LocalApp.(apprpc.Application), logger) + ) + + if err = appRPC.Serve(ctx, appRPCAddr, rpcserver.DefaultConfig()); err != nil { + return fmt.Errorf("unable to start app RPC server: %w", err) + } + // Wait for the exit signal <-ctx.Done() diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index af8b35e4179..2b5f82be692 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -34,6 +34,23 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/types" ) +// App wraps the TM2 base-app, and the Gnoland keepers +type App struct { + *sdk.BaseApp + + vmKeeper vm.VMKeeperI +} + +// NewQueryContext creates a new app query context (read-only) +func (a *App) NewQueryContext(h int64) (sdk.Context, error) { + return a.BaseApp.NewQueryContext(h) +} + +// VMKeeper returns the VM keeper associated with the app +func (a *App) VMKeeper() vm.VMKeeperI { + return a.vmKeeper +} + // AppOptions contains the options to create the gno.land ABCI application. type AppOptions struct { DB dbm.DB // required @@ -208,7 +225,13 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { vmk.Initialize(cfg.Logger, ms) ms.MultiWrite() // XXX why was't this needed? - return baseApp, nil + // Wrap the app + app := &App{ + BaseApp: baseApp, + vmKeeper: vmk, + } + + return app, nil } // GenesisAppConfig wraps the most important diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index e2c76a72e00..070295d955a 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/gnolang/gno/gno.land/pkg/gnoland/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -211,28 +212,28 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper *ptr++ } } - mock := &mockVMKeeper{ - makeGnoTransactionStoreFn: func(ctx sdk.Context) sdk.Context { + mockKeeper := &mock.VMKeeper{ + MakeGnoTransactionStoreFn: func(ctx sdk.Context) sdk.Context { makeCalls++ assert.False(t, containsGnoStore(ctx), "should not already contain gno store") return ctx.WithContext(context.WithValue(ctx.Context(), gnoStoreKey, gnoStoreValue)) }, - commitGnoTransactionStoreFn: func(ctx sdk.Context) { + CommitGnoTransactionStoreFn: func(ctx sdk.Context) { commitCalls++ assert.True(t, containsGnoStore(ctx), "should contain gno store") }, - loadStdlibFn: loadStdlib(&loadStdlibCalls), - loadStdlibCachedFn: loadStdlib(&loadStdlibCachedCalls), + LoadStdlibFn: loadStdlib(&loadStdlibCalls), + LoadStdlibCachedFn: loadStdlib(&loadStdlibCachedCalls), } // call initchainer cfg := InitChainerConfig{ StdlibDir: stdlibDir, - vmk: mock, - acck: &mockAuthKeeper{}, - bankk: &mockBankKeeper{}, - prmk: &mockParamsKeeper{}, - gpk: &mockGasPriceKeeper{}, + vmk: mockKeeper, + acck: &mock.AuthKeeper{}, + bankk: &mock.BankKeeper{}, + prmk: &mock.ParamsKeeper{}, + gpk: &mock.GasPriceKeeper{}, CacheStdlibLoad: cached, } @@ -509,14 +510,14 @@ func TestEndBlocker(t *testing.T) { return builder.String() } - newCommonEvSwitch := func() *mockEventSwitch { + newCommonEvSwitch := func() *mock.EventSwitch { var cb events.EventCallback - return &mockEventSwitch{ - addListenerFn: func(_ string, callback events.EventCallback) { + return &mock.EventSwitch{ + AddListenerFn: func(_ string, callback events.EventCallback) { cb = callback }, - fireEventFn: func(event events.Event) { + FireEventFn: func(event events.Event) { cb(event) }, } @@ -530,10 +531,10 @@ func TestEndBlocker(t *testing.T) { } // Create the collector - c := newCollector[validatorUpdate](&mockEventSwitch{}, noFilter) + c := newCollector[validatorUpdate](&mock.EventSwitch{}, noFilter) // Create the EndBlocker - eb := EndBlocker(c, nil, nil, nil, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, nil, &mock.EndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ @@ -558,8 +559,8 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch = newCommonEvSwitch() - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { + mockVMKeeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { vmCalled = true require.Equal(t, valRealm, pkgPath) @@ -577,7 +578,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(chain.Event{}) // Create the EndBlocker - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mock.EndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ @@ -605,8 +606,8 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch = newCommonEvSwitch() - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { + mockVMKeeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { vmCalled = true require.Equal(t, valRealm, pkgPath) @@ -624,7 +625,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(chain.Event{}) // Create the EndBlocker - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mock.EndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ @@ -648,8 +649,8 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch = newCommonEvSwitch() - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { + mockVMKeeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { require.Equal(t, valRealm, pkgPath) require.NotEmpty(t, expr) @@ -696,7 +697,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(txEvent) // Create the EndBlocker - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mock.EndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ @@ -737,8 +738,8 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch = newCommonEvSwitch() - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { + mockVMKeeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { require.Equal(t, valRealm, pkgPath) require.NotEmpty(t, expr) @@ -770,7 +771,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) mockEventSwitch.FireEvent(txEvent) - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mock.EndBlockerApp{}) res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, @@ -803,8 +804,8 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch = newCommonEvSwitch() - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { + mockVMKeeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { require.Equal(t, valRealm, pkgPath) require.NotEmpty(t, expr) @@ -835,7 +836,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) mockEventSwitch.FireEvent(txEvent) - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mock.EndBlockerApp{}) res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ PubKeyTypeURLs: []string{"/tm.PubKeySecp256k1"}, @@ -864,8 +865,8 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch = newCommonEvSwitch() - mockVMKeeper = &mockVMKeeper{ - queryFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { + mockVMKeeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, pkgPath, expr string) (string, error) { require.Equal(t, valRealm, pkgPath) require.NotEmpty(t, expr) @@ -890,7 +891,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, validatorEventFilter) mockEventSwitch.FireEvent(txEvent) - eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mock.EndBlockerApp{}) res := eb(sdk.Context{}.WithConsensusParams(&abci.ConsensusParams{ Validator: &abci.ValidatorParams{ PubKeyTypeURLs: []string{"/tm.PubKeyEd25519"}, diff --git a/gno.land/pkg/gnoland/mock/mock.go b/gno.land/pkg/gnoland/mock/mock.go new file mode 100644 index 00000000000..be8379f7b62 --- /dev/null +++ b/gno.land/pkg/gnoland/mock/mock.go @@ -0,0 +1,270 @@ +package mock + +import ( + "log/slog" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/doc" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + + "github.com/gnolang/gno/tm2/pkg/service" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type ( + FireEventDelegate func(events.Event) + AddListenerDelegate func(string, events.EventCallback) + RemoveListenerDelegate func(string) +) + +type EventSwitch struct { + service.BaseService + + FireEventFn FireEventDelegate + AddListenerFn AddListenerDelegate + RemoveListenerFn RemoveListenerDelegate +} + +func (m *EventSwitch) FireEvent(ev events.Event) { + if m.FireEventFn != nil { + m.FireEventFn(ev) + } +} + +func (m *EventSwitch) AddListener( + listenerID string, + cb events.EventCallback, +) { + if m.AddListenerFn != nil { + m.AddListenerFn(listenerID, cb) + } +} + +func (m *EventSwitch) RemoveListener(listenerID string) { + if m.RemoveListenerFn != nil { + m.RemoveListenerFn(listenerID) + } +} + +type VMKeeper struct { + AddPackageFn func(sdk.Context, vm.MsgAddPackage) error + CallFn func(sdk.Context, vm.MsgCall) (string, error) + QueryEvalFn func(sdk.Context, string, string) (string, error) + QueryFuncsFn func(sdk.Context, string) (vm.FunctionSignatures, error) + QueryPathsFn func(sdk.Context, string, int) ([]string, error) + QueryFileFn func(sdk.Context, string) (string, error) + QueryDocFn func(sdk.Context, string) (*doc.JSONDocumentation, error) + QueryStorageFn func(sdk.Context, string) (string, error) + RunFn func(sdk.Context, vm.MsgRun) (string, error) + LoadStdlibFn func(sdk.Context, string) + LoadStdlibCachedFn func(sdk.Context, string) + MakeGnoTransactionStoreFn func(sdk.Context) sdk.Context + CommitGnoTransactionStoreFn func(sdk.Context) +} + +func (m *VMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { + if m.AddPackageFn != nil { + return m.AddPackageFn(ctx, msg) + } + + return nil +} + +func (m *VMKeeper) Call(ctx sdk.Context, msg vm.MsgCall) (res string, err error) { + if m.CallFn != nil { + return m.CallFn(ctx, msg) + } + + return "", nil +} + +func (m *VMKeeper) QueryEval(ctx sdk.Context, pkgPath, expr string) (res string, err error) { + if m.QueryEvalFn != nil { + return m.QueryEvalFn(ctx, pkgPath, expr) + } + + return "", nil +} + +func (m *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (vm.FunctionSignatures, error) { + if m.QueryFuncsFn != nil { + return m.QueryFuncsFn(ctx, pkgPath) + } + + return vm.FunctionSignatures{}, nil +} + +func (m *VMKeeper) QueryPaths(ctx sdk.Context, target string, limit int) ([]string, error) { + if m.QueryPathsFn != nil { + return m.QueryPathsFn(ctx, target, limit) + } + + return nil, nil +} + +func (m *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (string, error) { + if m.QueryFileFn != nil { + return m.QueryFileFn(ctx, filepath) + } + + return "", nil +} + +func (m *VMKeeper) QueryDoc(ctx sdk.Context, pkgPath string) (*doc.JSONDocumentation, error) { + if m.QueryDocFn != nil { + return m.QueryDocFn(ctx, pkgPath) + } + + return nil, nil +} + +func (m *VMKeeper) QueryStorage(ctx sdk.Context, pkgPath string) (string, error) { + if m.QueryStorageFn != nil { + return m.QueryStorageFn(ctx, pkgPath) + } + + return "", nil +} + +func (m *VMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err error) { + if m.RunFn != nil { + return m.RunFn(ctx, msg) + } + + return "", nil +} + +func (m *VMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { + if m.LoadStdlibFn != nil { + m.LoadStdlibFn(ctx, stdlibDir) + } +} + +func (m *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { + if m.LoadStdlibCachedFn != nil { + m.LoadStdlibCachedFn(ctx, stdlibDir) + } +} + +func (m *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + if m.MakeGnoTransactionStoreFn != nil { + return m.MakeGnoTransactionStoreFn(ctx) + } + return ctx +} + +func (m *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { + if m.CommitGnoTransactionStoreFn != nil { + m.CommitGnoTransactionStoreFn(ctx) + } +} + +func (m *VMKeeper) InitGenesis(ctx sdk.Context, gs vm.GenesisState) {} + +type BankKeeper struct{} + +func (m *BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []bank.Input, outputs []bank.Output) error { + return nil +} + +func (m *BankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *BankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *BankKeeper) SubtractCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { + return nil, nil +} + +func (m *BankKeeper) AddCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { + return nil, nil +} + +func (m *BankKeeper) InitGenesis(ctx sdk.Context, data bank.GenesisState) {} +func (m *BankKeeper) GetParams(ctx sdk.Context) bank.Params { return bank.Params{} } +func (m *BankKeeper) GetCoins(ctx sdk.Context, addr crypto.Address) std.Coins { return nil } +func (m *BankKeeper) SetCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *BankKeeper) HasCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) bool { + return true +} + +type AuthKeeper struct{} + +func (m *AuthKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) std.Account { + return nil +} +func (m *AuthKeeper) GetAccount(ctx sdk.Context, addr crypto.Address) std.Account { return nil } +func (m *AuthKeeper) GetAllAccounts(ctx sdk.Context) []std.Account { return nil } +func (m *AuthKeeper) SetAccount(ctx sdk.Context, acc std.Account) {} +func (m *AuthKeeper) IterateAccounts(ctx sdk.Context, process func(std.Account) bool) {} +func (m *AuthKeeper) InitGenesis(ctx sdk.Context, data auth.GenesisState) {} +func (m *AuthKeeper) GetParams(ctx sdk.Context) auth.Params { return auth.Params{} } + +type ParamsKeeper struct{} + +func (m *ParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) {} +func (m *ParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) {} +func (m *ParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) {} +func (m *ParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) {} +func (m *ParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) {} +func (m *ParamsKeeper) GetStrings(ctx sdk.Context, key string, ptr *[]string) {} + +func (m *ParamsKeeper) SetString(ctx sdk.Context, key string, value string) {} +func (m *ParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) {} +func (m *ParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) {} +func (m *ParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) {} +func (m *ParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) {} +func (m *ParamsKeeper) SetStrings(ctx sdk.Context, key string, value []string) {} + +func (m *ParamsKeeper) Has(ctx sdk.Context, key string) bool { return false } +func (m *ParamsKeeper) GetRaw(ctx sdk.Context, key string) []byte { return nil } +func (m *ParamsKeeper) SetRaw(ctx sdk.Context, key string, value []byte) {} + +func (m *ParamsKeeper) GetStruct(ctx sdk.Context, key string, strctPtr any) {} +func (m *ParamsKeeper) SetStruct(ctx sdk.Context, key string, strct any) {} + +func (m *ParamsKeeper) GetAny(ctx sdk.Context, key string) any { return nil } +func (m *ParamsKeeper) SetAny(ctx sdk.Context, key string, value any) {} + +type GasPriceKeeper struct{} + +func (m *GasPriceKeeper) LastGasPrice(ctx sdk.Context) std.GasPrice { return std.GasPrice{} } +func (m *GasPriceKeeper) SetGasPrice(ctx sdk.Context, gp std.GasPrice) {} +func (m *GasPriceKeeper) UpdateGasPrice(ctx sdk.Context) {} + +type ( + LastBlockHeightDelegate func() int64 + LoggerDelegate func() *slog.Logger +) + +type EndBlockerApp struct { + LastBlockHeightFn LastBlockHeightDelegate + LoggerFn LoggerDelegate +} + +func (m *EndBlockerApp) LastBlockHeight() int64 { + if m.LastBlockHeightFn != nil { + return m.LastBlockHeightFn() + } + + return 0 +} + +func (m *EndBlockerApp) Logger() *slog.Logger { + if m.LoggerFn != nil { + return m.LoggerFn() + } + + return log.NewNoopLogger() +} diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go deleted file mode 100644 index acf42e9921c..00000000000 --- a/gno.land/pkg/gnoland/mock_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package gnoland - -import ( - "log/slog" - - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/events" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/sdk" - "github.com/gnolang/gno/tm2/pkg/sdk/auth" - "github.com/gnolang/gno/tm2/pkg/sdk/bank" - - "github.com/gnolang/gno/tm2/pkg/service" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type ( - fireEventDelegate func(events.Event) - addListenerDelegate func(string, events.EventCallback) - removeListenerDelegate func(string) -) - -type mockEventSwitch struct { - service.BaseService - - fireEventFn fireEventDelegate - addListenerFn addListenerDelegate - removeListenerFn removeListenerDelegate -} - -func (m *mockEventSwitch) FireEvent(ev events.Event) { - if m.fireEventFn != nil { - m.fireEventFn(ev) - } -} - -func (m *mockEventSwitch) AddListener( - listenerID string, - cb events.EventCallback, -) { - if m.addListenerFn != nil { - m.addListenerFn(listenerID, cb) - } -} - -func (m *mockEventSwitch) RemoveListener(listenerID string) { - if m.removeListenerFn != nil { - m.removeListenerFn(listenerID) - } -} - -type mockVMKeeper struct { - addPackageFn func(sdk.Context, vm.MsgAddPackage) error - callFn func(sdk.Context, vm.MsgCall) (string, error) - queryFn func(sdk.Context, string, string) (string, error) - runFn func(sdk.Context, vm.MsgRun) (string, error) - loadStdlibFn func(sdk.Context, string) - loadStdlibCachedFn func(sdk.Context, string) - makeGnoTransactionStoreFn func(ctx sdk.Context) sdk.Context - commitGnoTransactionStoreFn func(ctx sdk.Context) -} - -func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { - if m.addPackageFn != nil { - return m.addPackageFn(ctx, msg) - } - - return nil -} - -func (m *mockVMKeeper) Call(ctx sdk.Context, msg vm.MsgCall) (res string, err error) { - if m.callFn != nil { - return m.callFn(ctx, msg) - } - - return "", nil -} - -func (m *mockVMKeeper) QueryEval(ctx sdk.Context, pkgPath, expr string) (res string, err error) { - if m.queryFn != nil { - return m.queryFn(ctx, pkgPath, expr) - } - - return "", nil -} - -func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err error) { - if m.runFn != nil { - return m.runFn(ctx, msg) - } - - return "", nil -} - -func (m *mockVMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { - if m.loadStdlibFn != nil { - m.loadStdlibFn(ctx, stdlibDir) - } -} - -func (m *mockVMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { - if m.loadStdlibCachedFn != nil { - m.loadStdlibCachedFn(ctx, stdlibDir) - } -} - -func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { - if m.makeGnoTransactionStoreFn != nil { - return m.makeGnoTransactionStoreFn(ctx) - } - return ctx -} - -func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { - if m.commitGnoTransactionStoreFn != nil { - m.commitGnoTransactionStoreFn(ctx) - } -} - -func (m *mockVMKeeper) InitGenesis(ctx sdk.Context, gs vm.GenesisState) {} - -type mockBankKeeper struct{} - -func (m *mockBankKeeper) InputOutputCoins(ctx sdk.Context, inputs []bank.Input, outputs []bank.Output) error { - return nil -} - -func (m *mockBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { - return nil -} - -func (m *mockBankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { - return nil -} - -func (m *mockBankKeeper) SubtractCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { - return nil, nil -} - -func (m *mockBankKeeper) AddCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { - return nil, nil -} - -func (m *mockBankKeeper) InitGenesis(ctx sdk.Context, data bank.GenesisState) {} -func (m *mockBankKeeper) GetParams(ctx sdk.Context) bank.Params { return bank.Params{} } -func (m *mockBankKeeper) GetCoins(ctx sdk.Context, addr crypto.Address) std.Coins { return nil } -func (m *mockBankKeeper) SetCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) error { - return nil -} - -func (m *mockBankKeeper) HasCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) bool { - return true -} - -type mockAuthKeeper struct{} - -func (m *mockAuthKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) std.Account { - return nil -} -func (m *mockAuthKeeper) GetAccount(ctx sdk.Context, addr crypto.Address) std.Account { return nil } -func (m *mockAuthKeeper) GetAllAccounts(ctx sdk.Context) []std.Account { return nil } -func (m *mockAuthKeeper) SetAccount(ctx sdk.Context, acc std.Account) {} -func (m *mockAuthKeeper) IterateAccounts(ctx sdk.Context, process func(std.Account) bool) {} -func (m *mockAuthKeeper) InitGenesis(ctx sdk.Context, data auth.GenesisState) {} -func (m *mockAuthKeeper) GetParams(ctx sdk.Context) auth.Params { return auth.Params{} } - -type mockParamsKeeper struct{} - -func (m *mockParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) {} -func (m *mockParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) {} -func (m *mockParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) {} -func (m *mockParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) {} -func (m *mockParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) {} -func (m *mockParamsKeeper) GetStrings(ctx sdk.Context, key string, ptr *[]string) {} - -func (m *mockParamsKeeper) SetString(ctx sdk.Context, key string, value string) {} -func (m *mockParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) {} -func (m *mockParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) {} -func (m *mockParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) {} -func (m *mockParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) {} -func (m *mockParamsKeeper) SetStrings(ctx sdk.Context, key string, value []string) {} - -func (m *mockParamsKeeper) Has(ctx sdk.Context, key string) bool { return false } -func (m *mockParamsKeeper) GetRaw(ctx sdk.Context, key string) []byte { return nil } -func (m *mockParamsKeeper) SetRaw(ctx sdk.Context, key string, value []byte) {} - -func (m *mockParamsKeeper) GetStruct(ctx sdk.Context, key string, strctPtr any) {} -func (m *mockParamsKeeper) SetStruct(ctx sdk.Context, key string, strct any) {} - -func (m *mockParamsKeeper) GetAny(ctx sdk.Context, key string) any { return nil } -func (m *mockParamsKeeper) SetAny(ctx sdk.Context, key string, value any) {} - -type mockGasPriceKeeper struct{} - -func (m *mockGasPriceKeeper) LastGasPrice(ctx sdk.Context) std.GasPrice { return std.GasPrice{} } -func (m *mockGasPriceKeeper) SetGasPrice(ctx sdk.Context, gp std.GasPrice) {} -func (m *mockGasPriceKeeper) UpdateGasPrice(ctx sdk.Context) {} - -type ( - lastBlockHeightDelegate func() int64 - loggerDelegate func() *slog.Logger -) - -type mockEndBlockerApp struct { - lastBlockHeightFn lastBlockHeightDelegate - loggerFn loggerDelegate -} - -func (m *mockEndBlockerApp) LastBlockHeight() int64 { - if m.lastBlockHeightFn != nil { - return m.lastBlockHeightFn() - } - - return 0 -} - -func (m *mockEndBlockerApp) Logger() *slog.Logger { - if m.loggerFn != nil { - return m.loggerFn() - } - - return log.NewNoopLogger() -} diff --git a/gno.land/pkg/gnoland/rpc/mock_test.go b/gno.land/pkg/gnoland/rpc/mock_test.go new file mode 100644 index 00000000000..e3fe590e7d0 --- /dev/null +++ b/gno.land/pkg/gnoland/rpc/mock_test.go @@ -0,0 +1,38 @@ +package rpc + +import ( + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +type ( + // newQueryContextDelegate creates a new app query context (read-only) + newQueryContextDelegate func(height int64) (sdk.Context, error) + + // vmKeeperDelegate returns the VM keeper associated with the app + vmKeeperDelegate func() vm.VMKeeperI + + // Simulate runs a transaction in simulate mode on the latest state + simulateDelegate func([]byte) sdk.Result +) + +type mockApplication struct { + newQueryContextFn newQueryContextDelegate + vmKeeperFn vmKeeperDelegate +} + +func (m *mockApplication) NewQueryContext(height int64) (sdk.Context, error) { + if m.newQueryContextFn != nil { + return m.newQueryContextFn(height) + } + + return sdk.Context{}, nil +} + +func (m *mockApplication) VMKeeper() vm.VMKeeperI { + if m.vmKeeperFn != nil { + return m.vmKeeperFn() + } + + return nil +} diff --git a/gno.land/pkg/gnoland/rpc/rpc.go b/gno.land/pkg/gnoland/rpc/rpc.go new file mode 100644 index 00000000000..65b017433ab --- /dev/null +++ b/gno.land/pkg/gnoland/rpc/rpc.go @@ -0,0 +1,95 @@ +package rpc + +import ( + "context" + "fmt" + "log/slog" + "net/http" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + rpcserver "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// Application is the required Gnoland app abstraction for the RPC +type Application interface { + // NewQueryContext creates a new app query context (read-only) + NewQueryContext(height int64) (sdk.Context, error) + + // VMKeeper returns the VM keeper associated with the app + VMKeeper() vm.VMKeeperI + + // Simulate runs a transaction in simulate mode on the latest state + Simulate(txBytes []byte, tx sdk.Tx) sdk.Result +} + +// Server is the Gnoland (app) RPC server instance +type Server struct { + app Application + logger *slog.Logger +} + +// NewServer creates a new instance of the Gnoland (app) RPC server +func NewServer(app Application, logger *slog.Logger) *Server { + return &Server{ + app: app, + logger: logger.With("module", "gnoland_rpc"), + } +} + +// rpcFuncs returns the endpoint -> handler mapping +func (s *Server) rpcFuncs() map[string]*rpcserver.RPCFunc { + return map[string]*rpcserver.RPCFunc{ + "vm/render": rpcserver.NewRPCFunc(s.VMRender, "height,pkgPath,path"), + "vm/funcs": rpcserver.NewRPCFunc(s.VMFuncs, "height,pkgPath"), + "vm/eval": rpcserver.NewRPCFunc(s.VMEval, "height,data"), + "vm/file": rpcserver.NewRPCFunc(s.VMFile, "height,filepath"), + "vm/doc": rpcserver.NewRPCFunc(s.VMDoc, "height,pkgPath"), + "vm/paths": rpcserver.NewRPCFunc(s.VMPaths, "height,target,limit"), + "vm/storage": rpcserver.NewRPCFunc(s.VMStorage, "height,pkgPath"), + "vm/simulate": rpcserver.NewRPCFunc(s.VMSimulate, "tx"), + } +} + +// newMux creates a server mux, and registers the endpoints for both http and ws requests +func (s *Server) newMux() *http.ServeMux { + mux := http.NewServeMux() + + // Register the HTTP handlers + rpcserver.RegisterRPCFuncs(mux, s.rpcFuncs(), s.logger) + + // Register the websocket handlers as well + wsMgr := rpcserver.NewWebsocketManager(s.rpcFuncs()) + wsMgr.SetLogger(s.logger) + mux.HandleFunc("/websocket", wsMgr.WebsocketHandler) + + return mux +} + +func (s *Server) Serve( + ctx context.Context, + addr string, + cfg *rpcserver.Config, +) error { + if cfg == nil { + cfg = rpcserver.DefaultConfig() + } + + l, err := rpcserver.Listen(addr, cfg) + if err != nil { + return fmt.Errorf("unable to listen for app RPC on %s: %w", addr, err) + } + + go func() { + <-ctx.Done() + _ = l.Close() + }() + + go func() { + if err := rpcserver.StartHTTPServer(l, s.newMux(), s.logger, cfg); err != nil { + s.logger.Error("unable to gracefully stop app RPC", "err", err) + } + }() + + return nil +} diff --git a/gno.land/pkg/gnoland/rpc/rpc_test.go b/gno.land/pkg/gnoland/rpc/rpc_test.go new file mode 100644 index 00000000000..7595e4e0432 --- /dev/null +++ b/gno.land/pkg/gnoland/rpc/rpc_test.go @@ -0,0 +1,59 @@ +package rpc + +import ( + "io" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestServer_RegisterHandlers(t *testing.T) { + s := &Server{ + app: &mockApplication{}, + logger: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})), + } + + funcs := s.rpcFuncs() + + expected := []string{ + "vm/render", + "vm/funcs", + "vm/eval", + "vm/file", + "vm/doc", + "vm/paths", + "vm/storage", + } + + require.Len(t, funcs, len(expected)) + + for _, key := range expected { + assert.Contains(t, funcs, key) + } +} + +func TestServer_WebsocketReachable(t *testing.T) { + t.Parallel() + + s := &Server{ + app: &mockApplication{}, + logger: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})), + } + + mux := s.newMux() + + ts := httptest.NewServer(mux) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/websocket") + require.NoError(t, err) + + defer resp.Body.Close() + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} diff --git a/gno.land/pkg/gnoland/rpc/types.go b/gno.land/pkg/gnoland/rpc/types.go new file mode 100644 index 00000000000..6741bd477ff --- /dev/null +++ b/gno.land/pkg/gnoland/rpc/types.go @@ -0,0 +1,9 @@ +package rpc + +import "github.com/gnolang/gno/tm2/pkg/std" + +type SimulateResponse struct { + GasUsed int64 `json:"gas_used"` + StorageFee std.Coins `json:"storage_fee,omitempty"` + StorageDelta int64 `json:"storage_delta"` // bytes +} diff --git a/gno.land/pkg/gnoland/rpc/vm.go b/gno.land/pkg/gnoland/rpc/vm.go new file mode 100644 index 00000000000..c1597f172ce --- /dev/null +++ b/gno.land/pkg/gnoland/rpc/vm.go @@ -0,0 +1,189 @@ +package rpc + +import ( + "fmt" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/keyscli" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// TODO update response types, since now we can properly handle them (they don't need to be string values) + +// TODO Fix +func parseQueryEvalData(data string) (pkgPath, expr string) { + slash := strings.IndexByte(data, '/') + if slash >= 0 { + pkgPath += data[:slash] + data = data[slash:] + } + dot := strings.IndexByte(data, '.') + if dot < 0 { + panic("invalid query data") + } + pkgPath += data[:dot] + expr = data[dot+1:] + return +} + +// VMEval evaluates a call to an exported function without using gas, in read-only mode +func (s *Server) VMEval(_ *rpctypes.Context, height int64, data string) (string, error) { + realm, expr := parseQueryEvalData(data) + + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + result, err := s.app.VMKeeper().QueryEval(ctx, realm, expr) + if err != nil { + return "", fmt.Errorf("unable to evaluate expression: %w", err) + } + + return result, nil +} + +// VMRender evaluates the "Render" function call +func (s *Server) VMRender(_ *rpctypes.Context, height int64, pkgPath, path string) (string, error) { + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + expr := fmt.Sprintf("Render(%q)", path) + result, err := s.app.VMKeeper().QueryEval(ctx, pkgPath, expr) + if err != nil { + if strings.Contains(err.Error(), "Render not declared") { + err = vm.NoRenderDeclError{} + } + + return "", fmt.Errorf("unable to call Render: %w", err) + } + + return result, nil +} + +// VMFuncs returns the exported functions for the given package path +func (s *Server) VMFuncs(_ *rpctypes.Context, height int64, pkgPath string) (string, error) { + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + funcSigs, err := s.app.VMKeeper().QueryFuncs(ctx, pkgPath) + if err != nil { + return "", err + } + + return funcSigs.JSON(), nil +} + +// VMPaths lists all existing package paths prefixed with the specified target string, paginated +func (s *Server) VMPaths(_ *rpctypes.Context, height int64, target string, limit int) (string, error) { + const ( + defaultLimit = 1_000 + maxLimit = 10_000 + ) + + if limit <= 0 { + limit = defaultLimit + } + if limit > maxLimit { + limit = maxLimit + } + + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + paths, err := s.app.VMKeeper().QueryPaths(ctx, target, limit) + if err != nil { + return "", err + } + + return strings.Join(paths, "\n"), nil +} + +// VMFile returns package contents for a given package path +func (s *Server) VMFile(_ *rpctypes.Context, height int64, filepath string) (string, error) { + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + result, err := s.app.VMKeeper().QueryFile(ctx, filepath) + if err != nil { + return "", err + } + + return result, nil +} + +// VMDoc returns the JSON of the doc for a given package path, suitable for printing +func (s *Server) VMDoc(_ *rpctypes.Context, height int64, pkgPath string) (string, error) { + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + jsonDoc, err := s.app.VMKeeper().QueryDoc(ctx, pkgPath) + if err != nil { + return "", err + } + + return jsonDoc.JSON(), nil +} + +// VMStorage returns storage usage and deposit locked in a realm +func (s *Server) VMStorage(_ *rpctypes.Context, height int64, pkgPath string) (string, error) { + ctx, err := s.app.NewQueryContext(height) + if err != nil { + return "", fmt.Errorf("unable to create query context: %w", err) + } + + result, err := s.app.VMKeeper().QueryStorage(ctx, pkgPath) + if err != nil { + return "", err + } + + return result, nil +} + +// VMSimulate runs a transaction in simulate mode on the latest state. +// TX is the amino-encoded transaction +// +// TODO we shouldn't need a signed transaction to simulate execution in the VM. +// Ideally, we would have a common type that the VM understands, and that the VM keeper ports to from +// existing messages. The user would then use this type to simulate their action +func (s *Server) VMSimulate(_ *rpctypes.Context, txBytes []byte) (*SimulateResponse, error) { + var tx sdk.Tx + + // Decode the tx + if err := amino.Unmarshal(txBytes, &tx); err != nil { + return nil, fmt.Errorf("unable to decode tx: %w", err) + } + + // Run simulation on latest state + simulateRes := s.app.Simulate(txBytes, tx) + + if err := simulateRes.Error; err != nil { + return nil, fmt.Errorf("error encountered during simulation: %w", err) + } + + response := &SimulateResponse{ + GasUsed: simulateRes.GasUsed, + } + + // Fetch the storage deposit fees + bytesDelta, coinsDelta, hasEvents := keyscli.GetStorageInfo(simulateRes.Events) + if hasEvents { + response.StorageFee = coinsDelta + response.StorageDelta = bytesDelta + } + + return response, nil +} diff --git a/gno.land/pkg/gnoland/rpc/vm_test.go b/gno.land/pkg/gnoland/rpc/vm_test.go new file mode 100644 index 00000000000..bd7ffb1970e --- /dev/null +++ b/gno.land/pkg/gnoland/rpc/vm_test.go @@ -0,0 +1,719 @@ +package rpc + +import ( + "errors" + "strings" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland/mock" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/doc" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestServer_VMEval(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + realm = "gno.land/r/example" + expr = "Func()" + ) + + result, err := server.VMEval(nil, height, realm, expr) + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid eval", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + + expectedRealm = "gno.land/r/example" + expectedExpr = "Func()" + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, realm string, expr string) (string, error) { + require.Equal(t, expectedRealm, realm) + require.Equal(t, expectedExpr, expr) + + return "", queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMEval(nil, expectedHeight, expectedRealm, expectedExpr) + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid eval", func(t *testing.T) { + t.Parallel() + + var ( + expectedRealm = "gno.land/r/example" + expectedExpr = "Func()" + expectedResult = "hello" + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, realm string, expr string) (string, error) { + require.Equal(t, expectedRealm, realm) + require.Equal(t, expectedExpr, expr) + + return expectedResult, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMEval(nil, expectedHeight, expectedRealm, expectedExpr) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }) +} + +func TestServer_VMRender(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + realm = "gno.land/r/example" + ) + + result, err := server.VMRender(nil, height, realm, "") + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid render", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + + expectedRealm = "gno.land/r/example" + expectedExpr = "Render(\"\")" + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, realm string, expr string) (string, error) { + require.Equal(t, expectedRealm, realm) + require.Equal(t, expectedExpr, expr) + + return "", queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMRender(nil, expectedHeight, expectedRealm, "") + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid render", func(t *testing.T) { + t.Parallel() + + var ( + expectedRealm = "gno.land/r/example" + expectedExpr = "Render(\"\")" + expectedResult = "hello render" + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryEvalFn: func(_ sdk.Context, realm string, expr string) (string, error) { + require.Equal(t, expectedRealm, realm) + require.Equal(t, expectedExpr, expr) + + return expectedResult, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMRender(nil, expectedHeight, expectedRealm, "") + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }) +} + +func TestServer_VMFuncs(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + realm = "gno.land/r/example" + ) + + result, err := server.VMFuncs(nil, height, realm) + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid funcs", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + + expectedRealm = "gno.land/r/example" + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryFuncsFn: func(_ sdk.Context, realm string) (vm.FunctionSignatures, error) { + require.Equal(t, expectedRealm, realm) + + return nil, queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMFuncs(nil, expectedHeight, expectedRealm) + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid funcs", func(t *testing.T) { + t.Parallel() + + var ( + expectedRealm = "gno.land/r/example" + expectedFuncSigs = vm.FunctionSignatures{ + { + FuncName: "hello1", + }, + { + FuncName: "hello2", + }, + } + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryFuncsFn: func(_ sdk.Context, realm string) (vm.FunctionSignatures, error) { + require.Equal(t, expectedRealm, realm) + + return expectedFuncSigs, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMFuncs(nil, expectedHeight, expectedRealm) + require.NoError(t, err) + + assert.Equal(t, expectedFuncSigs.JSON(), result) + }) +} + +func TestServer_VMPaths(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + realm = "gno.land/r/example" + ) + + result, err := server.VMPaths(nil, height, realm, 1) + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid paths", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + + expectedTarget = "gno.land/r/example" + expectedLimit = 10 + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryPathsFn: func(_ sdk.Context, target string, limit int) ([]string, error) { + require.Equal(t, expectedTarget, target) + require.Equal(t, expectedLimit, limit) + + return nil, queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMPaths(nil, expectedHeight, expectedTarget, expectedLimit) + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid paths", func(t *testing.T) { + t.Parallel() + + var ( + expectedTarget = "gno.land/r/example" + expectedLimit = 10 + expectedPaths = []string{expectedTarget} + expectedHeight = int64(10) + + keeper = &mock.VMKeeper{ + QueryPathsFn: func(_ sdk.Context, target string, limit int) ([]string, error) { + require.Equal(t, expectedTarget, target) + require.Equal(t, expectedLimit, limit) + + return expectedPaths, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMPaths(nil, expectedHeight, expectedTarget, expectedLimit) + require.NoError(t, err) + + assert.Equal(t, strings.Join(expectedPaths, "\n"), result) + }) +} + +func TestServer_VMFile(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + filepath = "gno.land/r/example/file.gno" + ) + + result, err := server.VMFile(nil, height, filepath) + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid file query", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + expectedPath = "gno.land/r/example/file.gno" + expectedHeight = int64(0) + + keeper = &mock.VMKeeper{ + QueryFileFn: func(_ sdk.Context, fp string) (string, error) { + require.Equal(t, expectedPath, fp) + + return "", queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMFile(nil, expectedHeight, expectedPath) + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid file query", func(t *testing.T) { + t.Parallel() + + var ( + expectedPath = "gno.land/r/example/file.gno" + expectedHeight = int64(0) + expectedResult = "file contents" + + keeper = &mock.VMKeeper{ + QueryFileFn: func(_ sdk.Context, path string) (string, error) { + require.Equal(t, expectedPath, path) + + return expectedResult, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMFile(nil, expectedHeight, expectedPath) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }) +} + +func TestServer_VMDoc(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + pkg = "gno.land/r/example" + ) + + result, err := server.VMDoc(nil, height, pkg) + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid doc query", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + expectedPkg = "gno.land/r/example" + expectedHeight = int64(0) + + keeper = &mock.VMKeeper{ + QueryDocFn: func(_ sdk.Context, pkgPath string) (*doc.JSONDocumentation, error) { + require.Equal(t, expectedPkg, pkgPath) + + return nil, queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMDoc(nil, expectedHeight, expectedPkg) + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid doc query", func(t *testing.T) { + t.Parallel() + + var ( + expectedPkg = "gno.land/r/example" + expectedHeight = int64(0) + expectedDoc = &doc.JSONDocumentation{} + expectedJSON = expectedDoc.JSON() + + keeper = &mock.VMKeeper{ + QueryDocFn: func(_ sdk.Context, pkgPath string) (*doc.JSONDocumentation, error) { + require.Equal(t, expectedPkg, pkgPath) + + return expectedDoc, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMDoc(nil, expectedHeight, expectedPkg) + require.NoError(t, err) + + assert.Equal(t, expectedJSON, result) + }) +} + +func TestServer_VMStorage(t *testing.T) { + t.Parallel() + + t.Run("invalid context creation", func(t *testing.T) { + t.Parallel() + + var ( + sdkErr = errors.New("context err") + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + return sdk.Context{}, sdkErr + }, + } + + server = NewServer(app, log.NewNoopLogger()) + + height = int64(0) + pkg = "gno.land/r/example" + ) + + result, err := server.VMStorage(nil, height, pkg) + require.Empty(t, result) + + assert.ErrorIs(t, err, sdkErr) + }) + + t.Run("invalid storage query", func(t *testing.T) { + t.Parallel() + + var ( + queryErr = errors.New("query err") + expectedPkg = "gno.land/r/example" + expectedHeight = int64(0) + + keeper = &mock.VMKeeper{ + QueryStorageFn: func(_ sdk.Context, pkgPath string) (string, error) { + require.Equal(t, expectedPkg, pkgPath) + + return "", queryErr + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMStorage(nil, expectedHeight, expectedPkg) + require.Empty(t, result) + + assert.ErrorIs(t, err, queryErr) + }) + + t.Run("valid storage query", func(t *testing.T) { + t.Parallel() + + var ( + expectedPkg = "gno.land/r/example" + expectedHeight = int64(0) + expectedStorage = "storage: 10, deposit: 100" + + keeper = &mock.VMKeeper{ + QueryStorageFn: func(_ sdk.Context, pkgPath string) (string, error) { + require.Equal(t, expectedPkg, pkgPath) + + return expectedStorage, nil + }, + } + app = &mockApplication{ + newQueryContextFn: func(height int64) (sdk.Context, error) { + require.Equal(t, expectedHeight, height) + + return sdk.Context{}, nil + }, + vmKeeperFn: func() vm.VMKeeperI { + return keeper + }, + } + + server = NewServer(app, log.NewNoopLogger()) + ) + + result, err := server.VMStorage(nil, expectedHeight, expectedPkg) + require.NoError(t, err) + + assert.Equal(t, expectedStorage, result) + }) +} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index c644ee6b1b9..364352c8f84 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -54,6 +54,11 @@ type VMKeeperI interface { AddPackage(ctx sdk.Context, msg MsgAddPackage) error Call(ctx sdk.Context, msg MsgCall) (res string, err error) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) + QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) + QueryPaths(ctx sdk.Context, target string, limit int) ([]string, error) + QueryFile(ctx sdk.Context, filepath string) (res string, err error) + QueryDoc(ctx sdk.Context, pkgPath string) (*doc.JSONDocumentation, error) + QueryStorage(ctx sdk.Context, pkgPath string) (string, error) Run(ctx sdk.Context, msg MsgRun) (res string, err error) LoadStdlib(ctx sdk.Context, stdlibDir string) LoadStdlibCached(ctx sdk.Context, stdlibDir string) diff --git a/tm2/pkg/bft/rpc/lib/server/http_server.go b/tm2/pkg/bft/rpc/lib/server/http_server.go index a5cec3d5c81..40bed3b938e 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server.go @@ -37,8 +37,8 @@ type Config struct { func DefaultConfig() *Config { return &Config{ MaxOpenConnections: 0, // unlimited - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, + ReadTimeout: 60 * time.Second, + WriteTimeout: 60 * time.Second, MaxBodyBytes: int64(5000000), // 5MB MaxHeaderBytes: 1 << 20, // same as the net/http default } diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index c37241fcf60..72a5ed105c6 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -407,6 +407,32 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { } } +// NewQueryContext creates a read-only Context for the given height. +// If the height provided is 0, it uses the latest height (as genesis is unfetchable) +func (app *BaseApp) NewQueryContext(height int64) (Context, error) { + if height == 0 { + height = app.LastBlockHeight() + } + + // Load read-only snapshot + cacheMS, err := app.cms.MultiImmutableCacheWrapWithVersion(height) + if err != nil { + return Context{}, fmt.Errorf( + "failed to load state at height %d (latest: %d): %w", + height, app.LastBlockHeight(), err, + ) + } + + ctx := NewContext( + RunTxModeCheck, + cacheMS, + app.checkState.ctx.BlockHeader(), + app.logger, + ).WithMinGasPrices(app.minGasPrices) + + return ctx, nil +} + func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { if len(path) >= 2 { var result Result diff --git a/tm2/pkg/sdk/types.go b/tm2/pkg/sdk/types.go index 47395362f1a..87dbaf6535b 100644 --- a/tm2/pkg/sdk/types.go +++ b/tm2/pkg/sdk/types.go @@ -40,12 +40,9 @@ type ( GasPrice = std.GasPrice ) -var ( - ParseGasPrice = std.ParseGasPrice - ParseGasPrices = std.ParseGasPrices -) +var ParseGasPrices = std.ParseGasPrices -//---------------------------------------- +// ---------------------------------------- // Enum mode for app.runTx type RunTxMode uint8