diff --git a/client.go b/client.go index 7145c887..47f5f7c8 100644 --- a/client.go +++ b/client.go @@ -151,14 +151,17 @@ func (c *Client) UpdateTransactionMetadata(ctx context.Context, cmd *commands.Up return res, nil } -// Transactions retrieves a list of transactions using the user transactions API. -// This method applies optional query parameters and expects a response that can be -// unmarshaled into a slice of response.Transaction pointers. -// If the request fails or the response cannot be decoded, an error is returned. -func (c *Client) Transactions(ctx context.Context, opts ...queries.TransctionsQueryOption) ([]*response.Transaction, error) { +// Transactions retrieves a paginated list of transactions from the user transactions API. +// The returned response includes transactions and pagination details, such as the page number, +// sort order, and sorting field (sortBy). +// +// This method allows optional query parameters to be applied via the provided query options. +// The response is expected to unmarshal into a *response.PageModel[response.Transaction] struct. +// If the API request fails or the response cannot be decoded successfully, an error is returned. +func (c *Client) Transactions(ctx context.Context, opts ...queries.TransactionsQueryOption) (*queries.TransactionPage, error) { res, err := c.transactionsAPI.Transactions(ctx, opts...) if err != nil { - return nil, fmt.Errorf("failed to retrieve transactions from the user transactions API: %w", err) + return nil, fmt.Errorf("failed to retrieve transactions page from the user transactions API: %w", err) } return res, nil diff --git a/internal/api/v1/user/querybuilders/query_builder.go b/internal/api/v1/user/querybuilders/query_builder.go index 1c803d13..338cf237 100644 --- a/internal/api/v1/user/querybuilders/query_builder.go +++ b/internal/api/v1/user/querybuilders/query_builder.go @@ -9,15 +9,6 @@ import ( type QueryBuilderOption func(*QueryBuilder) -func WithQueryParamsFilter(q filter.QueryParams) QueryBuilderOption { - var zero filter.QueryParams - return func(qb *QueryBuilder) { - if q != zero { - qb.builders = append(qb.builders, &QueryParamsFilterBuilder{q}) - } - } -} - func WithMetadataFilter(m Metadata) QueryBuilderOption { return func(qb *QueryBuilder) { if m != nil { @@ -35,6 +26,15 @@ func WithModelFilter(m filter.ModelFilter) QueryBuilderOption { } } +func WithPageFilterQueryBuilder(p filter.Page) QueryBuilderOption { + var zero filter.Page + return func(qb *QueryBuilder) { + if p != zero { + qb.builders = append(qb.builders, &PageFilterBuilder{Page: p}) + } + } +} + func WithFilterQueryBuilder(b FilterQueryBuilder) QueryBuilderOption { return func(qb *QueryBuilder) { if b != nil { diff --git a/internal/api/v1/user/querybuilders/query_builder_test.go b/internal/api/v1/user/querybuilders/query_builder_test.go index c3020633..6d39aedf 100644 --- a/internal/api/v1/user/querybuilders/query_builder_test.go +++ b/internal/api/v1/user/querybuilders/query_builder_test.go @@ -14,9 +14,9 @@ import ( func TestQueryBuilder_Build(t *testing.T) { type filters struct { - QueryParamsFilter filter.QueryParams - MetadataFilter querybuilders.Metadata - ModelFilter filter.ModelFilter + MetadataFilter querybuilders.Metadata + ModelFilter filter.ModelFilter + PageFilter filter.Page } tests := map[string]struct { filters filters @@ -28,13 +28,13 @@ func TestQueryBuilder_Build(t *testing.T) { filters: filters{}, expectedParams: make(url.Values), }, - "query builder: URL values with query params filter-only": { + "query builder: URL values with page filter-only": { filters: filters{ - QueryParamsFilter: filter.QueryParams{ - Page: 10, - PageSize: 20, - OrderByField: "id", - SortDirection: "asc", + PageFilter: filter.Page{ + Number: 10, + Size: 20, + SortBy: "id", + Sort: "asc", }, }, expectedParams: url.Values{ @@ -58,11 +58,11 @@ func TestQueryBuilder_Build(t *testing.T) { }, "query builder: URL values with all filters set": { filters: filters{ - QueryParamsFilter: filter.QueryParams{ - Page: 10, - PageSize: 20, - OrderByField: "id", - SortDirection: "asc", + PageFilter: filter.Page{ + Number: 10, + Size: 20, + Sort: "asc", + SortBy: "id", }, ModelFilter: filter.ModelFilter{ IncludeDeleted: querybuilderstest.Ptr(true), @@ -96,11 +96,11 @@ func TestQueryBuilder_Build(t *testing.T) { }, "query builder: injected dependency filter query builder failure": { filters: filters{ - QueryParamsFilter: filter.QueryParams{ - Page: 10, - PageSize: 20, - OrderByField: "id", - SortDirection: "asc", + PageFilter: filter.Page{ + Number: 10, + Size: 20, + Sort: "id", + SortBy: "asc", }, }, builder: &filterQueryBuilderFailureStub{}, @@ -113,7 +113,7 @@ func TestQueryBuilder_Build(t *testing.T) { // when: opts := []querybuilders.QueryBuilderOption{ querybuilders.WithMetadataFilter(tc.filters.MetadataFilter), - querybuilders.WithQueryParamsFilter(tc.filters.QueryParamsFilter), + querybuilders.WithPageFilterQueryBuilder(tc.filters.PageFilter), querybuilders.WithModelFilter(tc.filters.ModelFilter), querybuilders.WithFilterQueryBuilder(tc.builder), } diff --git a/internal/api/v1/user/transactions/transactions.go b/internal/api/v1/user/transactions/transactions.go index 50f00c60..5d2712ab 100644 --- a/internal/api/v1/user/transactions/transactions.go +++ b/internal/api/v1/user/transactions/transactions.go @@ -80,7 +80,7 @@ func (a *API) Transaction(ctx context.Context, ID string) (*response.Transaction return &result, nil } -func (a *API) Transactions(ctx context.Context, transactionsOpts ...queries.TransctionsQueryOption) ([]*response.Transaction, error) { +func (a *API) Transactions(ctx context.Context, transactionsOpts ...queries.TransactionsQueryOption) (*queries.TransactionPage, error) { var query queries.TransactionsQuery for _, o := range transactionsOpts { o(&query) @@ -88,7 +88,7 @@ func (a *API) Transactions(ctx context.Context, transactionsOpts ...queries.Tran builderOpts := []querybuilders.QueryBuilderOption{ querybuilders.WithMetadataFilter(query.Metadata), - querybuilders.WithQueryParamsFilter(query.QueryParams), + querybuilders.WithPageFilterQueryBuilder(query.Page), querybuilders.WithFilterQueryBuilder(&transactionFilterBuilder{ TransactionFilter: query.Filter, ModelFilterBuilder: querybuilders.ModelFilterBuilder{ModelFilter: query.Filter.ModelFilter}, @@ -111,7 +111,7 @@ func (a *API) Transactions(ctx context.Context, transactionsOpts ...queries.Tran return nil, fmt.Errorf("HTTP response failure: %w", err) } - return result.Content, nil + return &result, nil } func NewAPI(addr string, cli *resty.Client) *API { diff --git a/internal/api/v1/user/transactions/transactions_test.go b/internal/api/v1/user/transactions/transactions_test.go index 5974f60a..0d713f4f 100644 --- a/internal/api/v1/user/transactions/transactions_test.go +++ b/internal/api/v1/user/transactions/transactions_test.go @@ -205,12 +205,12 @@ func TestTransactionsAPI_Transactions(t *testing.T) { tests := map[string]struct { responder httpmock.Responder statusCode int - expectedResponse []*response.Transaction + expectedResponse *response.PageModel[response.Transaction] expectedErr error }{ "HTTP GET /api/v1/transactions response: 200": { statusCode: http.StatusOK, - expectedResponse: transactionstest.ExpectedTransactions(t), + expectedResponse: transactionstest.ExpectedTransactionsPage(t), responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("transactionstest/transactions_200.json")), }, "HTTP GET /api/v1/transactions response: 400": { diff --git a/internal/api/v1/user/transactions/transactionstest/transactionstest.go b/internal/api/v1/user/transactions/transactionstest/transactionstest.go index 4be9dbc7..c8c27517 100644 --- a/internal/api/v1/user/transactions/transactionstest/transactionstest.go +++ b/internal/api/v1/user/transactions/transactionstest/transactionstest.go @@ -226,67 +226,75 @@ func ExpectedTransaction(t *testing.T) *response.Transaction { } } -func ExpectedTransactions(t *testing.T) []*response.Transaction { - return []*response.Transaction{ - { - Model: response.Model{ - CreatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), - UpdatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), - Metadata: map[string]any{ - "domain": "john.doe.test.4chain.space", - "example_key1": "example_key10_val", - "ip_address": "127.0.0.01", - "user_agent": "node-fetch", - "paymail_request": "HandleReceivedP2pTransaction", - "reference_id": "1c2dcc61-f48f-44f2-aba2-9a759a514d49", - "p2p_tx_metadata": map[string]any{ - "pubkey": "3efe9fcb-859c-47f1-b85f-0fa8b1eee065", - "sender": "john.doe@handcash.io", +func ExpectedTransactionsPage(t *testing.T) *response.PageModel[response.Transaction] { + return &response.PageModel[response.Transaction]{ + Content: []*response.Transaction{ + { + Model: response.Model{ + CreatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), + UpdatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), + Metadata: map[string]any{ + "domain": "john.doe.test.4chain.space", + "example_key1": "example_key10_val", + "ip_address": "127.0.0.01", + "user_agent": "node-fetch", + "paymail_request": "HandleReceivedP2pTransaction", + "reference_id": "1c2dcc61-f48f-44f2-aba2-9a759a514d49", + "p2p_tx_metadata": map[string]any{ + "pubkey": "3efe9fcb-859c-47f1-b85f-0fa8b1eee065", + "sender": "john.doe@handcash.io", + }, }, }, + ID: "2c250e21-c33a-41e3-a4e3-77c68b03244e", + Hex: "283b1c6deb6d6263b3cec7a4701d46d3", + XpubOutIDs: []string{"4c9a0a0d-ea4f-4f03-b740-84438b3d210d"}, + BlockHash: "47758f612c6bf5b454bcd642fe8031f6", + BlockHeight: 512, + Fee: 1, + NumberOfInputs: 2, + NumberOfOutputs: 3, + TotalValue: 311, + OutputValue: 100, + Status: "MINED", + TransactionDirection: "incoming", }, - ID: "2c250e21-c33a-41e3-a4e3-77c68b03244e", - Hex: "283b1c6deb6d6263b3cec7a4701d46d3", - XpubOutIDs: []string{"4c9a0a0d-ea4f-4f03-b740-84438b3d210d"}, - BlockHash: "47758f612c6bf5b454bcd642fe8031f6", - BlockHeight: 512, - Fee: 1, - NumberOfInputs: 2, - NumberOfOutputs: 3, - TotalValue: 311, - OutputValue: 100, - Status: "MINED", - TransactionDirection: "incoming", - }, - { - Model: response.Model{ - CreatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), - UpdatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), - Metadata: map[string]any{ - "domain": "jane.doe.test.4chain.space", - "example_key101": "example_key101_val", - "ip_address": "127.0.0.01", - "user_agent": "node-fetch", - "paymail_request": "HandleReceivedP2pTransaction", - "reference_id": "2c6dcc71-f42f-54f2-ada1-1c658a515d50", - "p2p_tx_metadata": map[string]any{ - "pubkey": "4fa8af6b-3217-2373-76da-0aa552ca88aa", - "sender": "jane.doe@handcash.io", + { + Model: response.Model{ + CreatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), + UpdatedAt: ParseTime(t, "2024-10-07T14:03:26.736816Z"), + Metadata: map[string]any{ + "domain": "jane.doe.test.4chain.space", + "example_key101": "example_key101_val", + "ip_address": "127.0.0.01", + "user_agent": "node-fetch", + "paymail_request": "HandleReceivedP2pTransaction", + "reference_id": "2c6dcc71-f42f-54f2-ada1-1c658a515d50", + "p2p_tx_metadata": map[string]any{ + "pubkey": "4fa8af6b-3217-2373-76da-0aa552ca88aa", + "sender": "jane.doe@handcash.io", + }, }, }, + ID: "1c110e11-c23a-51e5-a7e7-99c12b01233e", + Hex: "283b1c7deb7d7773b3cec7a8801d47d2", + XpubOutIDs: []string{"2c8a1a1d-ea5f-5f04-b890-92418b2d411d"}, + BlockHash: "56659f622c6bf5b554bcd742fe8132f9", + BlockHeight: 1024, + Fee: 1, + NumberOfInputs: 2, + NumberOfOutputs: 3, + TotalValue: 500, + OutputValue: 200, + Status: "MINED", + TransactionDirection: "incoming", }, - ID: "1c110e11-c23a-51e5-a7e7-99c12b01233e", - Hex: "283b1c7deb7d7773b3cec7a8801d47d2", - XpubOutIDs: []string{"2c8a1a1d-ea5f-5f04-b890-92418b2d411d"}, - BlockHash: "56659f622c6bf5b554bcd742fe8132f9", - BlockHeight: 1024, - Fee: 1, - NumberOfInputs: 2, - NumberOfOutputs: 3, - TotalValue: 500, - OutputValue: 200, - Status: "MINED", - TransactionDirection: "incoming", + }, + Page: response.PageDescription{ + Size: 2, + Number: 2, + TotalElements: 2, + TotalPages: 1, }, } } diff --git a/queries/transactions.go b/queries/transactions.go index cb95c769..b81a34d9 100644 --- a/queries/transactions.go +++ b/queries/transactions.go @@ -2,40 +2,45 @@ package queries import ( "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/bitcoin-sv/spv-wallet/models/response" ) -// TransactionsQuery aggregates query parameter filters describing the key-value pairs -// that should be appended to the transaction URL being constructed +// TransactionPage is an alias for the transactions response page model +// returned by the SPV Wallet API, which contains a paginated list of +// transactions along with pagination metadata. +type TransactionPage = response.PageModel[response.Transaction] + +// TransactionsQuery aggregates query parameters for constructing a transactions endpoint URL. +// It holds filters for metadata, transaction-specific attributes, and pagination. type TransactionsQuery struct { - Metadata map[string]any - Filter filter.TransactionFilter - QueryParams filter.QueryParams + Metadata map[string]any // Metadata filters for the transactions. + Filter filter.TransactionFilter // Transaction-specific filters (e.g., block height, status). + Page filter.Page // Pagination details (page number, size, sorting). } -// TransctionsQueryOption represents a functional option for creating a customized list -// of transaction query parameters. -type TransctionsQueryOption func(*TransactionsQuery) +// TransactionsQueryOption defines a functional option for configuring a TransactionsQuery instance. +type TransactionsQueryOption func(*TransactionsQuery) -// TransactionsQueryWithMetadataFilter applies specific metadata attributes as filters -// to the transactions endpoint URL being constructed. -func TransactionsQueryWithMetadataFilter(m map[string]any) TransctionsQueryOption { +// TransactionsQueryWithMetadataFilter adds metadata filters to the transactions endpoint URL. +// The specified metadata attributes will be appended as query parameters. +func TransactionsQueryWithMetadataFilter(m map[string]any) TransactionsQueryOption { return func(tq *TransactionsQuery) { tq.Metadata = m } } -// TransactionsQueryWithFilter applies general query parameters like BlockHeight, BlockHash, -// transaction status, etc. to the transactions endpoint URL being constructed. -func TransactionsQueryWithFilter(tf filter.TransactionFilter) TransctionsQueryOption { +// TransactionsQueryWithFilter adds transaction-specific filters, such as block height, block hash, +// transaction status, etc., to the transactions endpoint URL as query parameters. +func TransactionsQueryWithFilter(tf filter.TransactionFilter) TransactionsQueryOption { return func(tq *TransactionsQuery) { tq.Filter = tf } } -// TransactionsQueryWithQueryParamsFilter applies general query parameters like pagination and sort order etc. -// to the transactions endpoint URL being constructed. -func TransactionsQueryWithQueryParamsFilter(q filter.QueryParams) TransctionsQueryOption { +// TransactionsQueryWithPageFilter adds pagination details, like page number, page size, and sort order, +// to the transactions endpoint URL as query parameters. +func TransactionsQueryWithPageFilter(pf filter.Page) TransactionsQueryOption { return func(tq *TransactionsQuery) { - tq.QueryParams = q + tq.Page = pf } }