Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(SPV-1230): admin endpoints group - Contacts API. #23

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
bip32 "github.com/bitcoin-sv/go-sdk/compat/bip32"
"github.com/bitcoin-sv/spv-wallet-go-client/commands"
"github.com/bitcoin-sv/spv-wallet-go-client/config"
xpubs "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/users"
"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/xpubs"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/auth"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/restyutil"
"github.com/bitcoin-sv/spv-wallet-go-client/queries"
Expand All @@ -25,7 +27,9 @@ import (
// Methods may return wrapped errors, including models.SPVError or
// ErrUnrecognizedAPIResponse, depending on the behavior of the SPV Wallet API.
type AdminAPI struct {
xpubsAPI *xpubs.API // Internal API for managing operations related to XPubs.
xpubsAPI *xpubs.API // Internal API for managing operations related to XPubs.
contactsAPI *contacts.API
invitationsAPI *invitations.API
}

// CreateXPub creates a new XPub record via the Admin XPubs API.
Expand Down Expand Up @@ -60,6 +64,74 @@ func (a *AdminAPI) XPubs(ctx context.Context, opts ...queries.XPubQueryOption) (
return res, nil
}

// Contacts retrieves a paginated list of user contacts from the admin contacts API.
//
// The response includes contact data along with pagination details, such as the
// current page, sort order, and sortBy field. Optional query parameters can be
// provided using query options. The result is unmarshaled into a *queries.UserContactsPage.
// Returns an error if the API request fails or the response cannot be decoded.
func (a *AdminAPI) Contacts(ctx context.Context, opts ...queries.ContactQueryOption) (*queries.UserContactsPage, error) {
res, err := a.contactsAPI.Contacts(ctx, opts...)
if err != nil {
return nil, contacts.HTTPErrorFormatter("retrieve user contacts page", err).FormatGetErr()
}

return res, nil
}

// ContactUpdate updates a user's contact information through the admin contacts API.
//
// This method uses the `UpdateContact` command to specify the details of the contact to update.
// It sends the update request to the API, unmarshals the response into a `*response.Contact`,
// and returns the updated contact. If the API request fails or the response cannot be decoded,
// an error is returned.
func (a *AdminAPI) ContactUpdate(ctx context.Context, cmd *commands.UpdateContact) (*response.Contact, error) {
res, err := a.contactsAPI.UpdateContact(ctx, cmd)
if err != nil {
msg := fmt.Sprintf("update contact with ID: %s", cmd.ID)
return nil, contacts.HTTPErrorFormatter(msg, err).FormatPutErr()
}

return res, nil
}

// RemoveContact deletes a user contact with the given ID via the admin contacts API.
// Returns an error if the API request fails or the response cannot be decoded.
// A nil error indicates the deleting contact was successful.
func (a *AdminAPI) RemoveContact(ctx context.Context, ID string) error {
err := a.contactsAPI.RemoveContact(ctx, ID)
if err != nil {
msg := fmt.Sprintf("remove contact with ID: %s", ID)
return contacts.HTTPErrorFormatter(msg, err).FormatDeleteErr()
}

return nil
}

// AcceptInvitation processes and accepts a user contact invitation using the given ID via the admin invitations API.
// Returns an error if the API request fails. A nil error indicates the invitation was successfully accepted.
func (a *AdminAPI) AcceptInvitation(ctx context.Context, ID string) error {
err := a.invitationsAPI.AcceptInvitation(ctx, ID)
if err != nil {
msg := fmt.Sprintf("accept invitation with ID: %s", ID)
return invitations.HTTPErrorFormatter(msg, err).FormatDeleteErr()
}

return nil
}

// RejectInvitation processes and rejects a user contact invitation using the given ID via the admin invitations API.
// Returns an error if the API request fails. A nil error indicates the invitation was successfully rejected.
func (a *AdminAPI) RejectInvitation(ctx context.Context, ID string) error {
err := a.invitationsAPI.RejectInvitation(ctx, ID)
if err != nil {
msg := fmt.Sprintf("reject invitation with ID: %s", ID)
return invitations.HTTPErrorFormatter(msg, err).FormatDeleteErr()
}

return nil
}

// NewAdminAPIWithXPriv initializes a new AdminAPI instance using an extended private key (xPriv).
// This function configures the API client with the provided configuration and uses the xPriv key for authentication.
// If any step fails, an appropriate error is returned.
Expand Down Expand Up @@ -106,5 +178,9 @@ func initAdminAPI(cfg config.Config, auth authenticator) (*AdminAPI, error) {
}

httpClient := restyutil.NewHTTPClient(cfg, auth)
return &AdminAPI{xpubsAPI: xpubs.NewAPI(url, httpClient)}, nil
return &AdminAPI{
xpubsAPI: xpubs.NewAPI(url, httpClient),
contactsAPI: contacts.NewAPI(url, httpClient),
invitationsAPI: invitations.NewAPI(url, httpClient),
}, nil
}
12 changes: 11 additions & 1 deletion commands/contacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ package commands
// UpsertContact holds the necessary arguments for adding or updating a user's contact information.
type UpsertContact struct {
FullName string `json:"fullName"` // The full name of the user.
Metadata map[string]any `json:"metadata"` // Metadata associated with the transaction.
Metadata map[string]any `json:"metadata"` // Metadata associated with the contact.
Paymail string `json:"requesterPaymail"` // Paymail address of the user, which is used for secure and simplified payment transfers.
}

// UpdateContact represents the arguments defined for updating a user's contact information.
//
// Note: The `ID` field is not included in the request body sent to the SPV Wallet API.
// Instead, it is used as part of the endpoint path (e.g., /api/v1/admin/contacts/{ID}).
type UpdateContact struct {
ID string `json:"-"` // Unique identifier of the contact to be updated.
FullName string `json:"fullName"` // The full name of the contact.
Metadata map[string]any `json:"metadata"` // Metadata associated with the contact.
}
20 changes: 15 additions & 5 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ the wallet client package during interaction wit the SPV Wallet API.
```
task: [default] task --list
task: Available tasks for this project:
* 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.
* fetch-contacts-as-admin: Fetch contacts page as Admin.
* fetch-user-contact-by-paymail: Fetch user contact by given paymail.
* fetch-user-contacts: Fetch user contacts page.
* fetch-user-merkleroots: Fetch user Merkle roots page.
Expand All @@ -30,7 +33,11 @@ task: Available tasks for this project:
* fetch-user-transactions: Fetch user transactions page.
* fetch-user-utxos: Fetch user UTXOs page.
* fetch-user-xpub: Fetch current authorized user's xpub info.
* fetch-xpubs-as-admin: Fetch xPubs page as Admin.
* generate-keys: Generate keys for SPV Wallet API access.
* reject-invitation-as-admin: Reject invitation with a given ID as Admin.
* remove-contact-as-admin: Remove contact with a given ID as Admin.
* update-contact-as-admin: Update contact with a given ID as Admin.
* user-contact-confirmation: Confirm user contact with a given paymail address.
* user-contact-remove: Remove user contact with a given paymail address.
* user-contact-unconfirm: Unconfirm user contact with a given paymail address.
Expand All @@ -53,7 +60,7 @@ task: Available tasks for this project:

Before interacting with the SPV Wallet API, you must complete the authorization process.

To begin, generate a pair of keys using the `task generate-keys command`, which is included in the dedicated Taskfile.
To begin, generate a pair of keys using the `task generate-keys` command, which is included in the dedicated Taskfile.

**Example output:**
```
Expand Down Expand Up @@ -89,12 +96,12 @@ import (
func main() {
xPriv := "121d2f43-4261-42ab-813e-3d3fa4d87313"
cfg := wallet.NewDefaultConfig("http://localhost:3003")
spv, err := wallet.NewWithXPriv(cfg, xPriv)
userAPI, err := wallet.NewUserAPIWithXPriv(cfg, xPriv)
if err != nil {
log.Fatal(err)
}

xPub, err := spv.XPub(context.Background())
xPub, err := userAPI.XPub(context.Background())
if err != nil {
log.Fatal(err)
}
Expand All @@ -111,8 +118,11 @@ func Print(s string, a any) {
}
fmt.Println(string(res))
}

```

> [!TIP]
> The same principle applies when creating an AdminAPI client instance using one of the available constructors.

**Example output:**

```
Expand Down Expand Up @@ -144,5 +154,5 @@ cd examples
task name_of_the_example
```

> [!TIP]
> [!TIP]
> To verify Taskfile installation run: `task` command in the terminal.
42 changes: 42 additions & 0 deletions examples/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,48 @@ tasks:
- go run ../walletkeys/cmd/main.go
- echo "=================================================================="

accept-invitation-as-admin:
desc: "Accept invitation with a given ID as Admin."
silent: true
cmds:
- go run ./accept_invitation_as_admin/accept_invitation_as_admin.go

reject-invitation-as-admin:
desc: "Reject invitation with a given ID as Admin."
silent: true
cmds:
- go run ./reject_invitation_as_admin/reject_invitation_as_admin.go

fetch-contacts-as-admin:
desc: "Fetch contacts page as Admin."
silent: true
cmds:
- go run ./fetch_contacts_as_admin/fetch_contacts_as_admin.go

update-contact-as-admin:
desc: "Update contact with a given ID as Admin."
silent: true
cmds:
- go run ./update_contact_as_admin/update_contact_as_admin.go

remove-contact-as-admin:
desc: "Remove contact with a given ID as Admin."
silent: true
cmds:
- go run ./remove_contact_as_admin/remove_contact_as_admin.go

fetch-xpubs-as-admin:
desc: "Fetch xPubs page as Admin."
silent: true
cmds:
- go run ./fetch_xpubs_as_admin/fetch_xpubs_as_admin.go

create-xpub-as-admin:
desc: "Create xPub as Admin."
silent: true
cmds:
- go run ./create_xpub_as_admin/create_xpub_as_admin.go

fetch-user-shared-config:
desc: "Fetch user shared configuration."
silent: true
Expand Down
23 changes: 23 additions & 0 deletions examples/accept_invitation_as_admin/accept_invitation_as_admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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"
)

func main() {
adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
if err != nil {
log.Fatal(err)
}

id := "cd401f8f-a9ae-4efe-a2a1-de3e6832593d"
err = adminAPI.AcceptInvitation(context.Background(), id)
if err != nil {
log.Fatal(err)
}
}
29 changes: 29 additions & 0 deletions examples/create_xpub_as_admin/create_xpub_as_admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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() {
adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
if err != nil {
log.Fatal(err)
}

xPub, err := adminAPI.CreateXPub(context.Background(), &commands.CreateUserXpub{
XPub: "1c318ad8-5ee4-42d3-9cf5-5b0babec9156",
Metadata: querybuilders.Metadata{"key": "value"},
})
if err != nil {
log.Fatal(err)
}

exampleutil.Print("[HTTP POST] Create XPub - api/v1/admin/users", xPub)
}
24 changes: 24 additions & 0 deletions examples/fetch_contacts_as_admin/fetch_contacts_as_admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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"
)

func main() {
adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
if err != nil {
log.Fatal(err)
}

page, err := adminAPI.Contacts(context.Background())
mgosek-4chain marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Fatal(err)
}

exampleutil.Print("[HTTP GET] Contacts - api/v1/admin/contacts", page)
}
24 changes: 24 additions & 0 deletions examples/fetch_xpubs_as_admin/fetch_xpubs_as_admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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"
)

func main() {
adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
mgosek-4chain marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Fatal(err)
}

page, err := adminAPI.XPubs(context.Background())
if err != nil {
log.Fatal(err)
}

exampleutil.Print("[HTTP GET] XPubs - api/v1/admin/users", page)
}
23 changes: 23 additions & 0 deletions examples/reject_invitation_as_admin/reject_invitation_as_admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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"
)

func main() {
adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
if err != nil {
log.Fatal(err)
}

id := "bc84cb22-c9aa-415f-93e1-2ed0af27a6ef"
err = adminAPI.RejectInvitation(context.Background(), id)
if err != nil {
log.Fatal(err)
}
}
23 changes: 23 additions & 0 deletions examples/remove_contact_as_admin/remove_contact_as_admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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"
)

func main() {
adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv)
if err != nil {
log.Fatal(err)
}

id := "88db6027-e38a-43b7-97a0-45f08d535256"
err = adminAPI.RemoveContact(context.Background(), id)
if err != nil {
log.Fatal(err)
}
}
Loading
Loading