From 7821510279de0b85cca3700e5fa48e0d5ea53845 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:00:51 +0200 Subject: [PATCH 01/12] feat: json primitive Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/sdk/vm/convert.go | 86 ++++++++++++++++++++++++ gno.land/pkg/sdk/vm/keeper.go | 59 +++++++++-------- gno.land/pkg/sdk/vm/keeper_test.go | 103 +++++++++++++++++++++++++++-- gnovm/pkg/gnolang/values_string.go | 12 +++- tm2/pkg/sdk/baseapp.go | 3 + 5 files changed, 229 insertions(+), 34 deletions(-) diff --git a/gno.land/pkg/sdk/vm/convert.go b/gno.land/pkg/sdk/vm/convert.go index cafb6cad67f..f1a0e9b6991 100644 --- a/gno.land/pkg/sdk/vm/convert.go +++ b/gno.land/pkg/sdk/vm/convert.go @@ -192,6 +192,92 @@ func convertArgToGno(arg string, argT gno.Type) (tv gno.TypedValue) { } } +func JSONValues(m *gno.Machine, tvs ...gno.TypedValue) string { + var str strings.Builder + + str.WriteRune('[') + for i, tv := range tvs { + if i > 0 { + str.WriteRune(',') + } + str.WriteString(JSONValue(m, tv)) + } + str.WriteRune(']') + + return str.String() +} + +func JSONValue(m *gno.Machine, tv gno.TypedValue) string { + if tv.T == nil { + panic("empty type") + } + + switch bt := gno.BaseOf(tv.T).(type) { + case gno.PrimitiveType: + switch bt { + case gno.IntType: + return fmt.Sprintf("%d", tv.GetInt()) + case gno.Int8Type: + return fmt.Sprintf("%d", tv.GetInt8()) + case gno.Int16Type: + return fmt.Sprintf("%d", tv.GetInt16()) + case gno.UntypedRuneType, gno.Int32Type: + return fmt.Sprintf("%d", tv.GetInt32()) + case gno.Int64Type: + return fmt.Sprintf("%d", tv.GetInt64()) + case gno.UintType: + return fmt.Sprintf("%d", tv.GetUint()) + case gno.Uint8Type: + return fmt.Sprintf("%d", tv.GetUint8()) + case gno.DataByteType: + return fmt.Sprintf("%d", tv.GetDataByte()) + case gno.Uint16Type: + return fmt.Sprintf("%d", tv.GetUint16()) + case gno.Uint32Type: + return fmt.Sprintf("%d", tv.GetUint32()) + case gno.Uint64Type: + return fmt.Sprintf("%d", tv.GetUint64()) + case gno.Float32Type: + return fmt.Sprintf("%f", tv.GetFloat32()) + case gno.Float64Type: + return fmt.Sprintf("%f", tv.GetFloat64()) + case gno.UntypedBigintType, gno.BigintType: + return tv.V.(gno.BigintValue).V.String() + case gno.UntypedBigdecType, gno.BigdecType: + return tv.V.(gno.BigdecValue).V.String() + case gno.UntypedBoolType, gno.BoolType: + return fmt.Sprintf("%t", tv.GetBool()) + case gno.UntypedStringType, gno.StringType: + return strconv.Quote(tv.GetString()) + default: + panic("invalid primitive type - should not happen") + } + } + + if tv.V == nil { + return "" + } + + // if implements .String(), return it. + if tv.IsStringer() { + panic("nooo") + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "String"))) + return strconv.Quote(res[0].GetString()) + } + // if implements .Error(), return it. + if tv.IsError() { + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "Error"))) + return strconv.Quote(res[0].GetString()) + } + + var id string + if pv, ok := tv.V.(gno.PointerValue); ok { + id = pv.GetBase(m.Store).GetObjectID().String() + } + + return strconv.Quote(fmt.Sprintf(`<%s:%s>`, tv.T.String(), id)) +} + func convertFloat(value string, precision int) float64 { assertNoPlusPrefix(value) dec, _, err := apd.NewFromString(value) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 365473b3e7a..7d9ad4cc194 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -409,15 +409,18 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { pkgPath := msg.PkgPath // to import fnc := msg.Func gnostore := vm.getGnoTransactionStore(ctx) + // Get the package and function type. pv := gnostore.GetPackage(pkgPath, false) pl := gno.PackageNodeLocation(pkgPath) pn := gnostore.GetBlockNode(pl).(*gno.PackageNode) ft := pn.GetStaticTypeOf(gnostore, gno.Name(fnc)).(*gno.FuncType) + // Make main Package with imports. mpn := gno.NewPackageNode("main", "main", nil) mpn.Define("pkg", gno.TypedValue{T: &gno.PackageType{}, V: pv}) mpv := mpn.NewPackage() + // Parse expression. argslist := "" for i := range msg.Args { @@ -428,6 +431,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { } expr := fmt.Sprintf(`pkg.%s(%s)`, fnc, argslist) xn := gno.MustParseExpr(expr) + // Send send-coins to pkg from caller. pkgAddr := gno.DerivePkgAddr(pkgPath) caller := msg.Caller @@ -436,6 +440,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { if err != nil { return "", err } + // Convert Args to gno values. cx := xn.(*gno.CallExpr) if cx.Varg { @@ -451,6 +456,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { TypedValue: atv, } } + // Make context. // NOTE: if this is too expensive, // could it be safely partially memoized? @@ -466,6 +472,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { Banker: NewSDKBanker(vm, ctx), EventLogger: ctx.EventLogger(), } + // Construct machine and evaluate. m := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -477,8 +484,9 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { MaxCycles: vm.maxCycles, GasMeter: ctx.GasMeter(), }) - defer m.Release() m.SetActivePackage(mpv) + defer m.Release() + defer func() { if r := recover(); r != nil { switch r := r.(type) { @@ -494,13 +502,9 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { } } }() + rtvs := m.Eval(xn) - for i, rtv := range rtvs { - res = res + rtv.String() - if i < len(rtvs)-1 { - res += "\n" - } - } + res = JSONValues(m, rtvs...) // Log the telemetry logTelemetry( @@ -510,10 +514,12 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { Key: "operation", Value: attribute.StringValue("m_call"), }, + attribute.KeyValue{ + Key: "func", + Value: attribute.StringValue(msg.Func), + }, ) - res += "\n\n" // use `\n\n` as separator to separate results for single tx with multi msgs - return res, nil // TODO pay for gas? TODO see context? } @@ -750,13 +756,8 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res } }() rtvs := m.Eval(xx) - res = "" - for i, rtv := range rtvs { - res += rtv.String() - if i < len(rtvs)-1 { - res += "\n" - } - } + + res = JSONValues(m, rtvs...) return res, nil } @@ -834,20 +835,24 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err if memFile == nil { return "", fmt.Errorf("file %q is not available", filepath) // TODO: XSS protection } + return memFile.Body, nil - } else { - memPkg := store.GetMemPackage(dirpath) - if memPkg == nil { - return "", fmt.Errorf("package %q is not available", dirpath) // TODO: XSS protection - } - for i, memfile := range memPkg.Files { - if i > 0 { - res += "\n" - } - res += memfile.Name + } + + memPkg := store.GetMemPackage(dirpath) + if memPkg == nil { + return "", fmt.Errorf("package %q is not available", dirpath) // TODO: XSS protection + } + + for i, memfile := range memPkg.Files { + if i > 0 { + res += "\n" } - return res, nil + res += memfile.Name } + + return res, nil + } // logTelemetry logs the VM processing telemetry diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 9257da2ddaf..9a909a7a743 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -108,7 +108,7 @@ func Echo(msg string) string { msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) res, err := env.vmk.Call(ctx, msg2) assert.NoError(t, err) - assert.Equal(t, `("echo:hello world" string)`+"\n\n", res) + assert.Equal(t, `["echo:hello world"]`, res) // t.Log("result:", res) } @@ -251,7 +251,7 @@ func Echo(msg string) string { msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) res, err := env.vmk.Call(ctx, msg2) assert.NoError(t, err) - assert.Equal(t, `("echo:hello world" string)`+"\n\n", res) + assert.Equal(t, `["echo:hello world"]`, res) } // Sending too much realm package coins fails. @@ -347,11 +347,104 @@ func GetAdmin() string { coins := std.MustParseCoins("") msg2 := NewMsgCall(addr, coins, pkgPath, "GetAdmin", []string{}) res, err := env.vmk.Call(ctx, msg2) - addrString := fmt.Sprintf("(\"%s\" string)\n\n", addr.String()) + addrString := fmt.Sprintf("[\"%s\"]", addr.String()) assert.NoError(t, err) assert.Equal(t, addrString, res) } +func TestVMKeeperCallReturnPointerValues(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) + + // Create test package. + files := []*std.MemFile{ + {"init.gno", ` +package test + +type TStructA struct { } + +func (a *TStructA) String() string { return "S:This Is A" } + +type TStructB struct { } + +func (b *TStructB) Error() string { return "E:This Is B" } + +type TStructC struct { } + +var A, B, C = &TStructA{}, &TStructB{}, &TStructC{} + +func StringerReturnPointer() (*TStructA, *TStructB, *TStructC) { + return A, B, C +} +`}, + } + pkgPath := "gno.land/r/test" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg1) + assert.NoError(t, err) + + const expected = `["S:This Is A","E:This Is B",""]` + + // Run GetAdmin() + coins := std.MustParseCoins("") + msg2 := NewMsgCall(addr, coins, pkgPath, "StringerReturnPointer", []string{}) + res, err := env.vmk.Call(ctx, msg2) + assert.NoError(t, err) + assert.Equal(t, expected, res) +} + +func TestVMKeeperCallReturnValues(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) + + // Create test package. + files := []*std.MemFile{ + {"init.gno", ` +package test + +type TStructA struct { } + +func (a *TStructA) String() string { return "S:This Is A" } + +type TStructB struct { } + +func (b *TStructB) Error() string { return "E:This Is B" } + +type TStructC struct { } + +func StringerReturn() (TStructA, TStructB, TStructC) { + return TStructA{}, TStructB{}, TStructC{} +} +`}, + } + pkgPath := "gno.land/r/test" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg1) + require.NoError(t, err) + + const expected = `["","",""]` + + coins := std.MustParseCoins("") + msg3 := NewMsgCall(addr, coins, pkgPath, "StringerReturn", []string{}) + res, err := env.vmk.Call(ctx, msg3) + assert.NoError(t, err) + assert.Equal(t, expected, res) +} + // Call Run without imports, without variables. func TestVMKeeperRunSimple(t *testing.T) { env := setupTestEnv() @@ -490,7 +583,7 @@ func Echo(msg string) string { msg2 := NewMsgCall(addr, nil, pkgPath, "Echo", []string{"hello world"}) res, err := env.vmk.Call(ctx, msg2) require.NoError(t, err) - assert.Equal(t, `("echo:hello world" string)`+"\n\n", res) + assert.Equal(t, `["echo:hello world"]`, res) // Clear out gnovm and reinitialize. env.vmk.gnoStore = nil @@ -501,7 +594,7 @@ func Echo(msg string) string { // Run echo again, and it should still work. res, err = env.vmk.Call(ctx, msg2) require.NoError(t, err) - assert.Equal(t, `("echo:hello world" string)`+"\n\n", res) + assert.Equal(t, `["echo:hello world"]`, res) } func Test_loadStdlibPackage(t *testing.T) { diff --git a/gnovm/pkg/gnolang/values_string.go b/gnovm/pkg/gnolang/values_string.go index a414f440e4e..ea63c281d49 100644 --- a/gnovm/pkg/gnolang/values_string.go +++ b/gnovm/pkg/gnolang/values_string.go @@ -269,6 +269,14 @@ func (v *HeapItemValue) String() string { // ---------------------------------------- // *TypedValue.Sprint +func (tv *TypedValue) IsStringer() bool { + return IsImplementedBy(gStringerType, tv.T) +} + +func (tv *TypedValue) IsError() bool { + return IsImplementedBy(gErrorType, tv.T) +} + // for print() and println(). func (tv *TypedValue) Sprint(m *Machine) string { // if undefined, just "undefined". @@ -277,12 +285,12 @@ func (tv *TypedValue) Sprint(m *Machine) string { } // if implements .String(), return it. - if IsImplementedBy(gStringerType, tv.T) { + if tv.IsStringer() { res := m.Eval(Call(Sel(&ConstExpr{TypedValue: *tv}, "String"))) return res[0].GetString() } // if implements .Error(), return it. - if IsImplementedBy(gErrorType, tv.T) { + if tv.IsError() { res := m.Eval(Call(Sel(&ConstExpr{TypedValue: *tv}, "Error"))) return res[0].GetString() } diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 867a38d680a..81de038c15b 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -651,6 +651,9 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res // Each message result's Data must be length prefixed in order to separate // each result. + if i > 0 { + data = append(data, '\n') + } data = append(data, msgResult.Data...) events = append(events, msgResult.Events...) From 11c03272120a0a10f305a117d5a078cb7ae016a7 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:14:37 +0200 Subject: [PATCH 02/12] feat: json primitive Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/demo/users/users.gno | 1 + gno.land/pkg/sdk/vm/convert.go | 48 +++++--- gno.land/pkg/sdk/vm/convert_test.go | 144 +++++++++++++++++++++++ gno.land/pkg/sdk/vm/handler.go | 7 +- gno.land/pkg/sdk/vm/keeper.go | 33 +++++- gno.land/pkg/sdk/vm/msgs.go | 11 ++ gno.land/pkg/sdk/vm/types.go | 4 +- gnovm/pkg/gnolang/values.go | 8 ++ tm2/pkg/bft/abci/types/types.go | 10 +- tm2/pkg/sdk/types.go | 4 +- 10 files changed, 242 insertions(+), 28 deletions(-) diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 4a0b9c1caf7..b7dd0610588 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -32,6 +32,7 @@ var ( func Register(inviter std.Address, name string, profile string) { // assert CallTx call. std.AssertOriginCall() + // assert invited or paid. caller := std.GetCallerAt(2) if caller != std.GetOrigCaller() { diff --git a/gno.land/pkg/sdk/vm/convert.go b/gno.land/pkg/sdk/vm/convert.go index f1a0e9b6991..83de937ba8c 100644 --- a/gno.land/pkg/sdk/vm/convert.go +++ b/gno.land/pkg/sdk/vm/convert.go @@ -192,7 +192,7 @@ func convertArgToGno(arg string, argT gno.Type) (tv gno.TypedValue) { } } -func JSONValues(m *gno.Machine, tvs ...gno.TypedValue) string { +func JSONValues(m *gno.Machine, tvs []gno.TypedValue) string { var str strings.Builder str.WriteRune('[') @@ -209,7 +209,7 @@ func JSONValues(m *gno.Machine, tvs ...gno.TypedValue) string { func JSONValue(m *gno.Machine, tv gno.TypedValue) string { if tv.T == nil { - panic("empty type") + return "null" } switch bt := gno.BaseOf(tv.T).(type) { @@ -252,22 +252,40 @@ func JSONValue(m *gno.Machine, tv gno.TypedValue) string { default: panic("invalid primitive type - should not happen") } - } + case *gno.PointerType: + // Check if Pointer we type implement Stringer / Error - if tv.V == nil { - return "" - } + // If implements .Error(), return it. + if tv.IsError() { + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "Error"))) + return strconv.Quote(res[0].GetString()) + } + // If implements .String(), return it. + if tv.IsStringer() { + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "String"))) + return strconv.Quote(res[0].GetString()) + } + default: + // Check if pointer wraped value can implement Stringer / Error + ptv := gno.TypedValue{ + T: &gno.PointerType{Elt: tv.T}, + V: gno.PointerValue{TV: &tv, Base: tv.V}, + } - // if implements .String(), return it. - if tv.IsStringer() { - panic("nooo") - res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "String"))) - return strconv.Quote(res[0].GetString()) + // If implements .Error(), return it. + if ptv.IsError() { + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: ptv}, "Error"))) + return strconv.Quote(res[0].GetString()) + } + // If implements .String(), return it. + if ptv.IsStringer() { + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: ptv}, "String"))) + return strconv.Quote(res[0].GetString()) + } } - // if implements .Error(), return it. - if tv.IsError() { - res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "Error"))) - return strconv.Quote(res[0].GetString()) + + if tv.V == nil { + return "null" } var id string diff --git a/gno.land/pkg/sdk/vm/convert_test.go b/gno.land/pkg/sdk/vm/convert_test.go index 666ec1620fa..e28bf317a9e 100644 --- a/gno.land/pkg/sdk/vm/convert_test.go +++ b/gno.land/pkg/sdk/vm/convert_test.go @@ -2,10 +2,13 @@ package vm import ( "fmt" + "strconv" + "strings" "testing" "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConvertEmptyNumbers(t *testing.T) { @@ -37,3 +40,144 @@ func TestConvertEmptyNumbers(t *testing.T) { }) } } + +func TestConvertJSONValuePrimtive(t *testing.T) { + cases := []struct { + ValueRep string // Go representation + Expected string // string representation + }{ + // Boolean + {"nil", "null"}, + + // Boolean + {"true", "true"}, + {"false", "false"}, + + // int types + {"int(42)", `42`}, // Needs to be quoted for amino + {"int8(42)", `42`}, + {"int16(42)", `42`}, + {"int32(42)", `42`}, + {"int64(42)", `42`}, + + // uint types + {"uint(42)", `42`}, + {"uint8(42)", `42`}, + {"uint16(42)", `42`}, + {"uint32(42)", `42`}, + {"uint64(42)", `42`}, + + // Float types // XXX: Require amino unsafe + // {"float32(3.14)", "3.14"}, + // {"float64(3.14)", "3.14"}, + + // String type + {`"hello world"`, `"hello world"`}, + } + + for _, tc := range cases { + t.Run(tc.ValueRep, func(t *testing.T) { + m := gnolang.NewMachine("testdata", nil) + defer m.Release() + + nn := gnolang.MustParseFile("testdata.gno", + fmt.Sprintf(`package testdata; var Value = %s`, tc.ValueRep)) + m.RunFiles(nn) + m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) + + tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value")) + require.Len(t, tps, 1) + + tv := tps[0] + + rep := JSONValue(m, tv) + require.Equal(t, tc.Expected, rep) + }) + } +} + +func TestConvertJSONValueStruct(t *testing.T) { + const StructsFile = ` +package testdata + +// S struct +type S struct { B string } + +func (s *S) String() string { return s.B } +` + + t.Run("with pointer", func(t *testing.T) { + m := gnolang.NewMachine("testdata", nil) + defer m.Release() + + const expected = "Hello World" + nn := gnolang.MustParseFile("struct.gno", StructsFile) + m.RunFiles(nn) + nn = gnolang.MustParseFile("testdata.gno", + fmt.Sprintf(`package testdata; var Value = S{%q}`, expected)) + m.RunFiles(nn) + m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) + + tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value")) + require.Len(t, tps, 1) + + tv := tps[0] + rep := JSONValue(m, tv) + require.Equal(t, strconv.Quote(expected), rep) + }) + + t.Run("without pointer", func(t *testing.T) { + m := gnolang.NewMachine("testdata", nil) + defer m.Release() + + const expected = "Hello World" + nn := gnolang.MustParseFile("struct.gno", StructsFile) + m.RunFiles(nn) + nn = gnolang.MustParseFile("testdata.gno", + fmt.Sprintf(`package testdata; var Value = &S{%q}`, expected)) + m.RunFiles(nn) + m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) + + tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value")) + require.Len(t, tps, 1) + + tv := tps[0] + rep := JSONValue(m, tv) + require.Equal(t, strconv.Quote(expected), rep) + }) + +} + +func TestConvertJSONValuesList(t *testing.T) { + cases := []struct { + ValueRep []string // Go representation + Expected string // string representation + }{ + // Boolean + {[]string{}, "[]"}, + {[]string{"42"}, "[42]"}, + {[]string{"42", `"hello world"`}, `[42, "hello world"]`}, + } + + for _, tc := range cases { + t.Run(strings.Join(tc.ValueRep, "-"), func(t *testing.T) { + m := gnolang.NewMachine("testdata", nil) + defer m.Release() + + nn := gnolang.MustParseFile("testdata.gno", + fmt.Sprintf(`package testdata; var Value = []interface{%s}`, strings.Join(tc.ValueRep, ","))) + m.RunFiles(nn) + m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) + + tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value")) + unwarp := tps[0].T.(*gnolang.TypeType) + fmt.Println(unwarp.String()) + require.Len(t, tps, 1) + // require.Equal(t, gnolang.ArrayKind.String(), unwarp.T.Kind().String()) + fmt.Println(tps[0].T.Kind()) + tpvs := tps[0].V.(*gnolang.ArrayValue).List + rep := JSONValues(m, tpvs) + require.Equal(t, tc.Expected, rep) + }) + } +} diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index 7b26265f35d..b2073373406 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -48,14 +48,14 @@ func (vh vmHandler) handleMsgAddPackage(ctx sdk.Context, msg MsgAddPackage) sdk. return sdk.Result{} } -// Handle MsgCall. func (vh vmHandler) handleMsgCall(ctx sdk.Context, msg MsgCall) (res sdk.Result) { resstr, err := vh.vm.Call(ctx, msg) if err != nil { return abciResult(err) } + res.Data = []byte(resstr) - return + return res } // Handle MsgRun. @@ -64,8 +64,9 @@ func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) { if err != nil { return abciResult(err) } + res.Data = []byte(resstr) - return + return res } // ---------------------------------------- diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 7d9ad4cc194..90389dfae41 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -14,6 +14,7 @@ import ( "sync" "time" + "github.com/gnolang/gno/gnovm/pkg/gnolang" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -504,7 +505,15 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { }() rtvs := m.Eval(xn) - res = JSONValues(m, rtvs...) + + res = JSONValues(m, rtvs) + + for i, rtv := range rtvs { + res = res + rtv.String() + if i < len(rtvs)-1 { + res += "\n" + } + } // Log the telemetry logTelemetry( @@ -757,7 +766,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res }() rtvs := m.Eval(xx) - res = JSONValues(m, rtvs...) + res = JSONValues(m, rtvs) return res, nil } @@ -855,6 +864,26 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err } +func stringifyResult(m *gno.Machine, format Format, values ...gnolang.TypedValue) string { + switch format { + case FormatJSON: + return JSONValues(m, values) + case FormatDefault, "": + var res strings.Builder + + for i, v := range values { + if i > 0 { + res.WriteRune('\n') + } + res.WriteString(v.String()) + } + + return res.String() + default: + panic("unsuported formata") + } +} + // logTelemetry logs the VM processing telemetry func logTelemetry( gasUsed int64, diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index d650c23f382..82a303d4667 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -11,6 +11,15 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) +type Format string + +const ( + FormatMachine = "machine" // Default machine representation + FormatJSON = "json" + + FormatDefault = FormatMachine +) + //---------------------------------------- // MsgAddPackage @@ -83,6 +92,7 @@ func (msg MsgAddPackage) GetReceived() std.Coins { // MsgCall - executes a Gno statement. type MsgCall struct { + Format string `json:"format" yaml:"format"` Caller crypto.Address `json:"caller" yaml:"caller"` Send std.Coins `json:"send" yaml:"send"` PkgPath string `json:"pkg_path" yaml:"pkg_path"` @@ -94,6 +104,7 @@ var _ std.Msg = MsgCall{} func NewMsgCall(caller crypto.Address, send sdk.Coins, pkgPath, fnc string, args []string) MsgCall { return MsgCall{ + Format: FormatDefault, Caller: caller, Send: send, PkgPath: pkgPath, diff --git a/gno.land/pkg/sdk/vm/types.go b/gno.land/pkg/sdk/vm/types.go index 442c2d4b138..e2649dc844c 100644 --- a/gno.land/pkg/sdk/vm/types.go +++ b/gno.land/pkg/sdk/vm/types.go @@ -1,6 +1,8 @@ package vm -import "github.com/gnolang/gno/tm2/pkg/amino" +import ( + "github.com/gnolang/gno/tm2/pkg/amino" +) // Public facing function signatures. // See convertArgToGno() for supported types. diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index bbf77bf19c7..229ee596734 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -88,6 +88,14 @@ func (bv BigintValue) MarshalAmino() (string, error) { return string(bz), nil } +func (bv BigintValue) MarshalAminoJSON() (string, error) { + bz, err := bv.V.MarshalText() + if err != nil { + return "", err + } + return string(bz), nil +} + func (bv *BigintValue) UnmarshalAmino(s string) error { vv := big.NewInt(0) err := vv.UnmarshalText([]byte(s)) diff --git a/tm2/pkg/bft/abci/types/types.go b/tm2/pkg/bft/abci/types/types.go index 42376e712a6..b07dc598337 100644 --- a/tm2/pkg/bft/abci/types/types.go +++ b/tm2/pkg/bft/abci/types/types.go @@ -99,12 +99,12 @@ type Response interface { } type ResponseBase struct { - Error Error - Data []byte - Events []Event + Error Error `json:"error"` + Data []byte `json:"data"` + Events []Event `json:"events"` - Log string // nondeterministic - Info string // nondeterministic + Log string `json:"log"` // nondeterministic + Info string `json:"info"` // nondeterministic } func (ResponseBase) AssertResponse() {} diff --git a/tm2/pkg/sdk/types.go b/tm2/pkg/sdk/types.go index 47395362f1a..bd4960e5c91 100644 --- a/tm2/pkg/sdk/types.go +++ b/tm2/pkg/sdk/types.go @@ -23,8 +23,8 @@ type Handler interface { // Result is the union of ResponseDeliverTx and ResponseCheckTx plus events. type Result struct { abci.ResponseBase - GasWanted int64 - GasUsed int64 + GasWanted int64 `json:"gas_wanted"` + GasUsed int64 `json:"gas_used"` } // AnteHandler authenticates transactions, before their internal messages are handled. From 44de899f8231ee9c569e1ffee38d5c11e8cb48af Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:53:37 +0200 Subject: [PATCH 03/12] fix: test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/sdk/vm/convert_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/gno.land/pkg/sdk/vm/convert_test.go b/gno.land/pkg/sdk/vm/convert_test.go index e28bf317a9e..934cbb6ec63 100644 --- a/gno.land/pkg/sdk/vm/convert_test.go +++ b/gno.land/pkg/sdk/vm/convert_test.go @@ -165,17 +165,14 @@ func TestConvertJSONValuesList(t *testing.T) { defer m.Release() nn := gnolang.MustParseFile("testdata.gno", - fmt.Sprintf(`package testdata; var Value = []interface{%s}`, strings.Join(tc.ValueRep, ","))) + fmt.Sprintf(`package testdata; var Value = []interface{}{%s}`, strings.Join(tc.ValueRep, ","))) m.RunFiles(nn) m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value")) - unwarp := tps[0].T.(*gnolang.TypeType) - fmt.Println(unwarp.String()) require.Len(t, tps, 1) - // require.Equal(t, gnolang.ArrayKind.String(), unwarp.T.Kind().String()) - fmt.Println(tps[0].T.Kind()) - tpvs := tps[0].V.(*gnolang.ArrayValue).List + require.Equal(t, gnolang.SliceKind.String(), tps[0].T.Kind().String()) + tpvs := tps[0].V.(*gnolang.SliceValue).Base.(*gnolang.ArrayValue).List rep := JSONValues(m, tpvs) require.Equal(t, tc.Expected, rep) }) From aca85fbcd9cd6793198501b5e2fb4fee5e8f8317 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:25:54 +0200 Subject: [PATCH 04/12] wip: json primitive next Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/sdk/vm/convert.go | 6 +++--- gno.land/pkg/sdk/vm/convert_test.go | 28 ++++++++++++++-------------- gno.land/pkg/sdk/vm/keeper.go | 19 +++++-------------- gno.land/pkg/sdk/vm/msgs.go | 4 +++- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/gno.land/pkg/sdk/vm/convert.go b/gno.land/pkg/sdk/vm/convert.go index 83de937ba8c..296f36d7530 100644 --- a/gno.land/pkg/sdk/vm/convert.go +++ b/gno.land/pkg/sdk/vm/convert.go @@ -192,7 +192,7 @@ func convertArgToGno(arg string, argT gno.Type) (tv gno.TypedValue) { } } -func JSONValues(m *gno.Machine, tvs []gno.TypedValue) string { +func JSONPrimitiveValues(m *gno.Machine, tvs []gno.TypedValue) string { var str strings.Builder str.WriteRune('[') @@ -200,14 +200,14 @@ func JSONValues(m *gno.Machine, tvs []gno.TypedValue) string { if i > 0 { str.WriteRune(',') } - str.WriteString(JSONValue(m, tv)) + str.WriteString(JSONPrimitiveValue(m, tv)) } str.WriteRune(']') return str.String() } -func JSONValue(m *gno.Machine, tv gno.TypedValue) string { +func JSONPrimitiveValue(m *gno.Machine, tv gno.TypedValue) string { if tv.T == nil { return "null" } diff --git a/gno.land/pkg/sdk/vm/convert_test.go b/gno.land/pkg/sdk/vm/convert_test.go index 934cbb6ec63..a600ccd9678 100644 --- a/gno.land/pkg/sdk/vm/convert_test.go +++ b/gno.land/pkg/sdk/vm/convert_test.go @@ -67,9 +67,9 @@ func TestConvertJSONValuePrimtive(t *testing.T) { {"uint32(42)", `42`}, {"uint64(42)", `42`}, - // Float types // XXX: Require amino unsafe - // {"float32(3.14)", "3.14"}, - // {"float64(3.14)", "3.14"}, + // Float types + {"float32(3.14)", "3.14"}, + {"float64(3.14)", "3.14"}, // String type {`"hello world"`, `"hello world"`}, @@ -90,7 +90,7 @@ func TestConvertJSONValuePrimtive(t *testing.T) { tv := tps[0] - rep := JSONValue(m, tv) + rep := JSONPrimitiveValue(m, tv) require.Equal(t, tc.Expected, rep) }) } @@ -100,10 +100,10 @@ func TestConvertJSONValueStruct(t *testing.T) { const StructsFile = ` package testdata -// S struct -type S struct { B string } +// E struct, impement error +type E struct { S string } -func (s *S) String() string { return s.B } +func (e *E) Error() string { return e.S } ` t.Run("with pointer", func(t *testing.T) { @@ -114,7 +114,7 @@ func (s *S) String() string { return s.B } nn := gnolang.MustParseFile("struct.gno", StructsFile) m.RunFiles(nn) nn = gnolang.MustParseFile("testdata.gno", - fmt.Sprintf(`package testdata; var Value = S{%q}`, expected)) + fmt.Sprintf(`package testdata; var Value = E{%q}`, expected)) m.RunFiles(nn) m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) @@ -122,7 +122,7 @@ func (s *S) String() string { return s.B } require.Len(t, tps, 1) tv := tps[0] - rep := JSONValue(m, tv) + rep := JSONPrimitiveValue(m, tv) require.Equal(t, strconv.Quote(expected), rep) }) @@ -134,7 +134,7 @@ func (s *S) String() string { return s.B } nn := gnolang.MustParseFile("struct.gno", StructsFile) m.RunFiles(nn) nn = gnolang.MustParseFile("testdata.gno", - fmt.Sprintf(`package testdata; var Value = &S{%q}`, expected)) + fmt.Sprintf(`package testdata; var Value = &E{%q}`, expected)) m.RunFiles(nn) m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) @@ -142,10 +142,9 @@ func (s *S) String() string { return s.B } require.Len(t, tps, 1) tv := tps[0] - rep := JSONValue(m, tv) + rep := JSONPrimitiveValue(m, tv) require.Equal(t, strconv.Quote(expected), rep) }) - } func TestConvertJSONValuesList(t *testing.T) { @@ -156,7 +155,8 @@ func TestConvertJSONValuesList(t *testing.T) { // Boolean {[]string{}, "[]"}, {[]string{"42"}, "[42]"}, - {[]string{"42", `"hello world"`}, `[42, "hello world"]`}, + {[]string{"42", `"hello world"`}, `[42,"hello world"]`}, + {[]string{"42", `"hello world"`, "[]int{42}"}, `[42,"hello world"]`}, } for _, tc := range cases { @@ -173,7 +173,7 @@ func TestConvertJSONValuesList(t *testing.T) { require.Len(t, tps, 1) require.Equal(t, gnolang.SliceKind.String(), tps[0].T.Kind().String()) tpvs := tps[0].V.(*gnolang.SliceValue).Base.(*gnolang.ArrayValue).List - rep := JSONValues(m, tpvs) + rep := JSONPrimitiveValues(m, tpvs) require.Equal(t, tc.Expected, rep) }) } diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 90389dfae41..e7daecc5ee7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -505,16 +505,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { }() rtvs := m.Eval(xn) - - res = JSONValues(m, rtvs) - - for i, rtv := range rtvs { - res = res + rtv.String() - if i < len(rtvs)-1 { - res += "\n" - } - } - + stringifyResultValues(m, msg.Format, rtvs) // Log the telemetry logTelemetry( m.GasMeter.GasConsumed(), @@ -766,7 +757,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res }() rtvs := m.Eval(xx) - res = JSONValues(m, rtvs) + res = stringifyResultValues(m, msg.Format, rtvs) return res, nil } @@ -864,10 +855,10 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err } -func stringifyResult(m *gno.Machine, format Format, values ...gnolang.TypedValue) string { +func stringifyResultValues(m *gno.Machine, format Format, values []gnolang.TypedValue) string { switch format { case FormatJSON: - return JSONValues(m, values) + return JSONPrimitiveValues(m, values) case FormatDefault, "": var res strings.Builder @@ -880,7 +871,7 @@ func stringifyResult(m *gno.Machine, format Format, values ...gnolang.TypedValue return res.String() default: - panic("unsuported formata") + panic("unsuported stringify format ") } } diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 82a303d4667..d5086f7dbeb 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -92,12 +92,14 @@ func (msg MsgAddPackage) GetReceived() std.Coins { // MsgCall - executes a Gno statement. type MsgCall struct { - Format string `json:"format" yaml:"format"` Caller crypto.Address `json:"caller" yaml:"caller"` Send std.Coins `json:"send" yaml:"send"` PkgPath string `json:"pkg_path" yaml:"pkg_path"` Func string `json:"func" yaml:"func"` Args []string `json:"args" yaml:"args"` + + // XXX: This field is experimental, use with care as output is likely to change + Format Format `json:"format" yaml:"format"` } var _ std.Msg = MsgCall{} From 6347f2571bebefb09d704cdd3ff87a8fc17f74c4 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:46:01 +0100 Subject: [PATCH 05/12] wip: add format to query eval Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoland/app.go | 1 + gno.land/pkg/sdk/vm/convert.go | 15 ++----------- gno.land/pkg/sdk/vm/convert_test.go | 24 +++++++++++++++++++-- gno.land/pkg/sdk/vm/handler.go | 33 ++++++++++++++++++++--------- gno.land/pkg/sdk/vm/keeper.go | 7 ++++-- 5 files changed, 53 insertions(+), 27 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index e0c93f6194f..3fcacb1771b 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -388,6 +388,7 @@ func EndBlocker( ctx, valRealm, fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), + vm.FormatMachine, ) if err != nil { app.Logger().Error("unable to call VM during EndBlocker", "err", err) diff --git a/gno.land/pkg/sdk/vm/convert.go b/gno.land/pkg/sdk/vm/convert.go index 296f36d7530..2ad1590aa06 100644 --- a/gno.land/pkg/sdk/vm/convert.go +++ b/gno.land/pkg/sdk/vm/convert.go @@ -253,20 +253,14 @@ func JSONPrimitiveValue(m *gno.Machine, tv gno.TypedValue) string { panic("invalid primitive type - should not happen") } case *gno.PointerType: - // Check if Pointer we type implement Stringer / Error - + // Check if Pointer we type implement Error // If implements .Error(), return it. if tv.IsError() { res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "Error"))) return strconv.Quote(res[0].GetString()) } - // If implements .String(), return it. - if tv.IsStringer() { - res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "String"))) - return strconv.Quote(res[0].GetString()) - } default: - // Check if pointer wraped value can implement Stringer / Error + // Check if pointer wraped value can implement Error ptv := gno.TypedValue{ T: &gno.PointerType{Elt: tv.T}, V: gno.PointerValue{TV: &tv, Base: tv.V}, @@ -277,11 +271,6 @@ func JSONPrimitiveValue(m *gno.Machine, tv gno.TypedValue) string { res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: ptv}, "Error"))) return strconv.Quote(res[0].GetString()) } - // If implements .String(), return it. - if ptv.IsStringer() { - res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: ptv}, "String"))) - return strconv.Quote(res[0].GetString()) - } } if tv.V == nil { diff --git a/gno.land/pkg/sdk/vm/convert_test.go b/gno.land/pkg/sdk/vm/convert_test.go index a600ccd9678..a90e0fbf8a3 100644 --- a/gno.land/pkg/sdk/vm/convert_test.go +++ b/gno.land/pkg/sdk/vm/convert_test.go @@ -106,7 +106,7 @@ type E struct { S string } func (e *E) Error() string { return e.S } ` - t.Run("with pointer", func(t *testing.T) { + t.Run("null pointer", func(t *testing.T) { m := gnolang.NewMachine("testdata", nil) defer m.Release() @@ -114,7 +114,7 @@ func (e *E) Error() string { return e.S } nn := gnolang.MustParseFile("struct.gno", StructsFile) m.RunFiles(nn) nn = gnolang.MustParseFile("testdata.gno", - fmt.Sprintf(`package testdata; var Value = E{%q}`, expected)) + fmt.Sprintf(`package testdata; var Value *E = nil`, expected)) m.RunFiles(nn) m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) @@ -130,6 +130,26 @@ func (e *E) Error() string { return e.S } m := gnolang.NewMachine("testdata", nil) defer m.Release() + const expected = "Hello World" + nn := gnolang.MustParseFile("struct.gno", StructsFile) + m.RunFiles(nn) + nn = gnolang.MustParseFile("testdata.gno", + fmt.Sprintf(`package testdata; var Value = E{%q}`, expected)) + m.RunFiles(nn) + m.RunDeclaration(gnolang.ImportD("testdata", "testdata")) + + tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value")) + require.Len(t, tps, 1) + + tv := tps[0] + rep := JSONPrimitiveValue(m, tv) + require.Equal(t, strconv.Quote(expected), rep) + }) + + t.Run("with pointer", func(t *testing.T) { + m := gnolang.NewMachine("testdata", nil) + defer m.Release() + const expected = "Hello World" nn := gnolang.MustParseFile("struct.gno", StructsFile) m.RunFiles(nn) diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index b2073373406..7a72d126fa5 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -74,12 +74,13 @@ func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) { // query paths const ( - QueryPackage = "package" - QueryStore = "store" - QueryRender = "qrender" - QueryFuncs = "qfuncs" - QueryEval = "qeval" - QueryFile = "qfile" + QueryPackage = "package" + QueryStore = "store" + QueryRender = "qrender" + QueryFuncs = "qfuncs" + QueryEval = "qeval" + QueryEvalJSON = "qeval/json" // EXPERIMENTAL + QueryFile = "qfile" ) func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery { @@ -98,7 +99,9 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQ case QueryFuncs: res = vh.queryFuncs(ctx, req) case QueryEval: - res = vh.queryEval(ctx, req) + res = vh.queryEvalMachine(ctx, req) + case QueryEvalJSON: + res = vh.queryEvalJSON(ctx, req) case QueryFile: res = vh.queryFile(ctx, req) default: @@ -179,10 +182,10 @@ func (vh vmHandler) queryFuncs(ctx sdk.Context, req abci.RequestQuery) (res abci return } -// queryEval evaluates any expression in readonly mode and returns the results. -func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { +// queryEval evaluates any expression in readonly mode and returns the results based on the given format. +func (vh vmHandler) queryEval(ctx sdk.Context, format Format, req abci.RequestQuery) (res abci.ResponseQuery) { pkgPath, expr := parseQueryEvalData(string(req.Data)) - result, err := vh.vm.QueryEval(ctx, pkgPath, expr) + result, err := vh.vm.QueryEval(ctx, pkgPath, expr, FormatMachine) if err != nil { res = sdk.ABCIResponseQueryFromError(err) return @@ -191,6 +194,16 @@ func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci. return } +// queryEvalMachine evaluates any expression in readonly mode and returns the results in vm machine format. +func (vh vmHandler) queryEvalMachine(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { + return vh.queryEval(ctx, FormatMachine, req) +} + +// queryEvalJSON evaluates any expression in readonly mode and returns the results in JSON format. +func (vh vmHandler) queryEvalJSON(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { + return vh.queryEval(ctx, FormatJSON, req) +} + // parseQueryEval parses the input string of vm/qeval. It takes the first dot // after the first slash (if any) to separe the pkgPath and the expr. // For instance, in gno.land/r/realm.MyFunction(), gno.land/r/realm is the diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index f315d733876..2cc06a732fd 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -46,7 +46,7 @@ const ( 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) + QueryEval(ctx sdk.Context, pkgPath string, expr string, format Format) (res string, err error) Run(ctx sdk.Context, msg MsgRun) (res string, err error) LoadStdlib(ctx sdk.Context, stdlibDir string) LoadStdlibCached(ctx sdk.Context, stdlibDir string) @@ -514,6 +514,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { rtvs := m.Eval(xn) stringifyResultValues(m, msg.Format, rtvs) + // Log the telemetry logTelemetry( m.GasMeter.GasConsumed(), @@ -725,11 +726,13 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res "package not found: %s", pkgPath)) return "", err } + // Parse expression. xx, err := gno.ParseExpr(expr) if err != nil { return "", err } + // Construct new machine. msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), @@ -768,7 +771,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res }() rtvs := m.Eval(xx) - res = stringifyResultValues(m, msg.Format, rtvs) + res = stringifyResultValues(m, format, rtvs) return res, nil } From e906187c0dfcd0e313f76fda10bcfaf45d14584e Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:49:46 +0100 Subject: [PATCH 06/12] feat: add msg eval Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/sdk/vm/handler.go | 13 +++++++ gno.land/pkg/sdk/vm/keeper.go | 66 +++++++++++++++++++++++++++++++--- gno.land/pkg/sdk/vm/msgs.go | 59 +++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index 7a72d126fa5..86b7660eb35 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -33,6 +33,8 @@ func (vh vmHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result { return vh.handleMsgCall(ctx, msg) case MsgRun: return vh.handleMsgRun(ctx, msg) + case MsgEval: + return vh.handleMsgEval(ctx, msg) default: errMsg := fmt.Sprintf("unrecognized vm message type: %T", msg) return abciResult(std.ErrUnknownRequest(errMsg)) @@ -58,6 +60,17 @@ func (vh vmHandler) handleMsgCall(ctx sdk.Context, msg MsgCall) (res sdk.Result) return res } +// Handle MsgEval. +func (vh vmHandler) handleMsgEval(ctx sdk.Context, msg MsgEval) (res sdk.Result) { + resstr, err := vh.vm.Eval(ctx, msg) + if err != nil { + return abciResult(err) + } + + res.Data = []byte(resstr) + return res +} + // Handle MsgRun. func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) { resstr, err := vh.vm.Run(ctx, msg) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 2cc06a732fd..fcf381a21a0 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -712,10 +712,68 @@ func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionS return fsigs, nil } -// QueryEval evaluates a gno expression (readonly, for ABCI queries). -// TODO: modify query protocol to allow MsgEval. -// TODO: then, rename to "Eval". -func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { +func (vm *VMKeeper) Eval(ctx sdk.Context, msg MsgEval) (res string, err error) { + alloc := gno.NewAllocator(maxAllocQuery) + gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) + pkgAddr := gno.DerivePkgAddr(msg.PkgPath) + + // Get Package. + pv := gnostore.GetPackage(msg.PkgPath, false) + if pv == nil { + err = ErrInvalidPkgPath(fmt.Sprintf( + "package not found: %s", msg.PkgPath)) + return "", err + } + + // Parse expression. + xx, err := gno.ParseExpr(msg.Expr) + if err != nil { + return "", err + } + + // Construct new machine. + msgCtx := stdlibs.ExecContext{ + ChainID: ctx.ChainID(), + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), + // Msg: msg, + // OrigCaller: caller, + // OrigSend: send, + // OrigSendSpent: nil, + OrigPkgAddr: pkgAddr.Bech32(), + Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. + Params: NewSDKParams(vm, ctx), + EventLogger: ctx.EventLogger(), + } + m := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: msg.PkgPath, + Output: vm.Output, + Store: gnostore, + Context: msgCtx, + Alloc: alloc, + GasMeter: ctx.GasMeter(), + }) + defer m.Release() + defer func() { + if r := recover(); r != nil { + switch r.(type) { + case store.OutOfGasException: // panic in consumeGas() + panic(r) + default: + err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n", + r, m.String()) + return + } + } + }() + + rtvs := m.Eval(xx) + res = stringifyResultValues(m, msg.Format, rtvs) + return res, nil +} + +func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string, format Format) (res string, err error) { alloc := gno.NewAllocator(maxAllocQuery) gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) pkgAddr := gno.DerivePkgAddr(pkgPath) diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 6092ec5da7f..cf41a2d55ad 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -16,7 +16,7 @@ type Format string const ( FormatMachine = "machine" // Default machine representation - FormatJSON = "json" + FormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now FormatDefault = FormatMachine ) @@ -88,6 +88,63 @@ func (msg MsgAddPackage) GetReceived() std.Coins { return msg.Deposit } +//---------------------------------------- +// MsgEval + +// MsgEval - eval a Gno Expr. +type MsgEval struct { + Caller crypto.Address `json:"caller" yaml:"caller"` + PkgPath string `json:"pkg_path" yaml:"pkg_path"` + Expr string `json:"expr" yaml:"expr"` + + // XXX: This field is experimental, use with care as output is likely to change + Format Format `json:"format" yaml:"format"` +} + +var _ std.Msg = MsgEval{} + +func NewMsgEval(caller crypto.Address, format Format, pkgPath, expr string) MsgEval { + return MsgEval{ + Caller: caller, + PkgPath: pkgPath, + Format: FormatDefault, + } +} + +// Implements Msg. +func (msg MsgEval) Route() string { return RouterKey } + +// Implements Msg. +func (msg MsgEval) Type() string { return "eval" } + +// Implements Msg. +func (msg MsgEval) ValidateBasic() error { + if msg.Caller.IsZero() { + return std.ErrInvalidAddress("missing caller address") + } + if msg.PkgPath == "" { + return ErrInvalidPkgPath("missing package path") + } + if !gno.IsRealmPath(msg.PkgPath) { + return ErrInvalidPkgPath("pkgpath must be of a realm") + } + if msg.Expr == "" { // XXX + return ErrInvalidExpr("missing expr to eval") + } + + return nil +} + +// Implements Msg. +func (msg MsgEval) GetSignBytes() []byte { + return std.MustSortJSON(amino.MustMarshalJSON(msg)) +} + +// Implements Msg. +func (msg MsgEval) GetSigners() []crypto.Address { + return []crypto.Address{msg.Caller} +} + //---------------------------------------- // MsgCall From cf7e6d0fda47649c1392b72ba018c912208ceb94 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:14:31 +0100 Subject: [PATCH 07/12] chore: cleanup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/sdk/vm/keeper.go | 2 +- gno.land/pkg/sdk/vm/msgs.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index fcf381a21a0..d7458f9af91 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -513,7 +513,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { }() rtvs := m.Eval(xn) - stringifyResultValues(m, msg.Format, rtvs) + res = stringifyResultValues(m, msg.Format, rtvs) // Log the telemetry logTelemetry( diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index cf41a2d55ad..a37d33eddde 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -128,7 +128,7 @@ func (msg MsgEval) ValidateBasic() error { if !gno.IsRealmPath(msg.PkgPath) { return ErrInvalidPkgPath("pkgpath must be of a realm") } - if msg.Expr == "" { // XXX + if msg.Expr == "" { return ErrInvalidExpr("missing expr to eval") } From e5d26e1d64734b5f67dac63b7aa44d37854fd366 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:47:52 +0100 Subject: [PATCH 08/12] wip: use Eval keeper function for query Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoland/app.go | 9 +- gno.land/pkg/sdk/vm/handler.go | 75 ++++++++++++----- gno.land/pkg/sdk/vm/keeper.go | 147 ++++++--------------------------- gno.land/pkg/sdk/vm/msgs.go | 40 +++++---- gno.land/pkg/sdk/vm/package.go | 1 + 5 files changed, 103 insertions(+), 169 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 965ee0f9f99..85816ee79ef 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -405,12 +405,9 @@ func EndBlocker( } // Run the VM to get the updates from the chain - response, err := vmk.QueryEval( - ctx, - valRealm, - fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), - vm.FormatMachine, - ) + expr := fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()) + msgEval := vm.NewMsgEval(vm.ResultFormatMachine, valRealm, expr) + response, err := vmk.Eval(ctx, msgEval) if err != nil { app.Logger().Error("unable to call VM during EndBlocker", "err", err) diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index e957fa63edd..87170131018 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -82,13 +82,12 @@ func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) { // query paths const ( - QueryPackage = "package" - QueryStore = "store" - QueryRender = "qrender" - QueryFuncs = "qfuncs" - QueryEval = "qeval" - QueryEvalJSON = "qeval/json" // EXPERIMENTAL - QueryFile = "qfile" + QueryPackage = "package" + QueryStore = "store" + QueryRender = "qrender" + QueryFuncs = "qfuncs" + QueryEval = "qeval" + QueryFile = "qfile" ) func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery { @@ -106,12 +105,10 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQ res = vh.queryRender(ctx, req) case QueryFuncs: res = vh.queryFuncs(ctx, req) - case QueryEval: - res = vh.queryEvalMachine(ctx, req) - case QueryEvalJSON: - res = vh.queryEvalJSON(ctx, req) case QueryFile: res = vh.queryFile(ctx, req) + case QueryEval: + res = vh.queryEval(ctx, req) default: return sdk.ABCIResponseQueryFromError( std.ErrUnknownRequest(fmt.Sprintf( @@ -143,8 +140,13 @@ func (vh vmHandler) queryRender(ctx sdk.Context, req abci.RequestQuery) (res abc } pkgPath, path := reqData[:dot], reqData[dot+1:] + + // Generate msg eval request expr := fmt.Sprintf("Render(%q)", path) - result, err := vh.vm.QueryEvalString(ctx, pkgPath, expr) + msgEval := NewMsgEval(ResultFormatString, pkgPath, expr) + + // Try evaluate `Render` function + result, err := vh.vm.Eval(ctx, msgEval) if err != nil { res = sdk.ABCIResponseQueryFromError(err) return @@ -166,25 +168,56 @@ func (vh vmHandler) queryFuncs(ctx sdk.Context, req abci.RequestQuery) (res abci } // queryEval evaluates any expression in readonly mode and returns the results based on the given format. -func (vh vmHandler) queryEval(ctx sdk.Context, format Format, req abci.RequestQuery) (res abci.ResponseQuery) { - pkgPath, expr := parseQueryEvalData(string(req.Data)) - result, err := vh.vm.QueryEval(ctx, pkgPath, expr, FormatMachine) +func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { + var format ResultFormat + switch ss := strings.Split(req.Path, "/"); len(ss) { + case 2: + format = ResultFormatDefault + case 3: + format = ResultFormat(ss[2]) + default: + res = sdk.ABCIResponseQueryFromError(fmt.Errorf("invalid query")) + return + + } + + switch format { + case ResultFormatMachine, ResultFormatJSON, ResultFormatString: + default: + err := fmt.Errorf("invalid query result format %q", format) + res = sdk.ABCIResponseQueryFromError(err) + return + } + + pkgpath, expr := parseQueryEvalData(string(req.Data)) + fmt.Printf("-> %q %q %q\r\n", format, pkgpath, expr) + msgEval := NewMsgEval(format, pkgpath, expr) + if expr == "" { + res = sdk.ABCIResponseQueryFromError(fmt.Errorf("expr cannot be empty")) + return + } + + result, err := vh.vm.Eval(ctx, msgEval) if err != nil { res = sdk.ABCIResponseQueryFromError(err) return } + res.Data = []byte(result) return } -// queryEvalMachine evaluates any expression in readonly mode and returns the results in vm machine format. -func (vh vmHandler) queryEvalMachine(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - return vh.queryEval(ctx, FormatMachine, req) -} - // queryEvalJSON evaluates any expression in readonly mode and returns the results in JSON format. func (vh vmHandler) queryEvalJSON(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - return vh.queryEval(ctx, FormatJSON, req) + pkgath, expr := parseQueryEvalData(string(req.Data)) + msgEval := NewMsgEval(ResultFormatJSON, pkgath, expr) + result, err := vh.vm.Eval(ctx, msgEval) + if err != nil { + res = sdk.ABCIResponseQueryFromError(err) + return + } + res.Data = []byte(result) + return } // parseQueryEval parses the input string of vm/qeval. It takes the first dot diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 99340c8f329..2dc56b5d036 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -11,6 +11,7 @@ import ( "log/slog" "path/filepath" "regexp" + "strconv" "strings" "sync" "time" @@ -47,7 +48,7 @@ const ( 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, format Format) (res string, err error) + Eval(ctx sdk.Context, msg MsgEval) (res string, err error) Run(ctx sdk.Context, msg MsgRun) (res string, err error) LoadStdlib(ctx sdk.Context, stdlibDir string) LoadStdlibCached(ctx sdk.Context, stdlibDir string) @@ -756,128 +757,12 @@ func (vm *VMKeeper) Eval(ctx sdk.Context, msg MsgEval) (res string, err error) { Alloc: alloc, GasMeter: ctx.GasMeter(), }) - defer m.Release() - defer func() { - if r := recover(); r != nil { - switch r.(type) { - case store.OutOfGasException: // panic in consumeGas() - panic(r) - default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n", - r, m.String()) - return - } - } - }() - - rtvs := m.Eval(xx) - res = stringifyResultValues(m, msg.Format, rtvs) - return res, nil -} -func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string, format Format) (res string, err error) { - alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) - pkgAddr := gno.DerivePkgAddr(pkgPath) - // Get Package. - pv := gnostore.GetPackage(pkgPath, false) - if pv == nil { - err = ErrInvalidPkgPath(fmt.Sprintf( - "package not found: %s", pkgPath)) - return "", err - } - - // Parse expression. - xx, err := gno.ParseExpr(expr) - if err != nil { - return "", err - } - - // Construct new machine. - chainDomain := vm.getChainDomainParam(ctx) - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - ChainDomain: chainDomain, - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - // OrigCaller: caller, - // OrigSend: send, - // OrigSendSpent: nil, - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - Params: NewSDKParams(vm, ctx), - EventLogger: ctx.EventLogger(), - } - m := gno.NewMachineWithOptions( - gno.MachineOptions{ - PkgPath: pkgPath, - Output: vm.Output, - Store: gnostore, - Context: msgCtx, - Alloc: alloc, - GasMeter: ctx.GasMeter(), - }) defer m.Release() defer doRecover(m, &err) - rtvs := m.Eval(xx) - res = stringifyResultValues(m, format, rtvs) - return res, nil -} - -// QueryEvalString evaluates a gno expression (readonly, for ABCI queries). -// The result is expected to be a single string (not a tuple). -// TODO: modify query protocol to allow MsgEval. -// TODO: then, rename to "EvalString". -func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { - alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) - pkgAddr := gno.DerivePkgAddr(pkgPath) - // Get Package. - pv := gnostore.GetPackage(pkgPath, false) - if pv == nil { - err = ErrInvalidPkgPath(fmt.Sprintf( - "package not found: %s", pkgPath)) - return "", err - } - // Parse expression. - xx, err := gno.ParseExpr(expr) - if err != nil { - return "", err - } - // Construct new machine. - chainDomain := vm.getChainDomainParam(ctx) - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - ChainDomain: chainDomain, - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - // OrigCaller: caller, - // OrigSend: jsend, - // OrigSendSpent: nil, - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - Params: NewSDKParams(vm, ctx), - EventLogger: ctx.EventLogger(), - } - m := gno.NewMachineWithOptions( - gno.MachineOptions{ - PkgPath: pkgPath, - Output: vm.Output, - Store: gnostore, - Context: msgCtx, - Alloc: alloc, - GasMeter: ctx.GasMeter(), - }) - defer m.Release() - defer doRecover(m, &err) rtvs := m.Eval(xx) - if len(rtvs) != 1 { - return "", errors.New("expected 1 string result, got %d", len(rtvs)) - } else if rtvs[0].T.Kind() != gno.StringKind { - return "", errors.New("expected 1 string result, got %v", rtvs[0].T.Kind()) - } - res = rtvs[0].GetString() + res = stringifyResultValues(m, msg.ResultFormat, rtvs) return res, nil } @@ -909,11 +794,31 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err } -func stringifyResultValues(m *gno.Machine, format Format, values []gnolang.TypedValue) string { +func stringifyResultValues(m *gno.Machine, format ResultFormat, values []gnolang.TypedValue) string { switch format { - case FormatJSON: + case ResultFormatString: + if len(values) != 1 { + panic(fmt.Errorf("expected 1 string result, got %d", len(values))) + } + + tv := values[0] + switch bt := gno.BaseOf(tv.T).(type) { + case gno.PrimitiveType: + if bt.Kind() == gno.StringKind { + return tv.GetString() + } + case *gno.PointerType: + if tv.IsStringer() { + res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "String"))) + return strconv.Quote(res[0].GetString()) + } + } + + panic(fmt.Errorf("expected 1 `string` or `Stringer` result, got %v", tv.T.Kind())) + + case ResultFormatJSON: return JSONPrimitiveValues(m, values) - case FormatDefault, "": + case ResultFormatDefault, "": var res strings.Builder for i, v := range values { diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 777297ff9de..aea1b1be09a 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -12,13 +12,13 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -type Format string +type ResultFormat string const ( - FormatMachine = "machine" // Default machine representation - FormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now - - FormatDefault = FormatMachine + ResultFormatMachine ResultFormat = "machine" // Default machine representation + ResultFormatString = "string" // Single string represnetation + ResultFormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now + ResultFormatDefault = ResultFormatMachine ) //---------------------------------------- @@ -93,21 +93,24 @@ func (msg MsgAddPackage) GetReceived() std.Coins { // MsgEval - eval a Gno Expr. type MsgEval struct { - Caller crypto.Address `json:"caller" yaml:"caller"` - PkgPath string `json:"pkg_path" yaml:"pkg_path"` - Expr string `json:"expr" yaml:"expr"` + PkgPath string `json:"pkg_path" yaml:"pkg_path"` + Expr string `json:"expr" yaml:"expr"` + + // Caller will be use for signing only + Caller crypto.Address `json:"caller" yaml:"caller"` // XXX: This field is experimental, use with care as output is likely to change - Format Format `json:"format" yaml:"format"` + // Default format is machine + ResultFormat ResultFormat `json:"format" yaml:"format"` } var _ std.Msg = MsgEval{} -func NewMsgEval(caller crypto.Address, format Format, pkgPath, expr string) MsgEval { +func NewMsgEval(format ResultFormat, pkgPath, expr string) MsgEval { return MsgEval{ - Caller: caller, - PkgPath: pkgPath, - Format: FormatDefault, + PkgPath: pkgPath, + Expr: expr, + ResultFormat: format, } } @@ -122,12 +125,7 @@ func (msg MsgEval) ValidateBasic() error { if msg.Caller.IsZero() { return std.ErrInvalidAddress("missing caller address") } - if msg.PkgPath == "" { - return ErrInvalidPkgPath("missing package path") - } - if !gno.IsRealmPath(msg.PkgPath) { - return ErrInvalidPkgPath("pkgpath must be of a realm") - } + if msg.Expr == "" { return ErrInvalidExpr("missing expr to eval") } @@ -157,14 +155,14 @@ type MsgCall struct { Args []string `json:"args" yaml:"args"` // XXX: This field is experimental, use with care as output is likely to change - Format Format `json:"format" yaml:"format"` + Format ResultFormat `json:"format" yaml:"format"` } var _ std.Msg = MsgCall{} func NewMsgCall(caller crypto.Address, send sdk.Coins, pkgPath, fnc string, args []string) MsgCall { return MsgCall{ - Format: FormatDefault, + Format: ResultFormatDefault, Caller: caller, Send: send, PkgPath: pkgPath, diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index 0359061ccea..abdeb9089da 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -14,6 +14,7 @@ var Package = amino.RegisterPackage(amino.NewPackage( std.Package, gnovm.Package, ).WithTypes( + MsgEval{}, "m_eval", MsgCall{}, "m_call", MsgRun{}, "m_run", MsgAddPackage{}, "m_addpkg", // TODO rename both to MsgAddPkg? From 424308af8f1303bac139acda7fb4a0368e497736 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:48:10 +0100 Subject: [PATCH 09/12] feat: add jquery to gnokey Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/keyscli/query_json.go | 75 ++++++++++++++++++++++++++++++ gno.land/pkg/keyscli/root.go | 3 ++ 2 files changed, 78 insertions(+) create mode 100644 gno.land/pkg/keyscli/query_json.go diff --git a/gno.land/pkg/keyscli/query_json.go b/gno.land/pkg/keyscli/query_json.go new file mode 100644 index 00000000000..b49bb26d10e --- /dev/null +++ b/gno.land/pkg/keyscli/query_json.go @@ -0,0 +1,75 @@ +package keyscli + +import ( + "context" + "encoding/json" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" +) + +func NewQueryJSONCmd(rootCfg *client.BaseCfg, io commands.IO) *commands.Command { + cfg := &client.QueryCfg{ + RootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "jquery", + ShortUsage: "jquery [flags] ", + ShortHelp: "makes an ABCI query expecting json reply", + }, + cfg, + func(_ context.Context, args []string) error { + return execQuery(cfg, args, io) + }, + ) +} + +func execQuery(cfg *client.QueryCfg, args []string, io commands.IO) error { + if len(args) != 1 { + return flag.ErrHelp + } + + cfg.Path = args[0] + if cfg.Path == "vm/qeval" { + // automatically add json suffix for qeval + cfg.Path = cfg.Path + "/json" + } + + qres, err := client.QueryHandler(cfg) + if err != nil { + return err + } + + var output struct { + Response json.RawMessage `json:"response"` + Returns json.RawMessage `json:"returns,omitempty"` + } + + if output.Response, err = amino.MarshalJSONIndent(qres.Response, "", " "); err != nil { + io.ErrPrintfln("Unable to marshal response %+v\n", qres) + return fmt.Errorf("amino marshal json error: %w", err) + } + + // XXX: this is to specific + if cfg.Path == "vm/qeval/json" { + if len(qres.Response.Data) > 0 { + output.Returns = qres.Response.Data + } else { + output.Returns = []byte("[]") + } + } + + res, err := json.MarshalIndent(output, "", " ") + if err != nil { + io.ErrPrintfln("Unable to marshal output %+v\n", qres) + return fmt.Errorf("marshal json error: %w", err) + } + + io.Println(string(res)) + return nil +} diff --git a/gno.land/pkg/keyscli/root.go b/gno.land/pkg/keyscli/root.go index c910e01b82c..77237c25dee 100644 --- a/gno.land/pkg/keyscli/root.go +++ b/gno.land/pkg/keyscli/root.go @@ -40,6 +40,9 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command { client.NewQueryCmd(cfg, io), client.NewBroadcastCmd(cfg, io), + // Custom query command + NewQueryJSONCmd(cfg, io), + // Custom MakeTX command NewMakeTxCmd(cfg, io), ) From 9aa7139ecb49a854346878e44ad0f444867fdde4 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:00:20 +0100 Subject: [PATCH 10/12] chore: doc Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/keyscli/query_json.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gno.land/pkg/keyscli/query_json.go b/gno.land/pkg/keyscli/query_json.go index b49bb26d10e..43b0abee502 100644 --- a/gno.land/pkg/keyscli/query_json.go +++ b/gno.land/pkg/keyscli/query_json.go @@ -20,7 +20,8 @@ func NewQueryJSONCmd(rootCfg *client.BaseCfg, io commands.IO) *commands.Command commands.Metadata{ Name: "jquery", ShortUsage: "jquery [flags] ", - ShortHelp: "makes an ABCI query expecting json reply", + ShortHelp: "EXPERIMENTAL: makes an ABCI query and return a result in json", + LongHelp: "EXPERIMENTAL: makes an ABCI query and return a result in json", }, cfg, func(_ context.Context, args []string) error { @@ -55,7 +56,7 @@ func execQuery(cfg *client.QueryCfg, args []string, io commands.IO) error { return fmt.Errorf("amino marshal json error: %w", err) } - // XXX: this is to specific + // XXX: this is probably too specific if cfg.Path == "vm/qeval/json" { if len(qres.Response.Data) > 0 { output.Returns = qres.Response.Data From d574bf7a6c0bd672331c268f32c4c09502e78731 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:05:13 +0100 Subject: [PATCH 11/12] chore: cleanup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/sdk/vm/types.go | 4 +--- tm2/pkg/bft/abci/types/types.go | 10 +++++----- tm2/pkg/sdk/types.go | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/gno.land/pkg/sdk/vm/types.go b/gno.land/pkg/sdk/vm/types.go index e2649dc844c..442c2d4b138 100644 --- a/gno.land/pkg/sdk/vm/types.go +++ b/gno.land/pkg/sdk/vm/types.go @@ -1,8 +1,6 @@ package vm -import ( - "github.com/gnolang/gno/tm2/pkg/amino" -) +import "github.com/gnolang/gno/tm2/pkg/amino" // Public facing function signatures. // See convertArgToGno() for supported types. diff --git a/tm2/pkg/bft/abci/types/types.go b/tm2/pkg/bft/abci/types/types.go index b07dc598337..42376e712a6 100644 --- a/tm2/pkg/bft/abci/types/types.go +++ b/tm2/pkg/bft/abci/types/types.go @@ -99,12 +99,12 @@ type Response interface { } type ResponseBase struct { - Error Error `json:"error"` - Data []byte `json:"data"` - Events []Event `json:"events"` + Error Error + Data []byte + Events []Event - Log string `json:"log"` // nondeterministic - Info string `json:"info"` // nondeterministic + Log string // nondeterministic + Info string // nondeterministic } func (ResponseBase) AssertResponse() {} diff --git a/tm2/pkg/sdk/types.go b/tm2/pkg/sdk/types.go index bd4960e5c91..47395362f1a 100644 --- a/tm2/pkg/sdk/types.go +++ b/tm2/pkg/sdk/types.go @@ -23,8 +23,8 @@ type Handler interface { // Result is the union of ResponseDeliverTx and ResponseCheckTx plus events. type Result struct { abci.ResponseBase - GasWanted int64 `json:"gas_wanted"` - GasUsed int64 `json:"gas_used"` + GasWanted int64 + GasUsed int64 } // AnteHandler authenticates transactions, before their internal messages are handled. From 4db0338c99ff5605e99c8766a2595eeb9efd7530 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:13:26 +0100 Subject: [PATCH 12/12] fix: msg eval format Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoland/app.go | 2 +- gno.land/pkg/sdk/vm/handler.go | 12 ++++++------ gno.land/pkg/sdk/vm/keeper.go | 10 +++++----- gno.land/pkg/sdk/vm/msgs.go | 33 ++++++++++++++++----------------- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 85816ee79ef..e3768137912 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -406,7 +406,7 @@ func EndBlocker( // Run the VM to get the updates from the chain expr := fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()) - msgEval := vm.NewMsgEval(vm.ResultFormatMachine, valRealm, expr) + msgEval := vm.NewMsgEval(vm.FormatDefault, valRealm, expr) response, err := vmk.Eval(ctx, msgEval) if err != nil { app.Logger().Error("unable to call VM during EndBlocker", "err", err) diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index 87170131018..3bd6e627aea 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -143,7 +143,7 @@ func (vh vmHandler) queryRender(ctx sdk.Context, req abci.RequestQuery) (res abc // Generate msg eval request expr := fmt.Sprintf("Render(%q)", path) - msgEval := NewMsgEval(ResultFormatString, pkgPath, expr) + msgEval := NewMsgEval(FormatString, pkgPath, expr) // Try evaluate `Render` function result, err := vh.vm.Eval(ctx, msgEval) @@ -169,12 +169,12 @@ func (vh vmHandler) queryFuncs(ctx sdk.Context, req abci.RequestQuery) (res abci // queryEval evaluates any expression in readonly mode and returns the results based on the given format. func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - var format ResultFormat + var format Format switch ss := strings.Split(req.Path, "/"); len(ss) { case 2: - format = ResultFormatDefault + format = FormatDefault case 3: - format = ResultFormat(ss[2]) + format = Format(ss[2]) default: res = sdk.ABCIResponseQueryFromError(fmt.Errorf("invalid query")) return @@ -182,7 +182,7 @@ func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci. } switch format { - case ResultFormatMachine, ResultFormatJSON, ResultFormatString: + case FormatMachine, FormatJSON, FormatString: default: err := fmt.Errorf("invalid query result format %q", format) res = sdk.ABCIResponseQueryFromError(err) @@ -210,7 +210,7 @@ func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci. // queryEvalJSON evaluates any expression in readonly mode and returns the results in JSON format. func (vh vmHandler) queryEvalJSON(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { pkgath, expr := parseQueryEvalData(string(req.Data)) - msgEval := NewMsgEval(ResultFormatJSON, pkgath, expr) + msgEval := NewMsgEval(FormatJSON, pkgath, expr) result, err := vh.vm.Eval(ctx, msgEval) if err != nil { res = sdk.ABCIResponseQueryFromError(err) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 2dc56b5d036..11699cb66f5 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -762,7 +762,7 @@ func (vm *VMKeeper) Eval(ctx sdk.Context, msg MsgEval) (res string, err error) { defer doRecover(m, &err) rtvs := m.Eval(xx) - res = stringifyResultValues(m, msg.ResultFormat, rtvs) + res = stringifyResultValues(m, msg.Format, rtvs) return res, nil } @@ -794,9 +794,9 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err } -func stringifyResultValues(m *gno.Machine, format ResultFormat, values []gnolang.TypedValue) string { +func stringifyResultValues(m *gno.Machine, format Format, values []gnolang.TypedValue) string { switch format { - case ResultFormatString: + case FormatString: if len(values) != 1 { panic(fmt.Errorf("expected 1 string result, got %d", len(values))) } @@ -816,9 +816,9 @@ func stringifyResultValues(m *gno.Machine, format ResultFormat, values []gnolang panic(fmt.Errorf("expected 1 `string` or `Stringer` result, got %v", tv.T.Kind())) - case ResultFormatJSON: + case FormatJSON: return JSONPrimitiveValues(m, values) - case ResultFormatDefault, "": + case FormatDefault, "": var res strings.Builder for i, v := range values { diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index aea1b1be09a..ee1f0496af3 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -12,13 +12,14 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -type ResultFormat string +type Format string const ( - ResultFormatMachine ResultFormat = "machine" // Default machine representation - ResultFormatString = "string" // Single string represnetation - ResultFormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now - ResultFormatDefault = ResultFormatMachine + FormatMachine Format = "machine" // Default machine representation + FormatString = "string" // Single string represnetation + FormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now + + FormatDefault = FormatMachine ) //---------------------------------------- @@ -93,24 +94,22 @@ func (msg MsgAddPackage) GetReceived() std.Coins { // MsgEval - eval a Gno Expr. type MsgEval struct { - PkgPath string `json:"pkg_path" yaml:"pkg_path"` - Expr string `json:"expr" yaml:"expr"` - - // Caller will be use for signing only - Caller crypto.Address `json:"caller" yaml:"caller"` + PkgPath string `json:"pkg_path" yaml:"pkg_path"` + Expr string `json:"expr" yaml:"expr"` + Caller crypto.Address `json:"caller" yaml:"caller"` // XXX: This field is experimental, use with care as output is likely to change // Default format is machine - ResultFormat ResultFormat `json:"format" yaml:"format"` + Format Format `json:"format" yaml:"format"` } var _ std.Msg = MsgEval{} -func NewMsgEval(format ResultFormat, pkgPath, expr string) MsgEval { +func NewMsgEval(format Format, pkgPath, expr string) MsgEval { return MsgEval{ - PkgPath: pkgPath, - Expr: expr, - ResultFormat: format, + PkgPath: pkgPath, + Expr: expr, + Format: format, } } @@ -155,14 +154,14 @@ type MsgCall struct { Args []string `json:"args" yaml:"args"` // XXX: This field is experimental, use with care as output is likely to change - Format ResultFormat `json:"format" yaml:"format"` + Format Format `json:"format" yaml:"format"` } var _ std.Msg = MsgCall{} func NewMsgCall(caller crypto.Address, send sdk.Coins, pkgPath, fnc string, args []string) MsgCall { return MsgCall{ - Format: ResultFormatDefault, + Format: FormatDefault, Caller: caller, Send: send, PkgPath: pkgPath,