From 13332beb96a90c1f6711d6a3b8d5119ce9f779dc Mon Sep 17 00:00:00 2001 From: marston Date: Wed, 28 Jan 2026 21:09:19 -0500 Subject: [PATCH 1/5] feat(storage): add AllFilesByOwner query endpoint Add a new query endpoint to retrieve all files owned by a specific address. This is useful for clients that need to list their own files without scanning the entire file store. The query uses pagination and filters files by the owner field. Changes: - Add AllFilesByOwner RPC to Query service in proto - Implement AllFilesByOwner keeper method with owner filtering Note: Proto regeneration is required after merging. Co-Authored-By: Claude Opus 4.5 --- proto/canine_chain/storage/query.proto | 7 +++++ x/storage/keeper/grpc_query_active_deals.go | 34 +++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/proto/canine_chain/storage/query.proto b/proto/canine_chain/storage/query.proto index 45aac8ced..b32afeae8 100644 --- a/proto/canine_chain/storage/query.proto +++ b/proto/canine_chain/storage/query.proto @@ -58,6 +58,13 @@ service Query { "/jackal/canine-chain/storage/files/merkle/{merkle}"; } + // Queries a list of File items owned by a specific address. + rpc AllFilesByOwner(QueryAllFilesByOwner) + returns (QueryAllFilesByOwnerResponse) { + option (google.api.http).get = + "/jackal/canine-chain/storage/files/owner/{owner}"; + } + // Queries a Proof by provider_address, merkle, owner, and start. rpc Proof(QueryProof) returns (QueryProofResponse) { diff --git a/x/storage/keeper/grpc_query_active_deals.go b/x/storage/keeper/grpc_query_active_deals.go index 07181afbb..8c38b0f57 100644 --- a/x/storage/keeper/grpc_query_active_deals.go +++ b/x/storage/keeper/grpc_query_active_deals.go @@ -110,6 +110,40 @@ func (k Keeper) AllFilesByMerkle(c context.Context, req *types.QueryAllFilesByMe return &types.QueryAllFilesByMerkleResponse{Files: files, Pagination: pageRes}, nil } +// AllFilesByOwner returns a paginated list of files owned by a specific address +func (k Keeper) AllFilesByOwner(c context.Context, req *types.QueryAllFilesByOwner) (*types.QueryAllFilesByOwnerResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.Owner == "" { + return nil, status.Error(codes.InvalidArgument, "owner address is required") + } + + var files []types.UnifiedFile + ctx := sdk.UnwrapSDKContext(c) + + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.FilePrimaryKeyPrefix)) + + pageRes, err := query.Paginate(store, req.Pagination, func(_ []byte, value []byte) error { + var file types.UnifiedFile + if err := k.cdc.Unmarshal(value, &file); err != nil { + return err + } + + // Filter by owner address + if file.Owner == req.Owner { + files = append(files, file) + } + return nil + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &types.QueryAllFilesByOwnerResponse{Files: files, Pagination: pageRes}, nil +} + // OpenFiles returns a paginated list of files with space that providers have yet to fill // // TODO: Create unit-test cases for this From 1c69d9c7d5906709c2bedaf1e716b160ec668121 Mon Sep 17 00:00:00 2001 From: marston Date: Wed, 28 Jan 2026 21:50:45 -0500 Subject: [PATCH 2/5] fix(storage): manually add AllFilesByOwner to protobuf generated code Replace buf-generated code with manual additions to avoid gogoproto import compatibility issues. The buf tool generates code using github.com/cosmos/gogoproto which conflicts with the existing codebase using github.com/gogo/protobuf. Changes: - Add AllFilesByOwner to QueryServer and QueryClient interfaces - Add gRPC handler and service registration - Add HTTP gateway handlers for REST API endpoint Co-Authored-By: Claude Opus 4.5 --- x/storage/types/query.pb.go | 38 +++++++++++ x/storage/types/query.pb.gw.go | 119 +++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/x/storage/types/query.pb.go b/x/storage/types/query.pb.go index 7bf3c301f..8c965afcc 100644 --- a/x/storage/types/query.pb.go +++ b/x/storage/types/query.pb.go @@ -3117,6 +3117,8 @@ type QueryClient interface { EndangeredFiles(ctx context.Context, in *QueryOpenFiles, opts ...grpc.CallOption) (*QueryAllFilesResponse, error) // Queries a list of File items matching the merkle. AllFilesByMerkle(ctx context.Context, in *QueryAllFilesByMerkle, opts ...grpc.CallOption) (*QueryAllFilesByMerkleResponse, error) + // Queries a list of File items owned by a specific address. + AllFilesByOwner(ctx context.Context, in *QueryAllFilesByOwner, opts ...grpc.CallOption) (*QueryAllFilesByOwnerResponse, error) // Queries a Proof by provider_address, merkle, owner, and start. Proof(ctx context.Context, in *QueryProof, opts ...grpc.CallOption) (*QueryProofResponse, error) // Queries a list of Proof items. @@ -3236,6 +3238,15 @@ func (c *queryClient) AllFilesByMerkle(ctx context.Context, in *QueryAllFilesByM return out, nil } +func (c *queryClient) AllFilesByOwner(ctx context.Context, in *QueryAllFilesByOwner, opts ...grpc.CallOption) (*QueryAllFilesByOwnerResponse, error) { + out := new(QueryAllFilesByOwnerResponse) + err := c.cc.Invoke(ctx, "/canine_chain.storage.Query/AllFilesByOwner", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *queryClient) Proof(ctx context.Context, in *QueryProof, opts ...grpc.CallOption) (*QueryProofResponse, error) { out := new(QueryProofResponse) err := c.cc.Invoke(ctx, "/canine_chain.storage.Query/Proof", in, out, opts...) @@ -3458,6 +3469,8 @@ type QueryServer interface { EndangeredFiles(context.Context, *QueryOpenFiles) (*QueryAllFilesResponse, error) // Queries a list of File items matching the merkle. AllFilesByMerkle(context.Context, *QueryAllFilesByMerkle) (*QueryAllFilesByMerkleResponse, error) + // Queries a list of File items owned by a specific address. + AllFilesByOwner(context.Context, *QueryAllFilesByOwner) (*QueryAllFilesByOwnerResponse, error) // Queries a Proof by provider_address, merkle, owner, and start. Proof(context.Context, *QueryProof) (*QueryProofResponse, error) // Queries a list of Proof items. @@ -3531,6 +3544,9 @@ func (*UnimplementedQueryServer) EndangeredFiles(ctx context.Context, req *Query func (*UnimplementedQueryServer) AllFilesByMerkle(ctx context.Context, req *QueryAllFilesByMerkle) (*QueryAllFilesByMerkleResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AllFilesByMerkle not implemented") } +func (*UnimplementedQueryServer) AllFilesByOwner(ctx context.Context, req *QueryAllFilesByOwner) (*QueryAllFilesByOwnerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AllFilesByOwner not implemented") +} func (*UnimplementedQueryServer) Proof(ctx context.Context, req *QueryProof) (*QueryProofResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Proof not implemented") } @@ -3731,6 +3747,24 @@ func _Query_AllFilesByMerkle_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _Query_AllFilesByOwner_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryAllFilesByOwner) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).AllFilesByOwner(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/canine_chain.storage.Query/AllFilesByOwner", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).AllFilesByOwner(ctx, req.(*QueryAllFilesByOwner)) + } + return interceptor(ctx, in, info, handler) +} + func _Query_Proof_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(QueryProof) if err := dec(in); err != nil { @@ -4177,6 +4211,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "AllFilesByMerkle", Handler: _Query_AllFilesByMerkle_Handler, }, + { + MethodName: "AllFilesByOwner", + Handler: _Query_AllFilesByOwner_Handler, + }, { MethodName: "Proof", Handler: _Query_Proof_Handler, diff --git a/x/storage/types/query.pb.gw.go b/x/storage/types/query.pb.gw.go index e8b42afab..210aedd48 100644 --- a/x/storage/types/query.pb.gw.go +++ b/x/storage/types/query.pb.gw.go @@ -495,6 +495,78 @@ func local_request_Query_AllFilesByMerkle_0(ctx context.Context, marshaler runti } +var ( + filter_Query_AllFilesByOwner_0 = &utilities.DoubleArray{Encoding: map[string]int{"owner": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} +) + +func request_Query_AllFilesByOwner_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAllFilesByOwner + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["owner"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "owner") + } + + protoReq.Owner, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "owner", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_AllFilesByOwner_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.AllFilesByOwner(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_AllFilesByOwner_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAllFilesByOwner + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["owner"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "owner") + } + + protoReq.Owner, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "owner", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_AllFilesByOwner_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.AllFilesByOwner(ctx, &protoReq) + return msg, metadata, err + +} + func request_Query_Proof_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq QueryProof var metadata runtime.ServerMetadata @@ -1872,6 +1944,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_AllFilesByOwner_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_AllFilesByOwner_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_AllFilesByOwner_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Query_Proof_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -2582,6 +2677,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_AllFilesByOwner_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_AllFilesByOwner_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_AllFilesByOwner_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Query_Proof_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3060,6 +3175,8 @@ var ( pattern_Query_AllFilesByMerkle_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 4}, []string{"jackal", "canine-chain", "storage", "files", "merkle"}, "", runtime.AssumeColonVerbOpt(false))) + pattern_Query_AllFilesByOwner_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 4}, []string{"jackal", "canine-chain", "storage", "files", "owner"}, "", runtime.AssumeColonVerbOpt(false))) + pattern_Query_Proof_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5, 1, 0, 4, 1, 5, 6, 1, 0, 4, 1, 5, 7}, []string{"jackal", "canine-chain", "storage", "proofs", "provider_address", "merkle", "owner", "start"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_AllProofs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"jackal", "canine-chain", "storage", "proofs"}, "", runtime.AssumeColonVerbOpt(false))) @@ -3122,6 +3239,8 @@ var ( forward_Query_AllFilesByMerkle_0 = runtime.ForwardResponseMessage + forward_Query_AllFilesByOwner_0 = runtime.ForwardResponseMessage + forward_Query_Proof_0 = runtime.ForwardResponseMessage forward_Query_AllProofs_0 = runtime.ForwardResponseMessage From 8eda86daf082998830aaab7f6de50143eb8125bb Mon Sep 17 00:00:00 2001 From: marston Date: Thu, 29 Jan 2026 12:31:52 -0500 Subject: [PATCH 3/5] fix(storage): address PR review comments for AllFilesByOwner - Add bech32 address format validation for owner parameter - Fix pagination by using manual iteration instead of query.Paginate with filtering (query.Paginate counts all items, not filtered matches) The new implementation correctly handles pagination offset and limit while filtering, and returns accurate Total count of matching files. Co-Authored-By: Claude Opus 4.5 --- x/storage/keeper/grpc_query_active_deals.go | 47 +++++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/x/storage/keeper/grpc_query_active_deals.go b/x/storage/keeper/grpc_query_active_deals.go index 8c38b0f57..efa6d0928 100644 --- a/x/storage/keeper/grpc_query_active_deals.go +++ b/x/storage/keeper/grpc_query_active_deals.go @@ -120,28 +120,57 @@ func (k Keeper) AllFilesByOwner(c context.Context, req *types.QueryAllFilesByOwn return nil, status.Error(codes.InvalidArgument, "owner address is required") } + if _, err := sdk.AccAddressFromBech32(req.Owner); err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid owner address format") + } + var files []types.UnifiedFile ctx := sdk.UnwrapSDKContext(c) - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.FilePrimaryKeyPrefix)) + // Use manual iteration to correctly handle pagination with filtering + reverse := false + var limit uint64 = 100 + var offset uint64 = 0 + if req.Pagination != nil { + reverse = req.Pagination.Reverse + if req.Pagination.Limit > 0 { + limit = req.Pagination.Limit + } + offset = req.Pagination.Offset + } - pageRes, err := query.Paginate(store, req.Pagination, func(_ []byte, value []byte) error { + var skipped uint64 + var collected uint64 + var total uint64 + k.IterateFilesByMerkle(ctx, reverse, func(_ []byte, val []byte) bool { var file types.UnifiedFile - if err := k.cdc.Unmarshal(value, &file); err != nil { - return err + if err := k.cdc.Unmarshal(val, &file); err != nil { + return false } // Filter by owner address if file.Owner == req.Owner { - files = append(files, file) + total++ + // Handle offset + if skipped < offset { + skipped++ + return false + } + // Collect up to limit + if collected < limit { + files = append(files, file) + collected++ + } } - return nil + return false }) - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + + qpr := query.PageResponse{ + NextKey: nil, + Total: total, } - return &types.QueryAllFilesByOwnerResponse{Files: files, Pagination: pageRes}, nil + return &types.QueryAllFilesByOwnerResponse{Files: files, Pagination: &qpr}, nil } // OpenFiles returns a paginated list of files with space that providers have yet to fill From 29d229840818538078bd9f095609f418b4fa43d4 Mon Sep 17 00:00:00 2001 From: marston Date: Thu, 29 Jan 2026 12:53:54 -0500 Subject: [PATCH 4/5] test(storage): add unit tests for AllFilesByOwner query Comprehensive test coverage including: - Basic query returning files for specified owner only - Multiple owners with correct filtering - Pagination with offset and limit - Empty owner returns error - Invalid owner address format returns error - Non-existent owner returns empty list - Verify Total count is accurate for filtered results Co-Authored-By: Claude Opus 4.5 --- .../keeper/grpc_query_active_deals_test.go | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/x/storage/keeper/grpc_query_active_deals_test.go b/x/storage/keeper/grpc_query_active_deals_test.go index 40be39cec..451973fcc 100644 --- a/x/storage/keeper/grpc_query_active_deals_test.go +++ b/x/storage/keeper/grpc_query_active_deals_test.go @@ -144,6 +144,159 @@ func (suite *KeeperTestSuite) TestAllFiles() { suite.reset() } +func (suite *KeeperTestSuite) TestAllFilesByOwner() { + suite.SetupSuite() + + testAddresses, err := testutil.CreateTestAddresses("cosmos", 3) + suite.Require().NoError(err) + + owner1 := testAddresses[0] + owner2 := testAddresses[1] + depoAccount := testAddresses[2] + + coins := sdk.NewCoins(sdk.NewCoin("ujkl", sdk.NewInt(100000000000))) + testAcc, _ := sdk.AccAddressFromBech32(owner1) + err = suite.bankKeeper.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, testAcc, coins) + suite.Require().NoError(err) + + suite.storageKeeper.SetParams(suite.ctx, types.Params{ + DepositAccount: depoAccount, + ProofWindow: 50, + ChunkSize: 1024, + PriceFeed: "jklprice", + MissesToBurn: 3, + MaxContractAgeInBlocks: 100, + PricePerTbPerMonth: 8, + CollateralPrice: 2, + CheckWindow: 11, + ReferralCommission: 25, + PolRatio: 40, + }) + + // Create 5 files for owner1 + for i := 0; i < 5; i++ { + merkle := []byte(fmt.Sprintf("merkle_owner1_%d", i)) + suite.storageKeeper.SetFile(suite.ctx, types.UnifiedFile{ + Merkle: merkle, + Owner: owner1, + Start: int64(i), + Expires: 0, + FileSize: 1024, + ProofInterval: 400, + ProofType: 0, + Proofs: make([]string, 0), + MaxProofs: 3, + Note: "{}", + }) + } + + // Create 3 files for owner2 + for i := 0; i < 3; i++ { + merkle := []byte(fmt.Sprintf("merkle_owner2_%d", i)) + suite.storageKeeper.SetFile(suite.ctx, types.UnifiedFile{ + Merkle: merkle, + Owner: owner2, + Start: int64(i), + Expires: 0, + FileSize: 2048, + ProofInterval: 400, + ProofType: 0, + Proofs: make([]string, 0), + MaxProofs: 3, + Note: "{}", + }) + } + + // Test: Query files for owner1 - should return 5 files + pg := query.PageRequest{ + Offset: 0, + Reverse: false, + Limit: 100, + } + + res, err := suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pg, + Owner: owner1, + }) + suite.Require().NoError(err) + suite.Require().Equal(5, len(res.Files)) + suite.Require().Equal(uint64(5), res.Pagination.Total) + + // Verify all returned files belong to owner1 + for _, file := range res.Files { + suite.Require().Equal(owner1, file.Owner) + } + + // Test: Query files for owner2 - should return 3 files + res, err = suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pg, + Owner: owner2, + }) + suite.Require().NoError(err) + suite.Require().Equal(3, len(res.Files)) + suite.Require().Equal(uint64(3), res.Pagination.Total) + + // Verify all returned files belong to owner2 + for _, file := range res.Files { + suite.Require().Equal(owner2, file.Owner) + } + + // Test: Pagination with limit + pgLimit := query.PageRequest{ + Offset: 0, + Limit: 2, + } + res, err = suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pgLimit, + Owner: owner1, + }) + suite.Require().NoError(err) + suite.Require().Equal(2, len(res.Files)) + suite.Require().Equal(uint64(5), res.Pagination.Total) // Total should still be 5 + + // Test: Pagination with offset + pgOffset := query.PageRequest{ + Offset: 3, + Limit: 100, + } + res, err = suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pgOffset, + Owner: owner1, + }) + suite.Require().NoError(err) + suite.Require().Equal(2, len(res.Files)) // 5 total - 3 offset = 2 remaining + suite.Require().Equal(uint64(5), res.Pagination.Total) + + // Test: Empty owner should return error + _, err = suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pg, + Owner: "", + }) + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "owner address is required") + + // Test: Invalid owner address format should return error + _, err = suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pg, + Owner: "invalid-address", + }) + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "invalid owner address format") + + // Test: Non-existent owner should return empty list + // Use a known valid bech32 address that won't match any files we created + nonExistentOwner := "cosmos1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnrql8a" + res, err = suite.queryClient.AllFilesByOwner(context.Background(), &types.QueryAllFilesByOwner{ + Pagination: &pg, + Owner: nonExistentOwner, + }) + suite.Require().NoError(err) + suite.Require().Equal(0, len(res.Files)) + suite.Require().Equal(uint64(0), res.Pagination.Total) + + suite.reset() +} + func (suite *KeeperTestSuite) TestOpenFiles() { suite.SetupSuite() From e0a586a76e42a6d820259fab192ba01157d8be96 Mon Sep 17 00:00:00 2001 From: marston Date: Thu, 29 Jan 2026 13:50:46 -0500 Subject: [PATCH 5/5] refactor(storage): deduplicate filtered file query logic Extract common patterns into reusable helpers: - extractPaginationParams: extracts reverse/limit/offset with defaults - filterFilesWithPagination: generic filtered iteration with pagination Simplifies AllFilesByOwner, OpenFiles, and EndangeredFiles from ~50 lines each to ~10 lines each by reusing the common filter pattern. Removes 61 lines of duplicated code. Co-Authored-By: Claude Opus 4.5 --- x/storage/keeper/grpc_query_active_deals.go | 189 +++++++------------- 1 file changed, 64 insertions(+), 125 deletions(-) diff --git a/x/storage/keeper/grpc_query_active_deals.go b/x/storage/keeper/grpc_query_active_deals.go index efa6d0928..54f7f9c30 100644 --- a/x/storage/keeper/grpc_query_active_deals.go +++ b/x/storage/keeper/grpc_query_active_deals.go @@ -12,6 +12,58 @@ import ( "google.golang.org/grpc/status" ) +// paginationParams holds extracted pagination parameters +type paginationParams struct { + reverse bool + limit uint64 + offset uint64 +} + +// extractPaginationParams extracts pagination parameters with defaults +func extractPaginationParams(pagination *query.PageRequest) paginationParams { + p := paginationParams{ + reverse: false, + limit: 100, + offset: 0, + } + if pagination != nil { + p.reverse = pagination.Reverse + if pagination.Limit > 0 { + p.limit = pagination.Limit + } + p.offset = pagination.Offset + } + return p +} + +// filterFilesWithPagination iterates over files and returns those matching the filter with pagination +func (k Keeper) filterFilesWithPagination(ctx sdk.Context, params paginationParams, filter func(*types.UnifiedFile) bool) ([]types.UnifiedFile, *query.PageResponse) { + var files []types.UnifiedFile + var skipped, collected, total uint64 + + k.IterateFilesByMerkle(ctx, params.reverse, func(_ []byte, val []byte) bool { + var file types.UnifiedFile + if err := k.cdc.Unmarshal(val, &file); err != nil { + return false + } + + if filter(&file) { + total++ + if skipped < params.offset { + skipped++ + return false + } + if collected < params.limit { + files = append(files, file) + collected++ + } + } + return false + }) + + return files, &query.PageResponse{Total: total} +} + func (k Keeper) AllFiles(c context.Context, req *types.QueryAllFiles) (*types.QueryAllFilesResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "invalid request") @@ -124,159 +176,46 @@ func (k Keeper) AllFilesByOwner(c context.Context, req *types.QueryAllFilesByOwn return nil, status.Error(codes.InvalidArgument, "invalid owner address format") } - var files []types.UnifiedFile ctx := sdk.UnwrapSDKContext(c) + params := extractPaginationParams(req.Pagination) - // Use manual iteration to correctly handle pagination with filtering - reverse := false - var limit uint64 = 100 - var offset uint64 = 0 - if req.Pagination != nil { - reverse = req.Pagination.Reverse - if req.Pagination.Limit > 0 { - limit = req.Pagination.Limit - } - offset = req.Pagination.Offset - } - - var skipped uint64 - var collected uint64 - var total uint64 - k.IterateFilesByMerkle(ctx, reverse, func(_ []byte, val []byte) bool { - var file types.UnifiedFile - if err := k.cdc.Unmarshal(val, &file); err != nil { - return false - } - - // Filter by owner address - if file.Owner == req.Owner { - total++ - // Handle offset - if skipped < offset { - skipped++ - return false - } - // Collect up to limit - if collected < limit { - files = append(files, file) - collected++ - } - } - return false + files, pageRes := k.filterFilesWithPagination(ctx, params, func(file *types.UnifiedFile) bool { + return file.Owner == req.Owner }) - qpr := query.PageResponse{ - NextKey: nil, - Total: total, - } - - return &types.QueryAllFilesByOwnerResponse{Files: files, Pagination: &qpr}, nil + return &types.QueryAllFilesByOwnerResponse{Files: files, Pagination: pageRes}, nil } // OpenFiles returns a paginated list of files with space that providers have yet to fill -// -// TODO: Create unit-test cases for this func (k Keeper) OpenFiles(c context.Context, req *types.QueryOpenFiles) (*types.QueryAllFilesResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "invalid request") } - var files []types.UnifiedFile ctx := sdk.UnwrapSDKContext(c) + params := extractPaginationParams(req.Pagination) - reverse := false - var limit uint64 = 100 - if req.Pagination != nil { // HERE IS THE FIX - reverse = req.Pagination.Reverse - limit = req.Pagination.Limit - } - - var i uint64 - var total uint64 - k.IterateFilesByMerkle(ctx, reverse, func(_ []byte, val []byte) bool { - var file types.UnifiedFile - if err := k.cdc.Unmarshal(val, &file); err != nil { - return false - } - - if file.ContainsProver(req.ProviderAddress) { - return false - } - - if len(file.Proofs) < int(file.MaxProofs) { - total++ - if i >= limit { - return false - } - files = append(files, file) - } else { - return false - } - - i++ - - return false + files, pageRes := k.filterFilesWithPagination(ctx, params, func(file *types.UnifiedFile) bool { + return !file.ContainsProver(req.ProviderAddress) && len(file.Proofs) < int(file.MaxProofs) }) - qpr := query.PageResponse{ - NextKey: nil, - Total: total, - } - - return &types.QueryAllFilesResponse{Files: files, Pagination: &qpr}, nil + return &types.QueryAllFilesResponse{Files: files, Pagination: pageRes}, nil } // EndangeredFiles returns a paginated list of files with only 1x redundancy -// -// TODO: Create unit-test cases for this func (k Keeper) EndangeredFiles(c context.Context, req *types.QueryOpenFiles) (*types.QueryAllFilesResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "invalid request") } - var files []types.UnifiedFile ctx := sdk.UnwrapSDKContext(c) + params := extractPaginationParams(req.Pagination) - reverse := false - var limit uint64 = 100 - if req.Pagination != nil { // HERE IS THE FIX - reverse = req.Pagination.Reverse - limit = req.Pagination.Limit - } - - var i uint64 - var total uint64 - k.IterateFilesByMerkle(ctx, reverse, func(_ []byte, val []byte) bool { - var file types.UnifiedFile - if err := k.cdc.Unmarshal(val, &file); err != nil { - return false - } - - if file.ContainsProver(req.ProviderAddress) { - return false - } - - if len(file.Proofs) == 1 { - total++ - if i >= limit { - return false - } - files = append(files, file) - } else { - return false - } - - i++ - - return false + files, pageRes := k.filterFilesWithPagination(ctx, params, func(file *types.UnifiedFile) bool { + return !file.ContainsProver(req.ProviderAddress) && len(file.Proofs) == 1 }) - qpr := query.PageResponse{ - NextKey: nil, - Total: total, - } - - return &types.QueryAllFilesResponse{Files: files, Pagination: &qpr}, nil + return &types.QueryAllFilesResponse{Files: files, Pagination: pageRes}, nil } func (k Keeper) File(c context.Context, req *types.QueryFile) (*types.QueryFileResponse, error) {