diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index 8bcdd903831..044919173e8 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -14,7 +14,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -func TestClient_Render(t *testing.T) { +func TestRender(t *testing.T) { t.Parallel() testRealmPath := "gno.land/r/demo/deep/very/deep" expectedRender := []byte("it works!") @@ -54,7 +54,8 @@ func TestClient_Render(t *testing.T) { assert.Equal(t, data.Response.Data, expectedRender) } -func TestClient_CallSingle(t *testing.T) { +// Call tests +func TestCallSingle(t *testing.T) { t.Parallel() client := Client{ @@ -108,7 +109,7 @@ func TestClient_CallSingle(t *testing.T) { assert.Equal(t, string(res.DeliverTx.Data), "it works!") } -func TestClient_CallMultiple(t *testing.T) { +func TestCallMultiple(t *testing.T) { t.Parallel() client := Client{ @@ -178,7 +179,7 @@ func TestClient_CallMultiple(t *testing.T) { assert.NotNil(t, res) } -func TestClient_Call_Errors(t *testing.T) { +func TestCallErrors(t *testing.T) { t.Parallel() testCases := []struct { @@ -350,6 +351,7 @@ func TestClient_Call_Errors(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -549,3 +551,340 @@ func TestClient_Send_Errors(t *testing.T) { }) } } + +// Run tests +func TestRunSingle(t *testing.T) { + t.Parallel() + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() keys.Info { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return adr + }, + } + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte("hi gnoclient!\n"), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + msg := MsgRun{ + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody, + }, + }, + }, + Send: "", + } + + res, err := client.Run(cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, "hi gnoclient!\n", string(res.DeliverTx.Data)) +} + +func TestRunMultiple(t *testing.T) { + t.Parallel() + + client := Client{ + Signer: &mockSigner{ + sign: func(cfg SignCfg) (*std.Tx, error) { + return &std.Tx{}, nil + }, + info: func() keys.Info { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return adr + }, + } + }, + }, + RPCClient: &mockRPCClient{ + broadcastTxCommit: func(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + res := &ctypes.ResultBroadcastTxCommit{ + DeliverTx: abci.ResponseDeliverTx{ + ResponseBase: abci.ResponseBase{ + Data: []byte("hi gnoclient!\nhi gnoclient!\n"), + }, + }, + } + return res, nil + }, + }, + } + + cfg := BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + msg1 := MsgRun{ + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main1.gno", + Body: fileBody, + }, + }, + }, + Send: "", + } + + msg2 := MsgRun{ + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main2.gno", + Body: fileBody, + }, + }, + }, + Send: "", + } + + res, err := client.Run(cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, "hi gnoclient!\nhi gnoclient!\n", string(res.DeliverTx.Data)) +} + +func TestRunErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client Client + cfg BaseTxCfg + msgs []MsgRun + expectedError error + }{ + { + name: "Invalid Signer", + client: Client{ + Signer: nil, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []MsgRun{ + { + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: "", + }, + }, + expectedError: ErrMissingSigner, + }, + { + name: "Invalid RPCClient", + client: Client{ + &mockSigner{}, + nil, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []MsgRun{}, + expectedError: ErrMissingRPCClient, + }, + { + name: "Invalid Gas Fee", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []MsgRun{ + { + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: "", + }, + }, + expectedError: ErrInvalidGasFee, + }, + { + name: "Negative Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: -1, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []MsgRun{ + { + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: "", + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "0 Gas Wanted", + client: Client{ + Signer: &mockSigner{}, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 0, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []MsgRun{ + { + Package: &std.MemPackage{ + Name: "", + Path: "", + Files: []*std.MemFile{ + { + Name: "file1.gno", + Body: "", + }, + }, + }, + Send: "", + }, + }, + expectedError: ErrInvalidGasWanted, + }, + { + name: "Invalid Empty Package", + client: Client{ + Signer: &mockSigner{ + info: func() keys.Info { + return &mockKeysInfo{ + getAddress: func() crypto.Address { + adr, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + return adr + }, + } + }, + }, + RPCClient: &mockRPCClient{}, + }, + cfg: BaseTxCfg{ + GasWanted: 100000, + GasFee: "10000ugnot", + AccountNumber: 1, + SequenceNumber: 1, + Memo: "Test memo", + }, + msgs: []MsgRun{ + { + Package: nil, + Send: "", + }, + }, + expectedError: ErrEmptyPackage, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + res, err := tc.client.Run(tc.cfg, tc.msgs...) + assert.Nil(t, res) + assert.ErrorIs(t, err, tc.expectedError) + }) + } +} diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index 39088bc30d5..9f42a59b427 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -12,6 +12,7 @@ import ( ) var ( + ErrEmptyPackage = errors.New("empty package to run") ErrEmptyPkgPath = errors.New("empty pkg path") ErrEmptyFuncName = errors.New("empty function name") ErrInvalidGasWanted = errors.New("invalid gas wanted") @@ -44,17 +45,13 @@ type MsgSend struct { Send string // Send amount } -// RunCfg contains configuration options for running a temporary package on the blockchain. -type RunCfg struct { - Package *std.MemPackage - GasFee string // Gas fee - GasWanted int64 // Gas wanted - AccountNumber uint64 // Account number - SequenceNumber uint64 // Sequence number - Memo string // Memo +// MsgRun - syntax sugar for vm.MsgRun +type MsgRun struct { + Package *std.MemPackage // Package to run + Send string // Send amount } -// Call executes a contract call on the blockchain. +// Call executes a one or more MsgCall calls on the blockchain. func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { @@ -70,7 +67,7 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTx } // Parse MsgCall slice - vmMsgs := make([]vm.MsgCall, 0, len(msgs)) + vmMsgs := make([]std.Msg, 0, len(msgs)) for _, msg := range msgs { // Validate MsgCall fields if err := msg.validateMsgCall(); err != nil { @@ -84,19 +81,13 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTx } // Unwrap syntax sugar to vm.MsgCall slice - vmMsgs = append(vmMsgs, vm.MsgCall{ + vmMsgs = append(vmMsgs, std.Msg(vm.MsgCall{ Caller: c.Signer.Info().GetAddress(), PkgPath: msg.PkgPath, Func: msg.FuncName, Args: msg.Args, Send: send, - }) - } - - // Cast vm.MsgCall back into std.Msg - stdMsgs := make([]std.Msg, len(vmMsgs)) - for i, msg := range vmMsgs { - stdMsgs[i] = msg + })) } // Parse gas fee @@ -107,7 +98,7 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTx // Pack transaction tx := std.Tx{ - Msgs: stdMsgs, + Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, @@ -116,8 +107,8 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTx return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) } -// Send currency to an account on the blockchain. -func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { +// Run executes a one or more MsgRun calls on the blockchain. +func (c *Client) Run(cfg BaseTxCfg, msgs ...MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { return nil, err @@ -131,11 +122,11 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTx return nil, err } - // Parse MsgSend slice - vmMsgs := make([]bank.MsgSend, 0, len(msgs)) + // Parse MsgRun slice + vmMsgs := make([]std.Msg, 0, len(msgs)) for _, msg := range msgs { - // Validate MsgSend fields - if err := msg.validateMsgSend(); err != nil { + // Validate MsgCall fields + if err := msg.validateMsgRun(); err != nil { return nil, err } @@ -145,18 +136,22 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTx return nil, err } - // Unwrap syntax sugar to vm.MsgSend slice - vmMsgs = append(vmMsgs, bank.MsgSend{ - FromAddress: c.Signer.Info().GetAddress(), - ToAddress: msg.ToAddress, - Amount: send, - }) - } + caller := c.Signer.Info().GetAddress() + + // Precompile and validate Gno syntax + if err = gno.PrecompileAndCheckMempkg(msg.Package); err != nil { + return nil, err + } - // Cast vm.MsgSend back into std.Msg - stdMsgs := make([]std.Msg, len(vmMsgs)) - for i, msg := range vmMsgs { - stdMsgs[i] = msg + msg.Package.Name = "main" + msg.Package.Path = "" + + // Unwrap syntax sugar to vm.MsgCall slice + vmMsgs = append(vmMsgs, std.Msg(vm.MsgRun{ + Caller: caller, + Package: msg.Package, + Send: send, + })) } // Parse gas fee @@ -167,7 +162,7 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTx // Pack transaction tx := std.Tx{ - Msgs: stdMsgs, + Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, @@ -176,59 +171,58 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTx return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) } -// Temporarily load cfg.Package on the blockchain and run main() which can -// call realm functions and use println() to output to the "console". -// This returns bres where string(bres.DeliverTx.Data) is the "console" output. -func (c *Client) Run(cfg RunCfg) (*ctypes.ResultBroadcastTxCommit, error) { +// Send currency to an account on the blockchain. +func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { - return nil, errors.Wrap(err, "validate signer") + return nil, err } if err := c.validateRPCClient(); err != nil { - return nil, errors.Wrap(err, "validate RPC client") + return nil, err } - memPkg := cfg.Package - gasWanted := cfg.GasWanted - gasFee := cfg.GasFee - sequenceNumber := cfg.SequenceNumber - accountNumber := cfg.AccountNumber - memo := cfg.Memo - - // Validate config. - if memPkg.IsEmpty() { - return nil, errors.New("found an empty package " + memPkg.Path) + // Validate base transaction config + if err := cfg.validateBaseTxConfig(); err != nil { + return nil, err } - // Parse gas wanted & fee. - gasFeeCoins, err := std.ParseCoin(gasFee) - if err != nil { - return nil, errors.Wrap(err, "parsing gas fee coin") - } + // Parse MsgSend slice + vmMsgs := make([]std.Msg, 0, len(msgs)) + for _, msg := range msgs { + // Validate MsgSend fields + if err := msg.validateMsgSend(); err != nil { + return nil, err + } - caller := c.Signer.Info().GetAddress() + // Parse send coins + send, err := std.ParseCoins(msg.Send) + if err != nil { + return nil, err + } - // precompile and validate syntax - err = gno.PrecompileAndCheckMempkg(memPkg) - if err != nil { - return nil, errors.Wrap(err, "precompile and check") + // Unwrap syntax sugar to vm.MsgSend slice + vmMsgs = append(vmMsgs, std.Msg(bank.MsgSend{ + FromAddress: c.Signer.Info().GetAddress(), + ToAddress: msg.ToAddress, + Amount: send, + })) } - memPkg.Name = "main" - memPkg.Path = "" - // Construct message & transaction and marshal. - msg := vm.MsgRun{ - Caller: caller, - Package: memPkg, + // Parse gas fee + gasFeeCoins, err := std.ParseCoin(cfg.GasFee) + if err != nil { + return nil, err } + + // Pack transaction tx := std.Tx{ - Msgs: []std.Msg{msg}, - Fee: std.NewFee(gasWanted, gasFeeCoins), + Msgs: vmMsgs, + Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, - Memo: memo, + Memo: cfg.Memo, } - return c.signAndBroadcastTxCommit(tx, accountNumber, sequenceNumber) + return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) } // signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result. diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 4002e25077b..554cba8ecf2 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -3,18 +3,19 @@ package gnoclient import ( "testing" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnoenv" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestClient_Call_Single_Integration(t *testing.T) { +func TestCallSingle_Integration(t *testing.T) { // Set up in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) @@ -57,7 +58,7 @@ func TestClient_Call_Single_Integration(t *testing.T) { assert.Equal(t, expected, got) } -func TestClient_Call_Multiple_Integration(t *testing.T) { +func TestCallMultiple_Integration(t *testing.T) { // Set up in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) @@ -108,7 +109,7 @@ func TestClient_Call_Multiple_Integration(t *testing.T) { assert.Equal(t, expected, got) } -func TestClient_Send_Single_Integration(t *testing.T) { +func TestSendSingle_Integration(t *testing.T) { // Set up in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) @@ -156,7 +157,7 @@ func TestClient_Send_Single_Integration(t *testing.T) { assert.Equal(t, expected, got) } -func TestClient_Send_Multiple_Integration(t *testing.T) { +func TestSendMultiple_Integration(t *testing.T) { // Set up in-memory node config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) @@ -211,7 +212,148 @@ func TestClient_Send_Multiple_Integration(t *testing.T) { assert.Equal(t, expected, got) } -// todo add more integration tests. +// Run tests +func TestRunSingle_Integration(t *testing.T) { + // Set up in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Init Signer & RPCClient + signer := newInMemorySigner(t, "tendermint_test") + rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + + client := Client{ + Signer: signer, + RPCClient: rpcClient, + } + + // Make Tx config + baseCfg := BaseTxCfg{ + GasFee: "10000ugnot", + GasWanted: 8000000, + AccountNumber: 0, + SequenceNumber: 0, + Memo: "", + } + + fileBody := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/tests" +) +func main() { + println(ufmt.Sprintf("- before: %d", tests.Counter())) + for i := 0; i < 10; i++ { + tests.IncCounter() + } + println(ufmt.Sprintf("- after: %d", tests.Counter())) +}` + + // Make Msg configs + msg := MsgRun{ + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody, + }, + }, + }, + Send: "", + } + + res, err := client.Run(baseCfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, string(res.DeliverTx.Data), "- before: 0\n- after: 10\n") +} + +// Run tests +func TestRunMultiple_Integration(t *testing.T) { + // Set up in-memory node + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) + defer node.Stop() + + // Init Signer & RPCClient + signer := newInMemorySigner(t, "tendermint_test") + rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + + client := Client{ + Signer: signer, + RPCClient: rpcClient, + } + + // Make Tx config + baseCfg := BaseTxCfg{ + GasFee: "10000ugnot", + GasWanted: 8000000, + AccountNumber: 0, + SequenceNumber: 0, + Memo: "", + } + + fileBody1 := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/tests" +) +func main() { + println(ufmt.Sprintf("- before: %d", tests.Counter())) + for i := 0; i < 10; i++ { + tests.IncCounter() + } + println(ufmt.Sprintf("- after: %d", tests.Counter())) +}` + + fileBody2 := `package main +import ( + "std" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/deep/very/deep" +) +func main() { + println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) +}` + + // Make Msg configs + msg1 := MsgRun{ + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody1, + }, + }, + }, + Send: "", + } + msg2 := MsgRun{ + Package: &std.MemPackage{ + Files: []*std.MemFile{ + { + Name: "main.gno", + Body: fileBody2, + }, + }, + }, + Send: "", + } + + expected := "- before: 0\n- after: 10\nhi gnoclient!\n" + + res, err := client.Run(baseCfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) +} + +// todo add more integration tests: +// MsgCall with Send field populated (single/multiple) +// MsgRun with Send field populated (single/multiple) func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase { t.Helper() diff --git a/gno.land/pkg/gnoclient/util.go b/gno.land/pkg/gnoclient/util.go index 398bbca08fc..9860d567f1e 100644 --- a/gno.land/pkg/gnoclient/util.go +++ b/gno.land/pkg/gnoclient/util.go @@ -33,3 +33,11 @@ func (msg MsgSend) validateMsgSend() error { } return nil } + +func (msg MsgRun) validateMsgRun() error { + // todo replace with msg.ValidateBasic() after PR #1646 is merged. + if msg.Package == nil || len(msg.Package.Files) == 0 { + return ErrEmptyPackage + } + return nil +}