diff --git a/admin_api.go b/admin_api.go index 6e2e4930..96631766 100644 --- a/admin_api.go +++ b/admin_api.go @@ -10,6 +10,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/accesskeys" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/contacts" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/invitations" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/paymails" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/transactions" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/webhooks" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/xpubs" @@ -30,6 +31,7 @@ import ( // ErrUnrecognizedAPIResponse, depending on the behavior of the SPV Wallet API. type AdminAPI struct { xpubsAPI *xpubs.API + paymailsAPI *paymails.API accessKeyAPI *accesskeys.API transactionsAPI *transactions.API contactsAPI *contacts.API @@ -225,6 +227,64 @@ func NewAdminAPIWithXPriv(cfg config.Config, xPriv string) (*AdminAPI, error) { return initAdminAPI(cfg, authenticator) } +// Paymails retrieves a paginated list of paymail addresses via the Admin Paymails API. +// The response includes user paymails along with pagination metadata, such as +// the current page number, sort order, and the field used for sorting (sortBy). +// +// Query parameters can be configured using optional query options. These options allow +// filtering based on metadata, pagination settings, or specific paymail attributes. +// +// The API response is unmarshaled into a *queries.PaymailAddressPage struct. +// Returns an error if the API request fails or the response cannot be decoded. +func (a *AdminAPI) Paymails(ctx context.Context, opts ...queries.PaymailQueryOption) (*queries.PaymailAddressPage, error) { + res, err := a.paymailsAPI.Paymails(ctx, opts...) + if err != nil { + return nil, paymails.HTTPErrorFormatter("failed to retrieve paymail addresses page", err).FormatGetErr() + } + + return res, nil +} + +// Paymail retrieves the paymail address associated with the specified ID via the Admin Paymails API. +// The response is expected to be unmarshaled into a *response.PaymailAddress struct. +// Returns an error if the request fails or the response cannot be decoded. +func (a *AdminAPI) Paymail(ctx context.Context, ID string) (*response.PaymailAddress, error) { + res, err := a.paymailsAPI.Paymail(ctx, ID) + if err != nil { + msg := fmt.Sprintf("failed retrieve paymail address with ID: %s", ID) + return nil, paymails.HTTPErrorFormatter(msg, err).FormatGetErr() + } + + return res, nil +} + +// CreatePaymail creates a new paymail address record via the Admin Paymails API. +// The provided command contains the necessary parameters to define the paymail address record. +// +// The API response is unmarshaled into a *response.Xpub PaymailAddress. +// Returns an error if the API request fails or the response cannot be decoded. +func (a *AdminAPI) CreatePaymail(ctx context.Context, cmd *commands.CreatePaymail) (*response.PaymailAddress, error) { + res, err := a.paymailsAPI.CreatePaymail(ctx, cmd) + if err != nil { + return nil, paymails.HTTPErrorFormatter("failed to create paymail address", err).FormatPostErr() + } + + return res, nil +} + +// DeletePaymail deletes a paymail address with via the Admin Paymails API. +// It returns an error if the API request fails. A nil error indicates that the paymail +// was successfully deleted. +func (a *AdminAPI) DeletePaymail(ctx context.Context, address string) error { + err := a.paymailsAPI.DeletePaymail(ctx, address) + if err != nil { + msg := fmt.Sprintf("failed to remove paymail address: %s", address) + return paymails.HTTPErrorFormatter(msg, err).FormatGetErr() + } + + return nil +} + // NewAdminWithXPub initializes a new AdminAPI instance using an extended public key (xPub). // This function configures the API client with the provided configuration and uses the xPub key for authentication. // If any configuration or initialization step fails, an appropriate error is returned. @@ -252,10 +312,11 @@ func initAdminAPI(cfg config.Config, auth authenticator) (*AdminAPI, error) { } return &AdminAPI{ + paymailsAPI: paymails.NewAPI(url, httpClient), + transactionsAPI: transactions.NewAPI(url, httpClient), xpubsAPI: xpubs.NewAPI(url, httpClient), accessKeyAPI: accesskeys.NewAPI(url, httpClient), webhooksAPI: webhooks.NewAPI(url, httpClient), - transactionsAPI: transactions.NewAPI(url, httpClient), contactsAPI: contacts.NewAPI(url, httpClient), invitationsAPI: invitations.NewAPI(url, httpClient), }, nil diff --git a/commands/paymails.go b/commands/paymails.go new file mode 100644 index 00000000..23436f1d --- /dev/null +++ b/commands/paymails.go @@ -0,0 +1,13 @@ +package commands + +import "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + +// CreatePaymail defines the parameters required to create a new paymail address, +// including associated metadata such as the public name and avatar. +type CreatePaymail struct { + Metadata querybuilders.Metadata `json:"metadata"` // Metadata associated with the paymail as key-value pairs. + Key string `json:"key"` // The xpub key linked to the paymail. + Address string `json:"address"` // The paymail address to be created. + PublicName string `json:"public_name"` // The public display name associated with the paymail. + Avatar string `json:"avatar"` // The URL of the paymail's avatar image. +} diff --git a/commands/users.go b/commands/users.go index d689003d..3519962a 100644 --- a/commands/users.go +++ b/commands/users.go @@ -1,13 +1,15 @@ package commands +import "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + // UpdateXPubMetadata contains the parameters needed to update the metadata // associated with the current user's xpub. type UpdateXPubMetadata struct { - Metadata map[string]any `json:"metadata"` // Key-value pairs representing the xpub metadata + Metadata querybuilders.Metadata `json:"metadata"` // Key-value pairs representing the xpub metadata } // GenerateAccessKey contains the parameters needed to generate a new access key // for the current user, including any associated metadata. type GenerateAccessKey struct { - Metadata map[string]any `json:"metadata"` // Key-value pairs representing the access key metadata + Metadata querybuilders.Metadata `json:"metadata"` // Key-value pairs representing the access key metadata } diff --git a/examples/README.md b/examples/README.md index 6532ddc6..81121e30 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,6 +21,11 @@ the wallet client package during interaction wit the SPV Wallet API. ``` task: [default] task --list task: Available tasks for this project: +* create-paymail-as-admin: Create a paymail address. +* default: Display all available tasks. +* delete-paymail-as-admin: Delete paymail address. +* fetch-paymail-as-admin: Fetch paymail with a given address. +* fetch-paymails-as-admin: Fetch paymails page. * accept-invitation-as-admin: Accept invitation with a given ID as Admin. * create-xpub-as-admin: Create xPub as Admin. * default: Display all available tasks. diff --git a/examples/Taskfile.yml b/examples/Taskfile.yml index a6a15b4a..4c191431 100644 --- a/examples/Taskfile.yml +++ b/examples/Taskfile.yml @@ -14,6 +14,29 @@ tasks: - go run ../walletkeys/cmd/main.go - echo "==================================================================" + fetch-paymails-as-admin: + desc: "Fetch paymails page." + silent: true + cmds: + - go run ./fetch_paymails_as_admin/fetch_paymails_as_admin.go + + fetch-paymail-as-admin: + desc: "Fetch paymail with a given address." + silent: true + cmds: + - go run ./fetch_paymail_as_admin/fetch_paymail_as_admin.go + + create-paymail-as-admin: + desc: "Create a paymail address." + silent: true + cmds: + - go run ./create_paymail_as_admin/create_paymail_as_admin.go + + delete-paymail-as-admin: + desc: "Delete paymail address." + silent: true + cmds: + - go run ./delete_paymail_as_admin/delete_paymail_as_admin.go accept-invitation-as-admin: desc: "Accept invitation with a given ID as Admin." silent: true diff --git a/examples/create_paymail_as_admin/create_paymail_as_admin.go b/examples/create_paymail_as_admin/create_paymail_as_admin.go new file mode 100644 index 00000000..564caefa --- /dev/null +++ b/examples/create_paymail_as_admin/create_paymail_as_admin.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" +) + +func main() { + ctx := context.Background() + + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + xPub, err := adminAPI.CreateXPub(ctx, &commands.CreateUserXpub{ + Metadata: map[string]any{"xpub_key": "xpub_val"}, + XPub: examples.XPub, + }) + if err != nil { + log.Fatal(err) + } + exampleutil.Print("[HTTP POST][Step 1] Create xPub - api/v1/admin/users", xPub) + + addr := exampleutil.RandomPaymail() + paymail, err := adminAPI.CreatePaymail(ctx, &commands.CreatePaymail{ + Key: examples.XPub, + Address: addr, + Metadata: querybuilders.Metadata{"key": "value"}, + }) + if err != nil { + log.Fatal(err) + } + + exampleutil.Print("[HTTP POST][Step 2] Create Paymail - api/v1/admin/paymails", paymail) +} diff --git a/examples/delete_paymail_as_admin/delete_paymail_as_admin.go b/examples/delete_paymail_as_admin/delete_paymail_as_admin.go new file mode 100644 index 00000000..34b66678 --- /dev/null +++ b/examples/delete_paymail_as_admin/delete_paymail_as_admin.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "fmt" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" +) + +func main() { + ctx := context.Background() + + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + xPub, err := adminAPI.CreateXPub(ctx, &commands.CreateUserXpub{ + Metadata: map[string]any{"xpub_key": "xpub_val"}, + XPub: examples.XPub, + }) + if err != nil { + log.Fatal(err) + } + exampleutil.Print("[HTTP POST][Step 1] Create xPub - api/v1/admin/users", xPub) + + addr := exampleutil.RandomPaymail() + paymail, err := adminAPI.CreatePaymail(ctx, &commands.CreatePaymail{ + Key: examples.XPub, + Address: addr, + Metadata: querybuilders.Metadata{"key": "value"}, + }) + if err != nil { + log.Fatal(err) + } + exampleutil.Print("[HTTP POST][Step 2] Create Paymail - api/v1/admin/paymails", paymail) + + err = adminAPI.DeletePaymail(ctx, addr) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("[HTTP DELETE][Step 3] Delete Paymail: %s - api/v1/admin/paymails", addr) +} diff --git a/examples/exampleutil/exampleutil.go b/examples/exampleutil/exampleutil.go index a9b9a7eb..0cee8107 100644 --- a/examples/exampleutil/exampleutil.go +++ b/examples/exampleutil/exampleutil.go @@ -5,6 +5,9 @@ import ( "fmt" "log" "strings" + "time" + + "math/rand" "github.com/bitcoin-sv/spv-wallet-go-client/config" ) @@ -21,3 +24,10 @@ func Print(s string, a any) { } fmt.Println(string(res)) } + +func RandomPaymail() string { + seed := time.Now().UnixNano() + n := rand.New(rand.NewSource(seed)).Intn(500) + addr := fmt.Sprintf("john.doe.%dtest@4chain.test.com", n) + return addr +} diff --git a/examples/fetch_paymail_as_admin/fetch_paymail_as_admin.go b/examples/fetch_paymail_as_admin/fetch_paymail_as_admin.go new file mode 100644 index 00000000..42afd8da --- /dev/null +++ b/examples/fetch_paymail_as_admin/fetch_paymail_as_admin.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" +) + +func main() { + ctx := context.Background() + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + xPub, err := adminAPI.CreateXPub(ctx, &commands.CreateUserXpub{ + Metadata: map[string]any{"xpub_key": "xpub_val"}, + XPub: examples.XPub, + }) + if err != nil { + log.Fatal(err) + } + exampleutil.Print("[HTTP POST][Step 1] Create xPub - api/v1/admin/users", xPub) + + paymail, err := adminAPI.CreatePaymail(ctx, &commands.CreatePaymail{ + Key: examples.XPub, + Address: exampleutil.RandomPaymail(), + Metadata: querybuilders.Metadata{"key": "value"}, + }) + if err != nil { + log.Fatal(err) + } + exampleutil.Print("[HTTP POST][Step 2] Create Paymail - api/v1/admin/paymails", paymail) + + paymail, err = adminAPI.Paymail(ctx, paymail.ID) + if err != nil { + log.Fatal(err) + } + exampleutil.Print("[HTTP GET][Step 3] Fetch Paymail - api/v1/admin/paymails", paymail) +} diff --git a/examples/fetch_paymails_as_admin/fetch_paymails_as_admin.go b/examples/fetch_paymails_as_admin/fetch_paymails_as_admin.go new file mode 100644 index 00000000..0414bef0 --- /dev/null +++ b/examples/fetch_paymails_as_admin/fetch_paymails_as_admin.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/filter" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + page, err := adminAPI.Paymails(context.Background(), queries.PaymailQueryWithPageFilter(filter.Page{ + Size: 3, + })) + if err != nil { + log.Fatal(err) + } + + exampleutil.Print("[HTTP GET] Paymails page - api/v1/admin/paymails", page) +} diff --git a/internal/api/v1/admin/paymails/paymail_filter_builder.go b/internal/api/v1/admin/paymails/paymail_filter_builder.go new file mode 100644 index 00000000..3d478c5e --- /dev/null +++ b/internal/api/v1/admin/paymails/paymail_filter_builder.go @@ -0,0 +1,33 @@ +package paymails + +import ( + "fmt" + "net/url" + + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + "github.com/bitcoin-sv/spv-wallet/models/filter" +) + +type paymailFilterBuilder struct { + paymailFilter filter.AdminPaymailFilter + modelFilterBuilder querybuilders.ModelFilterBuilder +} + +func (p *paymailFilterBuilder) Build() (url.Values, error) { + modelFilterBuilder, err := p.modelFilterBuilder.Build() + if err != nil { + return nil, fmt.Errorf("failed to build model filter query params: %w", err) + } + + params := querybuilders.NewExtendedURLValues() + if len(modelFilterBuilder) > 0 { + params.Append(modelFilterBuilder) + } + + params.AddPair("id", p.paymailFilter.ID) + params.AddPair("xpubId", p.paymailFilter.XpubID) + params.AddPair("alias", p.paymailFilter.Alias) + params.AddPair("domain", p.paymailFilter.Domain) + params.AddPair("publicName", p.paymailFilter.PublicName) + return params.Values, nil +} diff --git a/internal/api/v1/admin/paymails/paymail_filter_builder_test.go b/internal/api/v1/admin/paymails/paymail_filter_builder_test.go new file mode 100644 index 00000000..3b7069fc --- /dev/null +++ b/internal/api/v1/admin/paymails/paymail_filter_builder_test.go @@ -0,0 +1,87 @@ +package paymails + +import ( + "net/url" + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/stretchr/testify/require" +) + +func TestPaymailFilterBuilder_Build(t *testing.T) { + tests := map[string]struct { + filter filter.AdminPaymailFilter + expectedParams url.Values + expectedErr error + }{ + "admin paymail filter: zero values": { + expectedParams: make(url.Values), + }, + "admin paymail filter: filter with only 'id' field set": { + filter: filter.AdminPaymailFilter{ + ID: ptr("b950f5de-3d3a-40b6-bdf8-c9d60e9e0a0a"), + }, + expectedParams: url.Values{ + "id": []string{"b950f5de-3d3a-40b6-bdf8-c9d60e9e0a0a"}, + }, + }, + "admin paymail filter: filter with only 'xPubId' field set": { + filter: filter.AdminPaymailFilter{ + XpubID: ptr("7d373830-1d74-4c4b-a435-04ce09398027"), + }, + expectedParams: url.Values{ + "xpubId": []string{"7d373830-1d74-4c4b-a435-04ce09398027"}, + }, + }, + "admin paymail filter: filter with only 'alias' field set": { + filter: filter.AdminPaymailFilter{ + Alias: ptr("alias"), + }, + expectedParams: url.Values{ + "alias": []string{"alias"}, + }, + }, + "admin paymail filter: filter with only 'public name' field set": { + filter: filter.AdminPaymailFilter{ + PublicName: ptr("Alice"), + }, + expectedParams: url.Values{ + "publicName": []string{"Alice"}, + }, + }, + "admin paymail filter: all fields set": { + filter: filter.AdminPaymailFilter{ + ID: ptr("b950f5de-3d3a-40b6-bdf8-c9d60e9e0a0a"), + XpubID: ptr("7d373830-1d74-4c4b-a435-04ce09398027"), + PublicName: ptr("Alice"), + Alias: ptr("alias"), + }, + expectedParams: url.Values{ + "publicName": []string{"Alice"}, + "xpubId": []string{"7d373830-1d74-4c4b-a435-04ce09398027"}, + "alias": []string{"alias"}, + "id": []string{"b950f5de-3d3a-40b6-bdf8-c9d60e9e0a0a"}, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + queryBuilder := paymailFilterBuilder{ + paymailFilter: tc.filter, + modelFilterBuilder: querybuilders.ModelFilterBuilder{ModelFilter: tc.filter.ModelFilter}, + } + + // then: + got, err := queryBuilder.Build() + require.ErrorIs(t, tc.expectedErr, err) + require.Equal(t, tc.expectedParams, got) + }) + } +} + +func ptr[T any](value T) *T { + return &value +} diff --git a/internal/api/v1/admin/paymails/paymails_api.go b/internal/api/v1/admin/paymails/paymails_api.go new file mode 100644 index 00000000..477a3c9b --- /dev/null +++ b/internal/api/v1/admin/paymails/paymails_api.go @@ -0,0 +1,115 @@ +package paymails + +import ( + "context" + "fmt" + "net/url" + + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/errutil" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/response" + "github.com/go-resty/resty/v2" +) + +const ( + route = "api/v1/admin/paymails" + api = "Admin User Paymails API" +) + +type API struct { + httpClient *resty.Client + url *url.URL +} + +func (a *API) DeletePaymail(ctx context.Context, address string) error { + type body struct { + Address string `json:"address"` + } + + _, err := a.httpClient. + R(). + SetContext(ctx). + SetBody(body{Address: address}). + Delete(a.url.JoinPath(address).String()) + if err != nil { + return fmt.Errorf("HTTP response failure: %w", err) + } + + return nil +} + +func (a *API) CreatePaymail(ctx context.Context, cmd *commands.CreatePaymail) (*response.PaymailAddress, error) { + var result response.PaymailAddress + _, err := a.httpClient. + R(). + SetContext(ctx). + SetResult(&result). + SetBody(cmd). + Post(a.url.String()) + if err != nil { + return nil, fmt.Errorf("HTTP response failure: %w", err) + } + + return &result, nil +} + +func (a *API) Paymail(ctx context.Context, ID string) (*response.PaymailAddress, error) { + var result response.PaymailAddress + _, err := a.httpClient. + R(). + SetContext(ctx). + SetResult(&result). + Get(a.url.JoinPath(ID).String()) + if err != nil { + return nil, fmt.Errorf("HTTP response failure: %w", err) + } + + return &result, nil +} + +func (a *API) Paymails(ctx context.Context, opts ...queries.PaymailQueryOption) (*queries.PaymailAddressPage, error) { + var query queries.PaymailQuery + for _, o := range opts { + o(&query) + } + + queryBuilder := querybuilders.NewQueryBuilder( + querybuilders.WithMetadataFilter(query.Metadata), + querybuilders.WithPageFilter(query.PageFilter), + querybuilders.WithFilterQueryBuilder(&paymailFilterBuilder{ + paymailFilter: query.PaymailFilter, + modelFilterBuilder: querybuilders.ModelFilterBuilder{ModelFilter: query.PaymailFilter.ModelFilter}, + }), + ) + params, err := queryBuilder.Build() + if err != nil { + return nil, fmt.Errorf("failed to build paymail address query params: %w", err) + } + + var result queries.PaymailAddressPage + _, err = a.httpClient. + R(). + SetContext(ctx). + SetResult(&result). + SetQueryParams(params.ParseToMap()). + Get(a.url.String()) + if err != nil { + return nil, fmt.Errorf("HTTP response failure: %w", err) + } + + return &result, nil +} + +func NewAPI(url *url.URL, httpClient *resty.Client) *API { + return &API{url: url.JoinPath(route), httpClient: httpClient} +} + +func HTTPErrorFormatter(action string, err error) *errutil.HTTPErrorFormatter { + return &errutil.HTTPErrorFormatter{ + Action: action, + API: api, + Err: err, + } +} diff --git a/internal/api/v1/admin/paymails/paymails_api_test.go b/internal/api/v1/admin/paymails/paymails_api_test.go new file mode 100644 index 00000000..1161cb3e --- /dev/null +++ b/internal/api/v1/admin/paymails/paymails_api_test.go @@ -0,0 +1,177 @@ +package paymails_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/errors" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/paymails/paymailstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/bitcoin-sv/spv-wallet/models/response" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" +) + +func TestPaymailsAPI_DeletePaymail(t *testing.T) { + id := "xpub22e6cba6-ef6e-432a-8612-63ac4b290ce9" + tests := map[string]struct { + responder httpmock.Responder + expectedResponse *response.PaymailAddress + expectedErr error + }{ + fmt.Sprintf("HTTP DELETE /api/v1/admin/paymails/%s response: 200", id): { + expectedResponse: paymailstest.ExpectedCreatedPaymail(t), + responder: httpmock.NewStringResponder(http.StatusOK, http.StatusText(http.StatusOK)), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/paymails/%s response: 400", id): { + expectedErr: spvwallettest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, spvwallettest.NewBadRequestSPVError()), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/paymails/%s response: 500", id): { + expectedErr: spvwallettest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, spvwallettest.NewInternalServerSPVError()), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/paymails/%s str response: 500", id): { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/paymails/" + id + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodDelete, url, tc.responder) + + // then: + err := wallet.DeletePaymail(context.Background(), id) + require.ErrorIs(t, err, tc.expectedErr) + }) + } +} + +func TestPaymailsAPI_CreatePaymail(t *testing.T) { + tests := map[string]struct { + responder httpmock.Responder + expectedResponse *response.PaymailAddress + expectedErr error + }{ + "HTTP POST /api/v1/admin/paymails response: 200": { + expectedResponse: paymailstest.ExpectedCreatedPaymail(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("paymailstest/post_paymail_200.json")), + }, + "HTTP POST /api/v1/admin/paymails response: 400": { + expectedErr: spvwallettest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, spvwallettest.NewBadRequestSPVError()), + }, + "HTTP POST /api/v1/admin/paymails response: 500": { + expectedErr: spvwallettest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, spvwallettest.NewInternalServerSPVError()), + }, + "HTTP POST /api/v1/admin/paymails str response: 500": { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/paymails" + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodPost, url, tc.responder) + + // then: + got, err := wallet.CreatePaymail(context.Background(), &commands.CreatePaymail{ + Key: "xpub22e6cba6-ef6e-432a-8612-63ac4b290ce9", + }) + require.ErrorIs(t, err, tc.expectedErr) + require.EqualValues(t, tc.expectedResponse, got) + }) + } +} + +func TestPaymailsAPI_Paymails(t *testing.T) { + tests := map[string]struct { + responder httpmock.Responder + expectedResponse *queries.PaymailAddressPage + expectedErr error + }{ + "HTTP GET /api/v1/admin/paymails response: 200": { + expectedResponse: paymailstest.ExpectedPaymailsPage(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("paymailstest/get_paymails_200.json")), + }, + "HTTP GET /api/v1/admin/paymails response: 400": { + expectedErr: spvwallettest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, spvwallettest.NewBadRequestSPVError()), + }, + "HTTP GET /api/v1/admin/paymails response: 500": { + expectedErr: spvwallettest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, spvwallettest.NewInternalServerSPVError()), + }, + "HTTP GET /api/v1/admin/paymails str response: 500": { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/paymails" + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodGet, url, tc.responder) + + // then: + got, err := wallet.Paymails(context.Background(), queries.PaymailQueryWithPageFilter(filter.Page{Size: 1})) + require.ErrorIs(t, err, tc.expectedErr) + require.EqualValues(t, tc.expectedResponse, got) + }) + } +} + +func TestPaymailsAPI_Paymail(t *testing.T) { + id := "98dbafe0-4e2b-4307-8fbf-c55209214bae" + tests := map[string]struct { + responder httpmock.Responder + expectedResponse *response.PaymailAddress + expectedErr error + }{ + fmt.Sprintf("HTTP GET /api/v1/admin/paymails/%s response: 200", id): { + expectedResponse: paymailstest.ExpectedPaymail(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("paymailstest/get_paymail_200.json")), + }, + fmt.Sprintf("HTTP GET /api/v1/admin/paymails/%s response: 400", id): { + expectedErr: spvwallettest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, spvwallettest.NewBadRequestSPVError()), + }, + fmt.Sprintf("HTTP GET /api/v1/admin/paymails/%s response: 500", id): { + expectedErr: spvwallettest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, spvwallettest.NewInternalServerSPVError()), + }, + fmt.Sprintf("HTTP GET /api/v1/admin/paymails/%s str response: 500", id): { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/paymails/" + id + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodGet, url, tc.responder) + + // then: + got, err := wallet.Paymail(context.Background(), id) + require.ErrorIs(t, err, tc.expectedErr) + require.EqualValues(t, tc.expectedResponse, got) + }) + } +} diff --git a/internal/api/v1/admin/paymails/paymailstest/get_paymail_200.json b/internal/api/v1/admin/paymails/paymailstest/get_paymail_200.json new file mode 100644 index 00000000..7c15f641 --- /dev/null +++ b/internal/api/v1/admin/paymails/paymailstest/get_paymail_200.json @@ -0,0 +1,12 @@ +{ + "createdAt": "2024-10-02T10:28:15.544234Z", + "updatedAt": "2024-10-02T10:34:54.836433Z", + "deletedAt": null, + "metadata": null, + "id": "98dbafe0-4e2b-4307-8fbf-c55209214bae", + "xpubId": "0d71ac87-ef56-4b1a-8372-814481cface6", + "alias": "john.doe.test", + "domain": "john.doe.test.4chain.space", + "publicName": "john.doe.test", + "avatar": "http://localhost:3003/static/paymail/avatar.jpg" + } diff --git a/internal/api/v1/admin/paymails/paymailstest/get_paymails_200.json b/internal/api/v1/admin/paymails/paymailstest/get_paymails_200.json new file mode 100644 index 00000000..618bedd5 --- /dev/null +++ b/internal/api/v1/admin/paymails/paymailstest/get_paymails_200.json @@ -0,0 +1,34 @@ +{ + "content": [ + { + "createdAt": "2024-11-18T06:50:07.144902Z", + "updatedAt": "2024-11-18T06:50:07.144932Z", + "deletedAt": null, + "metadata": null, + "id": "31b80181-4d8b-4766-9bc7-76a1d9c6b44d", + "xpubId": "69245a3a-f9ed-4046-9acb-9d66c0b3750c", + "alias": "john.doe.test", + "domain": "john.doe.4chain.space", + "publicName": "John Doe", + "avatar": "" + }, + { + "createdAt": "2024-11-08T15:10:44.688653Z", + "updatedAt": "2024-11-18T07:19:51.561691Z", + "deletedAt": null, + "metadata": null, + "id": "ec91273e-9fb7-4f10-9ecb-d1848d238814", + "xpubId": "68026cb6-a549-45e8-97b1-11426bb16769", + "alias": "jane.doe.test", + "domain": "jane.doe.4chain.space", + "publicName": "Jane Doe", + "avatar": "http://localhost:3003/static/paymail/avatar.jpg" + } + ], + "page": { + "size": 10, + "number": 1, + "totalElements": 2, + "totalPages": 1 + } +} diff --git a/internal/api/v1/admin/paymails/paymailstest/paymail_api_fixtures.go b/internal/api/v1/admin/paymails/paymailstest/paymail_api_fixtures.go new file mode 100644 index 00000000..6583fbe6 --- /dev/null +++ b/internal/api/v1/admin/paymails/paymailstest/paymail_api_fixtures.go @@ -0,0 +1,75 @@ +package paymailstest + +import ( + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/response" +) + +func ExpectedCreatedPaymail(t *testing.T) *response.PaymailAddress { + return &response.PaymailAddress{ + Model: response.Model{ + CreatedAt: spvwallettest.ParseTime(t, "2024-12-02T10:22:45.263654Z"), + UpdatedAt: spvwallettest.ParseTime(t, "2024-12-02T11:22:45.263664+01:00"), + }, + ID: "069d0011-580e-4fc6-9f24-45471b732a8b", + XpubID: "22e6cba6-ef6e-432a-8612-63ac4b290ce9", + Alias: "john.doe.test", + Domain: "example.com", + PublicName: "john.doe.test", + Avatar: "", + } +} + +func ExpectedPaymail(t *testing.T) *response.PaymailAddress { + return &response.PaymailAddress{ + Model: response.Model{ + CreatedAt: spvwallettest.ParseTime(t, "2024-10-02T10:28:15.544234Z"), + UpdatedAt: spvwallettest.ParseTime(t, "2024-10-02T10:34:54.836433Z"), + }, + ID: "98dbafe0-4e2b-4307-8fbf-c55209214bae", + XpubID: "0d71ac87-ef56-4b1a-8372-814481cface6", + Alias: "john.doe.test", + Domain: "john.doe.test.4chain.space", + PublicName: "john.doe.test", + Avatar: "http://localhost:3003/static/paymail/avatar.jpg", + } +} + +func ExpectedPaymailsPage(t *testing.T) *queries.PaymailAddressPage { + return &queries.PaymailAddressPage{ + Content: []*response.PaymailAddress{ + { + Model: response.Model{ + CreatedAt: spvwallettest.ParseTime(t, "2024-11-18T06:50:07.144902Z"), + UpdatedAt: spvwallettest.ParseTime(t, "2024-11-18T06:50:07.144932Z"), + }, + ID: "31b80181-4d8b-4766-9bc7-76a1d9c6b44d", + XpubID: "69245a3a-f9ed-4046-9acb-9d66c0b3750c", + Alias: "john.doe.test", + Domain: "john.doe.4chain.space", + PublicName: "John Doe", + }, + { + Model: response.Model{ + CreatedAt: spvwallettest.ParseTime(t, "2024-11-08T15:10:44.688653Z"), + UpdatedAt: spvwallettest.ParseTime(t, "2024-11-18T07:19:51.561691Z"), + }, + ID: "ec91273e-9fb7-4f10-9ecb-d1848d238814", + XpubID: "68026cb6-a549-45e8-97b1-11426bb16769", + Alias: "jane.doe.test", + Domain: "jane.doe.4chain.space", + PublicName: "Jane Doe", + Avatar: "http://localhost:3003/static/paymail/avatar.jpg", + }, + }, + Page: response.PageDescription{ + Size: 10, + Number: 1, + TotalElements: 2, + TotalPages: 1, + }, + } +} diff --git a/internal/api/v1/admin/paymails/paymailstest/post_paymail_200.json b/internal/api/v1/admin/paymails/paymailstest/post_paymail_200.json new file mode 100644 index 00000000..4560247f --- /dev/null +++ b/internal/api/v1/admin/paymails/paymailstest/post_paymail_200.json @@ -0,0 +1,12 @@ +{ + "createdAt": "2024-12-02T10:22:45.263654Z", + "updatedAt": "2024-12-02T11:22:45.263664+01:00", + "deletedAt": null, + "metadata": null, + "id": "069d0011-580e-4fc6-9f24-45471b732a8b", + "xpubId": "22e6cba6-ef6e-432a-8612-63ac4b290ce9", + "alias": "john.doe.test", + "domain": "example.com", + "publicName": "john.doe.test", + "avatar": "" + } diff --git a/internal/api/v1/querybuilders/extended_url_values_test.go b/internal/api/v1/querybuilders/extended_url_values_test.go index 96c3feba..fdb06ab4 100644 --- a/internal/api/v1/querybuilders/extended_url_values_test.go +++ b/internal/api/v1/querybuilders/extended_url_values_test.go @@ -6,15 +6,15 @@ import ( "time" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" - "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders/querybuilderstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" ) func TestExtendedURLValues_AddPair(t *testing.T) { // given: - to := querybuilderstest.ParseTime(t, "2024-10-07T14:03:26.736816Z") - from := querybuilderstest.ParseTime(t, "2024-10-07T14:03:26.736816Z") + to := spvwallettest.ParseTime(t, "2024-10-07T14:03:26.736816Z") + from := spvwallettest.ParseTime(t, "2024-10-07T14:03:26.736816Z") expectedValues := url.Values{ "key1": []string{"str"}, "key2": []string{"1"}, @@ -30,10 +30,10 @@ func TestExtendedURLValues_AddPair(t *testing.T) { params := querybuilders.NewExtendedURLValues() params.AddPair("key1", "str") params.AddPair("key2", 1) - params.AddPair("key3", querybuilderstest.Ptr("str_ptr")) - params.AddPair("key4", querybuilderstest.Ptr(uint64(64))) - params.AddPair("key5", querybuilderstest.Ptr(uint32(32))) - params.AddPair("key6", querybuilderstest.Ptr(bool(false))) + params.AddPair("key3", spvwallettest.Ptr("str_ptr")) + params.AddPair("key4", spvwallettest.Ptr(uint64(64))) + params.AddPair("key5", spvwallettest.Ptr(uint32(32))) + params.AddPair("key6", spvwallettest.Ptr(bool(false))) params.AddPair("key7", &filter.TimeRange{ From: &from, To: &to, @@ -45,8 +45,8 @@ func TestExtendedURLValues_AddPair(t *testing.T) { func TestExtendedURLValues_ParseToMap(t *testing.T) { // given: - to := querybuilderstest.ParseTime(t, "2024-10-07T14:03:26.736816Z") - from := querybuilderstest.ParseTime(t, "2024-10-07T14:03:26.736816Z") + to := spvwallettest.ParseTime(t, "2024-10-07T14:03:26.736816Z") + from := spvwallettest.ParseTime(t, "2024-10-07T14:03:26.736816Z") expectedValues := map[string]string{ "key1": "str", "key2": "1", @@ -61,10 +61,10 @@ func TestExtendedURLValues_ParseToMap(t *testing.T) { params := querybuilders.NewExtendedURLValues() params.AddPair("key1", "str") params.AddPair("key2", 1) - params.AddPair("key3", querybuilderstest.Ptr("str_ptr")) - params.AddPair("key4", querybuilderstest.Ptr(uint64(64))) - params.AddPair("key5", querybuilderstest.Ptr(uint32(32))) - params.AddPair("key6", querybuilderstest.Ptr(bool(false))) + params.AddPair("key3", spvwallettest.Ptr("str_ptr")) + params.AddPair("key4", spvwallettest.Ptr(uint64(64))) + params.AddPair("key5", spvwallettest.Ptr(uint32(32))) + params.AddPair("key6", spvwallettest.Ptr(bool(false))) params.AddPair("key7", &filter.TimeRange{ From: &from, To: &to, diff --git a/internal/api/v1/querybuilders/model_filter_builder_test.go b/internal/api/v1/querybuilders/model_filter_builder_test.go index 45d6c36e..8b22efdc 100644 --- a/internal/api/v1/querybuilders/model_filter_builder_test.go +++ b/internal/api/v1/querybuilders/model_filter_builder_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" - "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders/querybuilderstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" ) @@ -22,7 +22,7 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { "includeDeleted": []string{"true"}, }, filter: filter.ModelFilter{ - IncludeDeleted: querybuilderstest.Ptr(true), + IncludeDeleted: spvwallettest.Ptr(true), }, }, "model filter: filter with only created range 'from' field set": { @@ -31,7 +31,7 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { }, filter: filter.ModelFilter{ CreatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), }, }, }, @@ -41,7 +41,7 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { }, filter: filter.ModelFilter{ CreatedRange: &filter.TimeRange{ - To: querybuilderstest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), }, }, }, @@ -52,8 +52,8 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { }, filter: filter.ModelFilter{ CreatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), - To: querybuilderstest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), }, }, }, @@ -63,7 +63,7 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { }, filter: filter.ModelFilter{ UpdatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), }, }, }, @@ -73,7 +73,7 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { }, filter: filter.ModelFilter{ UpdatedRange: &filter.TimeRange{ - To: querybuilderstest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), }, }, }, @@ -84,8 +84,8 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { }, filter: filter.ModelFilter{ UpdatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), - To: querybuilderstest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), }, }, }, @@ -98,14 +98,14 @@ func TestModelFilterQueryBuilder_Build(t *testing.T) { "updatedRange[to]": []string{"2021-02-02T00:00:00Z"}, }, filter: filter.ModelFilter{ - IncludeDeleted: querybuilderstest.Ptr(true), + IncludeDeleted: spvwallettest.Ptr(true), CreatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), - To: querybuilderstest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), }, UpdatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), - To: querybuilderstest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), }, }, }, diff --git a/internal/api/v1/querybuilders/query_builder_test.go b/internal/api/v1/querybuilders/query_builder_test.go index 9d13dad4..da6c901c 100644 --- a/internal/api/v1/querybuilders/query_builder_test.go +++ b/internal/api/v1/querybuilders/query_builder_test.go @@ -8,7 +8,7 @@ import ( goclienterr "github.com/bitcoin-sv/spv-wallet-go-client/errors" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" - "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders/querybuilderstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" ) @@ -66,14 +66,14 @@ func TestQueryBuilder_Build(t *testing.T) { SortBy: "id", }, ModelFilter: filter.ModelFilter{ - IncludeDeleted: querybuilderstest.Ptr(true), + IncludeDeleted: spvwallettest.Ptr(true), CreatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), - To: querybuilderstest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), }, UpdatedRange: &filter.TimeRange{ - From: querybuilderstest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), - To: querybuilderstest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), + From: spvwallettest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), + To: spvwallettest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), }, }, MetadataFilter: querybuilders.Metadata{ diff --git a/internal/api/v1/querybuilders/querybuilderstest/querybuilderstest.go b/internal/api/v1/querybuilders/querybuilderstest/querybuilderstest.go deleted file mode 100644 index c24928e4..00000000 --- a/internal/api/v1/querybuilders/querybuilderstest/querybuilderstest.go +++ /dev/null @@ -1,18 +0,0 @@ -package querybuilderstest - -import ( - "testing" - "time" -) - -func ParseTime(t *testing.T, s string) time.Time { - ts, err := time.Parse(time.RFC3339Nano, s) - if err != nil { - t.Fatalf("test helper - time parse: %s", err) - } - return ts -} - -func Ptr[T any](value T) *T { - return &value -} diff --git a/queries/paymails.go b/queries/paymails.go new file mode 100644 index 00000000..b09edeba --- /dev/null +++ b/queries/paymails.go @@ -0,0 +1,46 @@ +package queries + +import ( + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/bitcoin-sv/spv-wallet/models/response" +) + +// PaymailAddressPage is an alias for the paymail addresses response page model returned by the SPV Wallet API. +// It provides a paginated list of paymails along with pagination metadata. +type PaymailAddressPage = response.PageModel[response.PaymailAddress] + +// PaymailQuery aggregates query parameters for constructing the paymail addresses endpoint URL. +// It contains filters for metadata, pagination, and user paymails-specific attributes. +type PaymailQuery struct { + Metadata map[string]any // Metadata filters for refining the search. + PageFilter filter.Page // Pagination details, including page number, size, and sorting. + PaymailFilter filter.AdminPaymailFilter // Filters for paymail attributes (ID, xPubID, alias, domain, public name). +} + +// PaymailQueryOption defines a functional option for configuring a ContactQuery instance. +// These options allow flexible setup of filters and pagination for the query. +type PaymailQueryOption func(*PaymailQuery) + +// PaymailQueryWithMetadataFilter adds metadata filters to the paymail addresses search URL. +// The provided metadata attributes are appended as query parameters. +func PaymailQueryWithMetadataFilter(m map[string]any) PaymailQueryOption { + return func(pq *PaymailQuery) { + pq.Metadata = m + } +} + +// PaymailQueryWithPageFilter adds pagination filters, like page number, size, and sorting options, +// to the paymail addresses search URL as query parameters. +func PaymailQueryWithPageFilter(f filter.Page) PaymailQueryOption { + return func(pq *PaymailQuery) { + pq.PageFilter = f + } +} + +// PaymailQueryWithPaymailFilter adds filters for paymail address attributes like ID, xPubID, alias, domain, public name. +// These filters are appended as query parameters to the paymail addresses search URL. +func PaymailQueryWithPaymailFilter(pf filter.AdminPaymailFilter) PaymailQueryOption { + return func(pq *PaymailQuery) { + pq.PaymailFilter = pf + } +}