diff --git a/pkg/client/block/block_result.go b/pkg/client/block/block_result.go index b95a74870..c41175aef 100644 --- a/pkg/client/block/block_result.go +++ b/pkg/client/block/block_result.go @@ -11,6 +11,14 @@ import ( type cometBlockResult coretypes.ResultBlock func (cbr *cometBlockResult) Height() int64 { + + //cdc := codec.NewProtoCodec(codectypes.NewInterfaceRegistry()) cbrPreJSON, err := cdc.MarshalJSON(cbr) + + //cbrJSON, err := json.MarshalIndent(cbrPreJSON, "", " ") + //if err != nil { + // panic(err) + //} + //fmt.Println(string(cbrJSON)) return cbr.Block.Header.Height } diff --git a/pkg/client/interface.go b/pkg/client/interface.go index 365c24b74..e860d0de7 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,5 +1,5 @@ //go:generate mockgen -destination=../../testutil/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient -//go:generate mockgen -destination=../../testutil/mockclient/block_client_mock.go -package=mockclient . Block,BlockClient +//go:generate mockgen -destination=../../testutil/mockclient/block_client_mock.go -package=mockclient . Block,BlockClient,BlockQueryClient //go:generate mockgen -destination=../../testutil/mockclient/delegation_client_mock.go -package=mockclient . DelegationClient //go:generate mockgen -destination=../../testutil/mockclient/tx_client_mock.go -package=mockclient . TxContext,TxClient //go:generate mockgen -destination=../../testutil/mockclient/supplier_client_mock.go -package=mockclient . SupplierClient diff --git a/testutil/e2e/app.go b/testutil/e2e/app.go new file mode 100644 index 000000000..d843984d9 --- /dev/null +++ b/testutil/e2e/app.go @@ -0,0 +1,149 @@ +package e2e + +import ( + "context" + "errors" + "net" + "net/http" + "sync" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/websocket" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + coretypes "github.com/cometbft/cometbft/rpc/core/types" + + "github.com/pokt-network/poktroll/testutil/integration" + "github.com/pokt-network/poktroll/testutil/testclient" +) + +// E2EApp wraps an integration.App and provides both gRPC and WebSocket servers for end-to-end testing +type E2EApp struct { + *integration.App + grpcServer *grpc.Server + grpcListener net.Listener + wsServer *http.Server + wsListener net.Listener + wsUpgrader websocket.Upgrader + wsConnMutex sync.RWMutex + wsConnections map[*websocket.Conn]map[string]struct{} // maps connections to their subscribed event queries + blockEventChan chan *coretypes.ResultEvent +} + +// NewE2EApp creates a new E2EApp instance with integration.App, gRPC, and WebSocket servers +func NewE2EApp(t *testing.T, opts ...integration.IntegrationAppOptionFn) *E2EApp { + t.Helper() + + // Initialize and start gRPC server + creds := insecure.NewCredentials() + grpcServer := grpc.NewServer(grpc.Creds(creds)) + mux := runtime.NewServeMux() + + rootPattern, err := runtime.NewPattern( + 1, + []int{int(utilities.OpLitPush), int(utilities.OpNop)}, + []string{""}, + "", + ) + require.NoError(t, err) + + // Create the integration app + opts = append(opts, integration.WithGRPCServer(grpcServer)) + app := integration.NewCompleteIntegrationApp(t, opts...) + app.RegisterGRPCServer(grpcServer) + + flagSet := testclient.NewFlagSet(t, "tcp://127.0.0.1:42070") + keyRing := keyring.NewInMemory(app.GetCodec()) + clientCtx := testclient.NewLocalnetClientCtx(t, flagSet).WithKeyring(keyRing) + + // Register the handler with the mux + client, err := grpc.NewClient("127.0.0.1:42069", grpc.WithInsecure()) + require.NoError(t, err) + + for _, mod := range app.GetModuleManager().Modules { + mod.(module.AppModuleBasic).RegisterGRPCGatewayRoutes(clientCtx, mux) + } + + // Create listeners for gRPC, WebSocket, and HTTP + grpcListener, err := net.Listen("tcp", "127.0.0.1:42069") + require.NoError(t, err, "failed to create gRPC listener") + + wsListener, err := net.Listen("tcp", "127.0.0.1:6969") + require.NoError(t, err, "failed to create WebSocket listener") + + e2eApp := &E2EApp{ + App: app, + grpcListener: grpcListener, + grpcServer: grpcServer, + wsListener: wsListener, + wsConnections: make(map[*websocket.Conn]map[string]struct{}), + wsUpgrader: websocket.Upgrader{}, + blockEventChan: make(chan *coretypes.ResultEvent, 1), + } + + mux.Handle(http.MethodPost, rootPattern, newPostHandler(client, e2eApp)) + + go func() { + if err := e2eApp.grpcServer.Serve(grpcListener); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + } + }() + + // Initialize and start WebSocket server + e2eApp.wsServer = newWebSocketServer(e2eApp) + go func() { + if err := e2eApp.wsServer.Serve(wsListener); err != nil && errors.Is(err, http.ErrServerClosed) { + if !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + } + }() + + // Initialize and start HTTP server + go func() { + if err := http.ListenAndServe("127.0.0.1:42070", mux); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + } + }() + + // Start event handling + go e2eApp.handleBlockEvents(t) + + return e2eApp +} + +// Close gracefully shuts down the E2EApp and its servers +func (app *E2EApp) Close() error { + app.grpcServer.GracefulStop() + if err := app.wsServer.Close(); err != nil { + return err + } + + close(app.blockEventChan) + + return nil +} + +// GetClientConn returns a gRPC client connection to the E2EApp's gRPC server +func (app *E2EApp) GetClientConn(ctx context.Context) (*grpc.ClientConn, error) { + return grpc.DialContext( + ctx, + app.grpcListener.Addr().String(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) +} + +// GetWSEndpoint returns the WebSocket endpoint URL +func (app *E2EApp) GetWSEndpoint() string { + return "ws://" + app.wsListener.Addr().String() + "/websocket" +} diff --git a/testutil/e2e/app_test.go b/testutil/e2e/app_test.go new file mode 100644 index 000000000..18f53c9b3 --- /dev/null +++ b/testutil/e2e/app_test.go @@ -0,0 +1,321 @@ +package e2e + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + "time" + + "cosmossdk.io/depinject" + "cosmossdk.io/math" + cometrpctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cometbft/cometbft/types" + cosmostx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/golang/mock/gomock" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/pokt-network/poktroll/app/volatile" + "github.com/pokt-network/poktroll/pkg/client/block" + "github.com/pokt-network/poktroll/pkg/client/events" + "github.com/pokt-network/poktroll/pkg/client/query" + "github.com/pokt-network/poktroll/pkg/client/tx" + txtypes "github.com/pokt-network/poktroll/pkg/client/tx/types" + "github.com/pokt-network/poktroll/testutil/integration" + "github.com/pokt-network/poktroll/testutil/mockclient" + "github.com/pokt-network/poktroll/testutil/testclient" + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" +) + +func TestGRPCServer2(t *testing.T) { + grpcServer := grpc.NewServer() + //gwKeeper := gatewaykeeper.NewKeeper() + //gwSvc := gatewaykeeper.NewMsgServerImpl(gwKeeper) + + app := integration.NewCompleteIntegrationApp(t) + app.RegisterGRPCServer(grpcServer) + + mux := runtime.NewServeMux() + err := http.ListenAndServe(":42070", mux) + require.NoError(t, err) + //gatewaytypes.RegisterMsgServer(grpcServer, gwSvc) + //gatewaytypes.RegisterMsgServer(app.MsgServiceRouter(), gwSvc) + + //gatewaytypes.RegisterQueryHandlerFromEndpoint() + + //reflectionService, err := services.NewReflectionService() + //require.NoError(t, err) + + //desc, err := reflectionService.FileDescriptors(nil, nil) + //require.NoError(t, err) + + //app := integration.NewCompleteIntegrationApp(t) + //grpcServer.RegisterService(desc, app.MsgServiceRouter()) +} + +func TestSanity(t *testing.T) { + app := integration.NewCompleteIntegrationApp(t) + + //app.Query(nil, &authtypes.QueryAccountRequest{ + // Address: "pokt1h04g6njyuv03dhd74a73pyzeadmd8dk7l9tsk8", + //}) + + //app.Query(nil, types2.RequestQuery{ + // Data: nil, + // Path: "", + // Height: 0, + // Prove: false, + //}) + + ctrl := gomock.NewController(t) + blockQueryClient := mockclient.NewMockBlockQueryClient(ctrl) + blockQueryClient.EXPECT(). + Block(gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, height *int64) (*cometrpctypes.ResultBlock, error) { + blockResultMock := &cometrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{ + Height: 1, + }, + }, + } + return blockResultMock, nil + }, + ).AnyTimes() + deps := depinject.Supply(app.QueryHelper(), blockQueryClient) + sharedClient, err := query.NewSharedQuerier(deps) + require.NoError(t, err) + + params, err := sharedClient.GetParams(app.GetSdkCtx()) + require.NoError(t, err) + + t.Logf("shared params: %+v", params) +} + +func TestNewE2EApp(t *testing.T) { + initialHeight := int64(7553) + // TODO_IN_THIS_COMMIT: does this 👆 need to be reconciled with the internal height of app? + + app := NewE2EApp(t) + + keyRing := keyring.NewInMemory(app.GetCodec()) + rec, err := keyRing.NewAccount( + "gateway2", + "suffer wet jelly furnace cousin flip layer render finish frequent pledge feature economy wink like water disease final erase goat include apple state furnace", + "", + cosmostypes.FullFundraiserPath, + hd.Secp256k1, + ) + require.NoError(t, err) + + gateway2Addr, err := rec.GetAddress() + require.NoError(t, err) + + // TODO_IN_THIS_COMMOT: fund gateway2 account. + _, err = app.RunMsg(t, &banktypes.MsgSend{ + FromAddress: app.GetFaucetBech32(), + ToAddress: gateway2Addr.String(), + Amount: cosmostypes.NewCoins(cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 100000000)), + }) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + blockQueryClient := mockclient.NewMockBlockQueryClient(ctrl) + blockQueryClient.EXPECT(). + Block(gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, height *int64) (*cometrpctypes.ResultBlock, error) { + //time.Sleep(time.Second * 100) + blockResultMock := &cometrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{ + Height: initialHeight, + }, + }, + } + return blockResultMock, nil + }, + ).AnyTimes() + //blockQueryClient, err := sdkclient.NewClientFromNode("tcp://127.0.0.1:42070") + //blockQueryClient, err := sdkclient.NewClientFromNode("tcp://127.0.0.1:26657") + //require.NoError(t, err) + + creds := insecure.NewCredentials() + grpcConn, err := grpc.NewClient("127.0.0.1:42069", grpc.WithTransportCredentials(creds)) + require.NoError(t, err) + deps := depinject.Supply(grpcConn, blockQueryClient) + + sharedQueryClient, err := query.NewSharedQuerier(deps) + require.NoError(t, err) + + sharedParams, err := sharedQueryClient.GetParams(app.GetSdkCtx()) + require.NoError(t, err) + + t.Logf("shared params: %+v", sharedParams) + + eventsQueryClient := events.NewEventsQueryClient("ws://127.0.0.1:6969/websocket") + //eventsQueryClient := events.NewEventsQueryClient("ws://127.0.0.1:26657/websocket") + deps = depinject.Configs(deps, depinject.Supply(eventsQueryClient)) + blockClient, err := block.NewBlockClient(app.GetSdkCtx(), deps) + require.NoError(t, err) + + // TODO_IN_THIS_COMMIT: NOT localnet flagset NOR context, should be + // configured to match the E2E app listeners. + flagSet := testclient.NewFlagSet(t, "tcp://127.0.0.1:42070") + clientCtx := testclient.NewLocalnetClientCtx(t, flagSet).WithKeyring(keyRing) + + txFactory, err := cosmostx.NewFactoryCLI(clientCtx, flagSet) + require.NoError(t, err) + + deps = depinject.Configs(deps, depinject.Supply(txtypes.Context(clientCtx), txFactory)) + + //_, txContext := testtx.NewE2ETxContext(t, keyRing, flagSet) + txContext, err := tx.NewTxContext(deps) + require.NoError(t, err) + + deps = depinject.Configs(deps, depinject.Supply(blockClient, txContext)) + txClient, err := tx.NewTxClient(app.GetSdkCtx(), deps, tx.WithSigningKeyName("gateway2")) + require.NoError(t, err) + + time.Sleep(time.Second * 1) + + eitherErr := txClient.SignAndBroadcast( + app.GetSdkCtx(), + gatewaytypes.NewMsgStakeGateway( + "pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz", + cosmostypes.NewCoin(volatile.DenomuPOKT, math.NewInt(100000001)), + ), + ) + + // TODO_IN_THIS_COMMIT: signal to the WS server to send another block result event... + //app.NextBlock(t) + + err, errCh := eitherErr.SyncOrAsyncError() + require.NoError(t, err) + require.NoError(t, <-errCh) +} + +func TestGRPCServer(t *testing.T) { + app := NewE2EApp(t) + t.Cleanup(func() { + app.Close() + }) + + creds := insecure.NewCredentials() + grpcConn, err := grpc.NewClient("127.0.0.1:42069", grpc.WithTransportCredentials(creds)) + require.NoError(t, err) + + //dataHex, err := hex.DecodeString("0A2B706F6B74313577336668667963306C747476377235383565326E6370663674326B6C3975683872736E797A") + require.NoError(t, err) + + //req := gatewaytypes.QueryGetGatewayRequest{ + // Address: "pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz", + //} + //res := &abci.ResponseQuery{} + + //grpcConn.Invoke(context.Background(), "abci_query", req, res) + + ctrl := gomock.NewController(t) + blockQueryClient := mockclient.NewMockBlockQueryClient(ctrl) + blockQueryClient.EXPECT(). + Block(gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, height *int64) (*cometrpctypes.ResultBlock, error) { + //time.Sleep(time.Second * 100) + blockResultMock := &cometrpctypes.ResultBlock{ + Block: &types.Block{ + Header: types.Header{ + Height: 1, + }, + }, + } + return blockResultMock, nil + }, + ).AnyTimes() + + deps := depinject.Supply(grpcConn, blockQueryClient) + + sharedQueryClient, err := query.NewSharedQuerier(deps) + require.NoError(t, err) + + //res := new(gatewaytypes.QueryGetGatewayResponse) + // + //err = grpcConn.Invoke(context.Background(), "/poktroll.gateway.Query/Gateway", anyReq, res) + //dataHex, err := hex.DecodeString("0A2B706F6B74313577336668667963306C747476377235383565326E6370663674326B6C3975683872736E797A") + //require.NoError(t, err) + + //req := &abci.RequestQuery{ + // Data: dataHex, + // Path: "/cosmos.auth.v1beta1.Query/Account", + // Height: 0, + // Prove: false, + //} + // + //err = grpcConn.Invoke(context.Background(), "abci_query", req, res) + ////err = grpcConn.Invoke(context.Background(), "abci_query", req, res) + //require.NoError(t, err) + + require.NoError(t, err) + sharedParams, err := sharedQueryClient.GetParams(app.GetSdkCtx()) + require.NoError(t, err) + + t.Logf("shared params: %+v", sharedParams) + + //"method" : "abci_query", + //"params" : { + // "data" : "0A2B706F6B74313577336668667963306C747476377235383565326E6370663674326B6C3975683872736E797A", + // "height" : "0", + // "path" : "/cosmos.auth.v1beta1.Query/Account", + // "prove" : false + //} + + //"method" : "broadcast_tx_async", + //"params" : { + // "tx" : "CmsKZgohL3Bva3Ryb2xsLmdhdGV3YXkuTXNnU3Rha2VHYXRld2F5EkEKK3Bva3QxNXczZmhmeWMwbHR0djdyNTg1ZTJuY3BmNnQya2w5dWg4cnNueXoSEgoFdXBva3QSCTEwMDAwMDAwMRiGOxJYCk4KRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiEDZo2bY9XquUsFljtW/OKWVCDhYFf7NbidN4Y99VQ9438SBAoCCAESBhCqoYLJAhpAw5e7iJN5SpFit3fftxnZY7EDiFqupi7XEL3sUyeV0IBSQv2JZ7Cdu0dCG0yEVgj0xarkPi7dR10pNDL1gcUJxw==" + //} +} + +func TestSanity3(t *testing.T) { + app := NewE2EApp(t) + t.Cleanup(func() { + app.Close() + }) + + time.Sleep(time.Second * 1) + + client := http.DefaultClient + //res, err := client.Do(&http.Request{ + // Method: http.MethodPost, + // URL: &url.URL{Scheme: "http", Host: "127.0.0.1:42070", Path: ""}, + // Body: io.NopCloser(bytes.NewBuffer([]byte(`{"jsonrpc":"2.0","id":"0","method":"abci_query","params":{"path":"/cosmos.auth.v1beta1.Query/Account","data":"0A2B706F6B74313577336668667963306C747476377235383565326E6370663674326B6C3975683872736E797A","prove":false,"height":"0"}}`))), + //}) + res, err := client.Post( + "http://127.0.0.1:42070/", + "application/json", + bytes.NewBuffer([]byte(`{ + "jsonrpc":"2.0", + "id":"0", + "method":"abci_query", + "params":{"path":"/cosmos.auth.v1beta1.Query/Account", + "data":"0A2B706F6B74313577336668667963306C747476377235383565326E6370663674326B6C3975683872736E797A", + "prove":false, + "height":"0" + } + }`)), + ) + require.NoError(t, err) + + result, err := io.ReadAll(res.Body) + require.NoError(t, err) + + t.Logf("result: %s", result) +} diff --git a/testutil/e2e/comet.go b/testutil/e2e/comet.go new file mode 100644 index 000000000..5e607f039 --- /dev/null +++ b/testutil/e2e/comet.go @@ -0,0 +1,216 @@ +package e2e + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/cometbft/cometbft/abci/types" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + coregrpc "github.com/cometbft/cometbft/rpc/grpc" + rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + gogogrpc "github.com/cosmos/gogoproto/grpc" + "github.com/grpc-ecosystem/grpc-gateway/runtime" +) + +// TODO_IN_THIS_COMMIT: godoc... +type CometBFTMethod string + +// TODO_IN_THIS_COMMIT: godoc... +type ServiceMethodUri string + +const ( + abciQueryMethod = CometBFTMethod("abci_query") + broadcastTxSyncMethod = CometBFTMethod("broadcast_tx_sync") + broadcastTxAsyncMethod = CometBFTMethod("broadcast_tx_async") + broadcastTxCommitMethod = CometBFTMethod("broadcast_tx_commit") + + authAccountQueryUri = ServiceMethodUri("/cosmos.auth.v1beta1.Query/Account") +) + +// handleABCIQuery handles the actual ABCI query logic +func newPostHandler(client gogogrpc.ClientConn, app *E2EApp) runtime.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { + ctx := context.Background() + // DEV_NOTE: http.Error() automatically sets the Content-Type header to "text/plain". + w.Header().Set("Content-Type", "application/json") + + // Read and log request body + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error reading request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + // Parse JSON-RPC request + var req rpctypes.RPCRequest + if err = json.Unmarshal(body, &req); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + params := make(map[string]json.RawMessage) + if err = json.Unmarshal(req.Params, ¶ms); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + var response rpctypes.RPCResponse + switch CometBFTMethod(req.Method) { + // TODO_IN_THIS_COMMIT: extract... + case abciQueryMethod: + var ( + resData []byte + height int64 + ) + + pathRaw, hasPath := params["path"] + if !hasPath { + writeErrorResponse(w, req, "missing path param", string(req.Params)) + return + } + + var path string + if err = json.Unmarshal(pathRaw, &path); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + switch ServiceMethodUri(path) { + case authAccountQueryUri: + dataRaw, hasData := params["data"] + if !hasData { + writeErrorResponse(w, req, "missing data param", string(req.Params)) + return + } + + data, err := hex.DecodeString(string(bytes.Trim(dataRaw, `"`))) + if err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + queryReq := new(authtypes.QueryAccountRequest) + if err = queryReq.Unmarshal(data); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + var height int64 + heightRaw, hasHeight := params["height"] + if hasHeight { + if err = json.Unmarshal(bytes.Trim(heightRaw, `"`), &height); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + } + + queryRes := new(authtypes.QueryAccountResponse) + if err = client.Invoke(ctx, path, queryReq, queryRes); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + resData, err = queryRes.Marshal() + if err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + } + + abciQueryRes := coretypes.ResultABCIQuery{ + Response: types.ResponseQuery{ + //Code: 0, + //Index: 0, + //Key: nil, + Value: resData, + Height: height, + }, + } + + response = rpctypes.NewRPCSuccessResponse(req.ID, abciQueryRes) + case broadcastTxSyncMethod, broadcastTxAsyncMethod, broadcastTxCommitMethod: + fmt.Println(">>>> BROADCAST_TX") + + var txBz []byte + txRaw, hasTx := params["tx"] + if !hasTx { + writeErrorResponse(w, req, "missing tx param", string(req.Params)) + return + } + if err = json.Unmarshal(txRaw, &txBz); err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + // TODO_CONSIDERATION: more correct implementation of the different + // broadcast_tx methods (i.e. sync, async, commit) is a matter of + // the sequence of running the tx and sending the JSON-RPC response. + + _, finalizeBlockRes, err := app.RunTx(nil, txBz) + if err != nil { + writeErrorResponseFromErr(w, req, err) + return + } + + // DEV_NOTE: There SHOULD ALWAYS be exactly one tx result so long as + // we're finalizing one tx at a time (single tx blocks). + txRes := finalizeBlockRes.GetTxResults()[0] + + bcastTxRes := coregrpc.ResponseBroadcastTx{ + CheckTx: &types.ResponseCheckTx{ + Code: txRes.GetCode(), + Data: txRes.GetData(), + Log: txRes.GetLog(), + Info: txRes.GetInfo(), + GasWanted: txRes.GetGasWanted(), + GasUsed: txRes.GetGasUsed(), + Events: txRes.GetEvents(), + Codespace: txRes.GetCodespace(), + }, + TxResult: &types.ExecTxResult{ + Code: txRes.GetCode(), + Data: txRes.GetData(), + Log: txRes.GetLog(), + Info: txRes.GetInfo(), + GasWanted: txRes.GetGasWanted(), + GasUsed: txRes.GetGasUsed(), + Events: txRes.GetEvents(), + Codespace: txRes.GetCodespace(), + }, + } + + response = rpctypes.NewRPCSuccessResponse(req.ID, bcastTxRes) + default: + response = rpctypes.NewRPCErrorResponse(req.ID, 500, "unsupported method", string(req.Params)) + } + + if err = json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +// TODO_IN_THIS_COMMIT: godoc... +func writeErrorResponseFromErr(w http.ResponseWriter, req rpctypes.RPCRequest, err error) { + var errMsg string + if err != nil { + errMsg = err.Error() + } + writeErrorResponse(w, req, errMsg, "") +} + +// TODO_IN_THIS_COMMIT: godoc... +func writeErrorResponse(w http.ResponseWriter, req rpctypes.RPCRequest, msg, data string) { + errRes := rpctypes.NewRPCErrorResponse(req.ID, 500, msg, data) + if err := json.NewEncoder(w).Encode(errRes); err != nil { + // TODO_IN_THIS_COMMIT: log error + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/testutil/e2e/grpc_server.go b/testutil/e2e/grpc_server.go new file mode 100644 index 000000000..df8caf702 --- /dev/null +++ b/testutil/e2e/grpc_server.go @@ -0,0 +1 @@ +package e2e diff --git a/testutil/e2e/ws_server.go b/testutil/e2e/ws_server.go new file mode 100644 index 000000000..cb6b10ec4 --- /dev/null +++ b/testutil/e2e/ws_server.go @@ -0,0 +1,478 @@ +package e2e + +import ( + "encoding/json" + "net/http" + "strings" + "testing" + + coretypes "github.com/cometbft/cometbft/rpc/core/types" + rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/gorilla/websocket" +) + +// newWebSocketServer creates and configures a new WebSocket server for the E2EApp +func newWebSocketServer(app *E2EApp) *http.Server { + mux := http.NewServeMux() + mux.HandleFunc("/websocket", app.handleWebSocket) + return &http.Server{Handler: mux} +} + +// handleWebSocket handles incoming WebSocket connections and subscriptions +func (app *E2EApp) handleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := app.wsUpgrader.Upgrade(w, r, nil) + if err != nil { + return + } + + app.wsConnMutex.Lock() + app.wsConnections[conn] = make(map[string]struct{}) + app.wsConnMutex.Unlock() + + go app.handleWebSocketConnection(conn) +} + +// TODO_IN_THIS_COMMIT: move +var mockBlockResultJSON = ` +{ + "query" : "tm.event='NewBlock'", + "data" : { + "type" : "tendermint/event/NewBlock", + "value" : { + "block" : { + "header" : { + "version" : { + "block" : "11" + }, + "chain_id" : "poktroll", + "height" : "7554", + "time" : "2025-01-03T14:55:39.944873259Z", + "last_block_id" : { + "hash" : "BC2C6FDDAC8A8A7CF79D4682FC3E76AAFCACB84974FC7C37577322D00D7C7525", + "parts" : { + "total" : 1, + "hash" : "478F75135E06F132E99770C9F6B3D276157532A97252B0D7EDF153E0C8143E80" + } + }, + "last_commit_hash" : "13D79604C6171FCCA01F5F9CA85F03973F4AF6D367E3FED27CAD4B8F87B5A01F", + "data_hash" : "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "validators_hash" : "100E8F1C31B34BBF84E5832CDE10B33EB1D8CA28E5761278A4347EA149CD49ED", + "next_validators_hash" : "100E8F1C31B34BBF84E5832CDE10B33EB1D8CA28E5761278A4347EA149CD49ED", + "consensus_hash" : "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F", + "app_hash" : "BC5335B4EEF06E71C08FC051C89BB965F77B5364E55495E0487294C0AF92C251", + "last_results_hash" : "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "evidence_hash" : "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "proposer_address" : "294A784968AC4F506E9DF5E672C430B821F1362F" + }, + "data" : { + "txs" : [ ] + }, + "evidence" : { + "evidence" : [ ] + }, + "last_commit" : { + "height" : "7553", + "round" : 0, + "block_id" : { + "hash" : "BC2C6FDDAC8A8A7CF79D4682FC3E76AAFCACB84974FC7C37577322D00D7C7525", + "parts" : { + "total" : 1, + "hash" : "478F75135E06F132E99770C9F6B3D276157532A97252B0D7EDF153E0C8143E80" + } + }, + "signatures" : [ { + "block_id_flag" : 2, + "validator_address" : "294A784968AC4F506E9DF5E672C430B821F1362F", + "timestamp" : "2025-01-03T14:55:39.944873259Z", + "signature" : "Fur3Gg1nLlzpRuN4aBY90Xb/BZVBGl57zjbMkPscycAi8/cGBYub/EHUTQbxUZjchQ0h1hcQlDQNc2Eu1oiiBQ==" + } ] + } + }, + "block_id" : { + "hash" : "5F1522B51BCE44338C1ED0D4BBAC048CA149EA14D2488C2AC730FFE4F206FBC8", + "parts" : { + "total" : 1, + "hash" : "1DF66D12EF879200392F078033C7CE314AA57B1BE38084C20C526AAD42DF90EE" + } + }, + "result_finalize_block" : { + "events" : [ { + "type" : "coin_spent", + "attributes" : [ { + "key" : "spender", + "value" : "pokt1m3h30wlvsf8llruxtpukdvsy0km2kum84hcvmc", + "index" : true + }, { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "coin_received", + "attributes" : [ { + "key" : "receiver", + "value" : "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", + "index" : true + }, { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "transfer", + "attributes" : [ { + "key" : "recipient", + "value" : "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", + "index" : true + }, { + "key" : "sender", + "value" : "pokt1m3h30wlvsf8llruxtpukdvsy0km2kum84hcvmc", + "index" : true + }, { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "message", + "attributes" : [ { + "key" : "sender", + "value" : "pokt1m3h30wlvsf8llruxtpukdvsy0km2kum84hcvmc", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "mint", + "attributes" : [ { + "key" : "bonded_ratio", + "value" : "0.000000000000013043", + "index" : true + }, { + "key" : "inflation", + "value" : "0.000000000000000000", + "index" : true + }, { + "key" : "annual_provisions", + "value" : "0.000000000000000000", + "index" : true + }, { + "key" : "amount", + "value" : "0", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "coin_spent", + "attributes" : [ { + "key" : "spender", + "value" : "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", + "index" : true + }, { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "coin_received", + "attributes" : [ { + "key" : "receiver", + "value" : "pokt1jv65s3grqf6v6jl3dp4t6c9t9rk99cd86emg48", + "index" : true + }, { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "transfer", + "attributes" : [ { + "key" : "recipient", + "value" : "pokt1jv65s3grqf6v6jl3dp4t6c9t9rk99cd86emg48", + "index" : true + }, { + "key" : "sender", + "value" : "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", + "index" : true + }, { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "message", + "attributes" : [ { + "key" : "sender", + "value" : "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "commission", + "attributes" : [ { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "validator", + "value" : "poktvaloper18kk3aqe2pjz7x7993qp2pjt95ghurra9c5ef0t", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + }, { + "type" : "rewards", + "attributes" : [ { + "key" : "amount", + "value" : "", + "index" : true + }, { + "key" : "validator", + "value" : "poktvaloper18kk3aqe2pjz7x7993qp2pjt95ghurra9c5ef0t", + "index" : true + }, { + "key" : "mode", + "value" : "BeginBlock", + "index" : true + } ] + } ], + "validator_updates" : [ ], + "consensus_param_updates" : { + "block" : { + "max_bytes" : "22020096", + "max_gas" : "-1" + }, + "evidence" : { + "max_age_num_blocks" : "100000", + "max_age_duration" : "172800000000000", + "max_bytes" : "1048576" + }, + "validator" : { + "pub_key_types" : [ "ed25519" ] + }, + "version" : { }, + "abci" : { } + }, + "app_hash" : "kPhqTfc+m5pORs+A7Y/eGTM8w0gdZXFMGW64KLnjGdU=" + } + } + }, + "events" : { + "commission.amount" : [ "" ], + "commission.mode" : [ "BeginBlock" ], + "transfer.sender" : [ "pokt1m3h30wlvsf8llruxtpukdvsy0km2kum84hcvmc", "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9" ], + "transfer.amount" : [ "", "" ], + "message.sender" : [ "pokt1m3h30wlvsf8llruxtpukdvsy0km2kum84hcvmc", "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9" ], + "message.mode" : [ "BeginBlock", "BeginBlock" ], + "mint.bonded_ratio" : [ "0.000000000000013043" ], + "mint.mode" : [ "BeginBlock" ], + "rewards.validator" : [ "poktvaloper18kk3aqe2pjz7x7993qp2pjt95ghurra9c5ef0t" ], + "coin_spent.amount" : [ "", "" ], + "transfer.recipient" : [ "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", "pokt1jv65s3grqf6v6jl3dp4t6c9t9rk99cd86emg48" ], + "rewards.amount" : [ "" ], + "rewards.mode" : [ "BeginBlock" ], + "coin_spent.spender" : [ "pokt1m3h30wlvsf8llruxtpukdvsy0km2kum84hcvmc", "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9" ], + "coin_received.receiver" : [ "pokt17xpfvakm2amg962yls6f84z3kell8c5ldlu5h9", "pokt1jv65s3grqf6v6jl3dp4t6c9t9rk99cd86emg48" ], + "coin_received.mode" : [ "BeginBlock", "BeginBlock" ], + "transfer.mode" : [ "BeginBlock", "BeginBlock" ], + "mint.inflation" : [ "0.000000000000000000" ], + "mint.amount" : [ "0" ], + "coin_spent.mode" : [ "BeginBlock", "BeginBlock" ], + "coin_received.amount" : [ "", "" ], + "mint.annual_provisions" : [ "0.000000000000000000" ], + "commission.validator" : [ "poktvaloper18kk3aqe2pjz7x7993qp2pjt95ghurra9c5ef0t" ], + "tm.event" : [ "NewBlock" ] + } +}` + +// handleWebSocketConnection handles messages from a specific WebSocket connection +func (app *E2EApp) handleWebSocketConnection(conn *websocket.Conn) { + defer func() { + app.wsConnMutex.Lock() + delete(app.wsConnections, conn) + app.wsConnMutex.Unlock() + conn.Close() + }() + + for { + _, message, err := conn.ReadMessage() + if err != nil { + return + } + + var req rpctypes.RPCRequest + if err = json.Unmarshal(message, &req); err != nil { + continue + } + + // Handle subscribe/unsubscribe requests + if req.Method == "subscribe" { + var params struct { + Query string `json:"query"` + } + if err = json.Unmarshal(req.Params, ¶ms); err != nil { + continue + } + + app.wsConnMutex.Lock() + app.wsConnections[conn][params.Query] = struct{}{} + app.wsConnMutex.Unlock() + + // Send subscription response + resp := rpctypes.RPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + // TODO_IN_THIS_COMMIT: generate a mock result... + //Result: json.RawMessage(mockBlockResultJSON), + // DEV_NOTE: Query subscription responses are initially empty; data is sent as subsequent events occur. + Result: json.RawMessage("{}"), + } + + //time.Sleep(time.Second * 4) + + if err = conn.WriteJSON(resp); err != nil { + panic(err) + } + //if err = conn.WriteJSON(resp); err != nil { + // return + //} + } + } +} + +// handleBlockEvents coordinates block finalization with WebSocket event broadcasting +func (app *E2EApp) handleBlockEvents(t *testing.T) { + for event := range app.blockEventChan { + app.wsConnMutex.RLock() + for conn, queries := range app.wsConnections { + // Check if connection is subscribed to this event type + for query := range queries { + _ = query + _ = event + //if eventMatchesQuery(event, query) { + // // Marshal the event to JSON + // eventJSON, err := json.Marshal(event) + // if err != nil { + // t.Logf("failed to marshal event: %v", err) + // continue + // } + + response := rpctypes.RPCResponse{ + JSONRPC: "2.0", + ID: nil, // Events don't have an ID + // TODO_IN_THIS_COMMIT: make this dynamic! + Result: json.RawMessage(mockBlockResultJSON), + } + + if err := conn.WriteJSON(response); err != nil { + app.wsConnMutex.RUnlock() + app.wsConnMutex.Lock() + delete(app.wsConnections, conn) + app.wsConnMutex.Unlock() + app.wsConnMutex.RLock() + continue + } + //} + } + } + app.wsConnMutex.RUnlock() + } +} + +// TODO_IN_THIS_COMMIT: also wrap RunMsgs... +// TODO_IN_THIS_COMMIT: godoc... +// Override RunMsg to also emit transaction events via WebSocket +func (app *E2EApp) RunMsg(t *testing.T, msg sdk.Msg) (tx.MsgResponse, error) { + msgRes, err := app.App.RunMsg(t, msg) + if err != nil { + return nil, err + } + + // Create and emit block event with transaction results + blockEvent := createBlockEvent(app.GetSdkCtx(), msgRes) + app.blockEventChan <- blockEvent + + return msgRes, nil +} + +// createBlockEvent creates a CometBFT-compatible event from transaction results +func createBlockEvent(ctx *sdk.Context, msgRes tx.MsgResponse) *coretypes.ResultEvent { + // Convert SDK events to map[string][]string format that CometBFT expects + events := make(map[string][]string) + for _, event := range ctx.EventManager().Events() { + // Each event type becomes a key, and its attributes become the values + for _, attr := range event.Attributes { + if events[event.Type] == nil { + events[event.Type] = make([]string, 0) + } + events[event.Type] = append(events[event.Type], string(attr.Value)) + } + } + + return &coretypes.ResultEvent{ + Query: "tm.event='NewBlock'", + Data: map[string]interface{}{ + "height": ctx.BlockHeight(), + "hash": ctx.BlockHeader().LastBlockId.Hash, + "events": events, + // Add other relevant block and transaction data here as needed + }, + Events: events, + } +} + +//// createTxEvent creates a CometBFT-compatible event from transaction results +//func createTxEvent(tx *coretypes.ResultTx, index int) *coretypes.ResultEvent { +// return &coretypes.ResultEvent{ +// Query: "tm.event='Tx'", +// Data: map[string]interface{}{ +// "height": ctx.BlockHeight(), +// "hash": ctx.BlockHeader().LastBlockId.Hash, +// "events": events, +// // Add other relevant block and transaction data here as needed +// }, +// Events: events, +// } +//} + +// eventMatchesQuery checks if an event matches a subscription query +func eventMatchesQuery(event *coretypes.ResultEvent, query string) bool { + // Basic implementation - should be expanded to handle more complex queries + return strings.Contains(query, event.Query) +} diff --git a/testutil/integration/app.go b/testutil/integration/app.go index 715c0135b..84caf1544 100644 --- a/testutil/integration/app.go +++ b/testutil/integration/app.go @@ -103,7 +103,7 @@ type App struct { txCfg client.TxConfig authority sdk.AccAddress moduleManager module.Manager - queryHelper *baseapp.QueryServiceTestHelper + queryHelper *baseapp.GRPCQueryRouter keyRing keyring.Keyring ringClient crypto.RingClient preGeneratedAccts *testkeyring.PreGeneratedAccountIterator @@ -139,7 +139,7 @@ func NewIntegrationApp( modules map[string]appmodule.AppModule, keys map[string]*storetypes.KVStoreKey, msgRouter *baseapp.MsgServiceRouter, - queryHelper *baseapp.QueryServiceTestHelper, + queryHelper *baseapp.GRPCQueryRouter, opts ...IntegrationAppOptionFn, ) *App { t.Helper() @@ -513,8 +513,8 @@ func NewCompleteIntegrationApp(t *testing.T, opts ...IntegrationAppOptionFn) *Ap ) // Prepare the message & query routers - msgRouter := baseapp.NewMsgServiceRouter() - queryHelper := baseapp.NewQueryServerTestHelper(sdkCtx, registry) + msgRouter := bApp.MsgServiceRouter() + queryHelper := bApp.GRPCQueryRouter() // Prepare the authz keeper and module authzKeeper := authzkeeper.NewKeeper( @@ -563,32 +563,43 @@ func NewCompleteIntegrationApp(t *testing.T, opts ...IntegrationAppOptionFn) *Ap opts..., ) + configurator := module.NewConfigurator(cdc, msgRouter, queryHelper) + //// TODO_IN_THIS_COMMIT: clean up... + //flagSet := testclient.NewFlagSet(t, "tcp://127.0.0.1:42070") + //keyRing := keyring.NewInMemory(cdc) + //clientCtx := testclient.NewLocalnetClientCtx(t, flagSet).WithKeyring(keyRing) + + for _, mod := range integrationApp.GetModuleManager().Modules { + //mod.(module.AppModuleBasic).RegisterGRPCGatewayRoutes(clientCtx, mux) + mod.(module.HasServices).RegisterServices(configurator) + } + // Register the message servers - banktypes.RegisterMsgServer(msgRouter, bankkeeper.NewMsgServerImpl(bankKeeper)) - tokenomicstypes.RegisterMsgServer(msgRouter, tokenomicskeeper.NewMsgServerImpl(tokenomicsKeeper)) - servicetypes.RegisterMsgServer(msgRouter, servicekeeper.NewMsgServerImpl(serviceKeeper)) - sharedtypes.RegisterMsgServer(msgRouter, sharedkeeper.NewMsgServerImpl(sharedKeeper)) - gatewaytypes.RegisterMsgServer(msgRouter, gatewaykeeper.NewMsgServerImpl(gatewayKeeper)) - apptypes.RegisterMsgServer(msgRouter, appkeeper.NewMsgServerImpl(applicationKeeper)) - suppliertypes.RegisterMsgServer(msgRouter, supplierkeeper.NewMsgServerImpl(supplierKeeper)) - prooftypes.RegisterMsgServer(msgRouter, proofkeeper.NewMsgServerImpl(proofKeeper)) - authtypes.RegisterMsgServer(msgRouter, authkeeper.NewMsgServerImpl(accountKeeper)) - sessiontypes.RegisterMsgServer(msgRouter, sessionkeeper.NewMsgServerImpl(sessionKeeper)) - authz.RegisterMsgServer(msgRouter, authzKeeper) + //banktypes.RegisterMsgServer(msgRouter, bankkeeper.NewMsgServerImpl(bankKeeper)) + //tokenomicstypes.RegisterMsgServer(msgRouter, tokenomicskeeper.NewMsgServerImpl(tokenomicsKeeper)) + //servicetypes.RegisterMsgServer(msgRouter, servicekeeper.NewMsgServerImpl(serviceKeeper)) + //sharedtypes.RegisterMsgServer(msgRouter, sharedkeeper.NewMsgServerImpl(sharedKeeper)) + //gatewaytypes.RegisterMsgServer(msgRouter, gatewaykeeper.NewMsgServerImpl(gatewayKeeper)) + //apptypes.RegisterMsgServer(msgRouter, appkeeper.NewMsgServerImpl(applicationKeeper)) + //suppliertypes.RegisterMsgServer(msgRouter, supplierkeeper.NewMsgServerImpl(supplierKeeper)) + //prooftypes.RegisterMsgServer(msgRouter, proofkeeper.NewMsgServerImpl(proofKeeper)) + //authtypes.RegisterMsgServer(msgRouter, authkeeper.NewMsgServerImpl(accountKeeper)) + //sessiontypes.RegisterMsgServer(msgRouter, sessionkeeper.NewMsgServerImpl(sessionKeeper)) + //authz.RegisterMsgServer(msgRouter, authzKeeper) // Register query servers - banktypes.RegisterQueryServer(queryHelper, bankKeeper) - authz.RegisterQueryServer(queryHelper, authzKeeper) - tokenomicstypes.RegisterQueryServer(queryHelper, tokenomicsKeeper) - servicetypes.RegisterQueryServer(queryHelper, serviceKeeper) - sharedtypes.RegisterQueryServer(queryHelper, sharedKeeper) - gatewaytypes.RegisterQueryServer(queryHelper, gatewayKeeper) - apptypes.RegisterQueryServer(queryHelper, applicationKeeper) - suppliertypes.RegisterQueryServer(queryHelper, supplierKeeper) - prooftypes.RegisterQueryServer(queryHelper, proofKeeper) - // TODO_TECHDEBT: What is the query server for authtypes? - // authtypes.RegisterQueryServer(queryHelper, accountKeeper) - sessiontypes.RegisterQueryServer(queryHelper, sessionKeeper) + //banktypes.RegisterQueryServer(queryHelper, bankKeeper) + //authz.RegisterQueryServer(queryHelper, authzKeeper) + //tokenomicstypes.RegisterQueryServer(queryHelper, tokenomicsKeeper) + //servicetypes.RegisterQueryServer(queryHelper, serviceKeeper) + //sharedtypes.RegisterQueryServer(queryHelper, sharedKeeper) + //gatewaytypes.RegisterQueryServer(queryHelper, gatewayKeeper) + //apptypes.RegisterQueryServer(queryHelper, applicationKeeper) + //suppliertypes.RegisterQueryServer(queryHelper, supplierKeeper) + //prooftypes.RegisterQueryServer(queryHelper, proofKeeper) + //// TODO_TECHDEBT: What is the query server for authtypes? + //// authtypes.RegisterQueryServer(queryHelper, accountKeeper) + //sessiontypes.RegisterQueryServer(queryHelper, sessionKeeper) // Need to go to the next block to finalize the genesis and setup. // This has to be after the params are set, as the params are stored in the @@ -661,8 +672,7 @@ func (app *App) GetPreGeneratedAccounts() *testkeyring.PreGeneratedAccountIterat // QueryHelper returns the query helper used by the application that can be // used to submit queries to the application. -func (app *App) QueryHelper() *baseapp.QueryServiceTestHelper { - app.queryHelper.Ctx = *app.sdkCtx +func (app *App) QueryHelper() *baseapp.GRPCQueryRouter { return app.queryHelper } @@ -695,7 +705,9 @@ func (app *App) GetFaucetBech32() string { // returned. In order to run a message, the application must have a handler for it. // These handlers are registered on the application message service router. func (app *App) RunMsgs(t *testing.T, msgs ...sdk.Msg) (txMsgResps []tx.MsgResponse, err error) { - t.Helper() + if t != nil { + t.Helper() + } // Commit the updated state after the message has been handled. var finalizeBlockRes *abci.ResponseFinalizeBlock @@ -728,6 +740,25 @@ func (app *App) RunMsgs(t *testing.T, msgs ...sdk.Msg) (txMsgResps []tx.MsgRespo app.logger.Info("Running msg", "msg", msg.String()) } + txMsgResps, finalizeBlockRes, err = app.RunTx(t, txBz) + if err != nil { + // DEV_NOTE: Intentionally returning and not asserting nil error to improve reusability. + return nil, err + } + + return txMsgResps, nil +} + +// TODO_IN_THIS_COMMIT: godoc... +func (app *App) RunTx(t *testing.T, txBz []byte) ( + txMsgResps []tx.MsgResponse, + finalizeBlockRes *abci.ResponseFinalizeBlock, + err error, +) { + if t != nil { + t.Helper() + } + // Finalize the block with the transaction. finalizeBlockReq := &cmtabcitypes.RequestFinalizeBlock{ Height: app.LastBlockHeight() + 1, @@ -741,12 +772,14 @@ func (app *App) RunMsgs(t *testing.T, msgs ...sdk.Msg) (txMsgResps []tx.MsgRespo finalizeBlockRes, err = app.FinalizeBlock(finalizeBlockReq) if err != nil { - return nil, fmt.Errorf("finalizing block: %w", err) + return nil, nil, fmt.Errorf("finalizing block: %w", err) } - // NB: We're batching the messages in a single transaction, so we expect - // a single transaction result. - require.Equal(t, 1, len(finalizeBlockRes.TxResults)) + if t != nil { + // NB: We're batching the messages in a single transaction, so we expect + // a single transaction result. + require.Equal(t, 1, len(finalizeBlockRes.TxResults)) + } // Collect the message responses. Accumulate errors related to message handling // failure. If any message fails, an error will be returned. @@ -759,24 +792,29 @@ func (app *App) RunMsgs(t *testing.T, msgs ...sdk.Msg) (txMsgResps []tx.MsgRespo } txMsgDataBz := txResult.GetData() - require.NotNil(t, txMsgDataBz) + if t != nil { + require.NotNil(t, txMsgDataBz) + } txMsgData := new(cosmostypes.TxMsgData) err = app.GetCodec().Unmarshal(txMsgDataBz, txMsgData) - require.NoError(t, err) + if t != nil { + require.NoError(t, err) + } var txMsgRes tx.MsgResponse err = app.GetCodec().UnpackAny(txMsgData.MsgResponses[0], &txMsgRes) - require.NoError(t, err) - require.NotNil(t, txMsgRes) + if t != nil { + require.NoError(t, err) + require.NotNil(t, txMsgRes) + } else { + return nil, nil, err + } txMsgResps = append(txMsgResps, txMsgRes) } - if txResultErrs != nil { - return nil, err - } - return txMsgResps, nil + return txMsgResps, finalizeBlockRes, nil } // NextBlocks calls NextBlock numBlocks times @@ -819,6 +857,9 @@ func (app *App) NextBlock(t *testing.T) { Time: app.sdkCtx.BlockTime(), // Randomize the proposer address for each block. ProposerAddress: sample.ConsAddress().Bytes(), + DecidedLastCommit: cmtabcitypes.CommitInfo{ + Votes: []cmtabcitypes.VoteInfo{{}}, + }, }) require.NoError(t, err) @@ -963,6 +1004,11 @@ func (app *App) setupDefaultActorsState( app.NextBlock(t) } +// TODO_IN_THIS_COMMIT: godoc... +func (app *App) GetModuleManager() module.Manager { + return app.moduleManager +} + // fundAccount mints and sends amountUpokt tokens to the given recipientAddr. // // TODO_IMPROVE: Eliminate usage of and remove this function in favor of diff --git a/testutil/integration/options.go b/testutil/integration/options.go index 1695002cf..e273a14f5 100644 --- a/testutil/integration/options.go +++ b/testutil/integration/options.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" cosmostypes "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + gogogrpc "github.com/cosmos/gogoproto/grpc" tlm "github.com/pokt-network/poktroll/x/tokenomics/token_logic_module" ) @@ -17,6 +18,8 @@ type IntegrationAppConfig struct { // InitChainer function. InitChainerModuleFns []InitChainerModuleFn TokenLogicModules []tlm.TokenLogicModule + + grpcServer gogogrpc.Server } // IntegrationAppOptionFn is a function that receives and has the opportunity to @@ -70,3 +73,10 @@ func WithTokenLogicModules(tokenLogicModules []tlm.TokenLogicModule) Integration config.TokenLogicModules = tokenLogicModules } } + +// TODO_IN_THIS_COMMIT: godoc... +func WithGRPCServer(grpcServer gogogrpc.Server) IntegrationAppOptionFn { + return func(config *IntegrationAppConfig) { + config.grpcServer = grpcServer + } +} diff --git a/testutil/testclient/localnet.go b/testutil/testclient/localnet.go index 354a1dd28..1e6fdb00b 100644 --- a/testutil/testclient/localnet.go +++ b/testutil/testclient/localnet.go @@ -100,10 +100,19 @@ func NewLocalnetClientCtx(t gocuke.TestingT, flagSet *pflag.FlagSet) *client.Con func NewLocalnetFlagSet(t gocuke.TestingT) *pflag.FlagSet { t.Helper() + return NewFlagSet(t, CometLocalTCPURL) +} + +// TODO_IN_THIS_COMMIT: godoc... +func NewFlagSet(t gocuke.TestingT, cometTCPURL string) *pflag.FlagSet { + t.Helper() + mockFlagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) // TODO_IMPROVE: It would be nice if the value could be set correctly based // on whether the test using it is running in tilt or not. - mockFlagSet.String(flags.FlagNode, CometLocalTCPURL, "use localnet poktrolld node") + //mockFlagSet.Bool(flags.FlagGRPCInsecure, true, "use insecure grpc connection") + //mockFlagSet.String(flags.FlagGRPC, cometTCPURL, "use localnet poktrolld node") + mockFlagSet.String(flags.FlagNode, cometTCPURL, "use localnet poktrolld node") mockFlagSet.String(flags.FlagHome, "", "use localnet poktrolld node") mockFlagSet.String(flags.FlagKeyringBackend, "test", "use test keyring") mockFlagSet.String(flags.FlagChainID, app.Name, "use poktroll chain-id") diff --git a/testutil/testclient/testtx/context.go b/testutil/testclient/testtx/context.go index 449e88b8f..7fc9bbd2a 100644 --- a/testutil/testclient/testtx/context.go +++ b/testutil/testclient/testtx/context.go @@ -15,6 +15,7 @@ import ( cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" cosmostypes "github.com/cosmos/cosmos-sdk/types" "github.com/golang/mock/gomock" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/pkg/client" @@ -245,10 +246,19 @@ func NewAnyTimesTxTxContext( ) (*mockclient.MockTxContext, client.TxContext) { t.Helper() - var ( - ctrl = gomock.NewController(t) - flagSet = testclient.NewLocalnetFlagSet(t) - ) + flagSet := testclient.NewLocalnetFlagSet(t) + return NewE2ETxContext(t, keyring, flagSet) +} + +// TODO_IN_THIS_COMMIT: godoc... +func NewE2ETxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + flagSet *pflag.FlagSet, +) (*mockclient.MockTxContext, client.TxContext) { + t.Helper() + + var ctrl = gomock.NewController(t) // intercept #GetAccountNumberSequence() call to mock response and prevent actual query accountRetrieverMock := mockclient.NewMockAccountRetriever(ctrl)