Skip to content

Commit

Permalink
refactor(SPV-1230): admin endpoints group - Contacts API. (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgosek-4chain authored Dec 4, 2024
1 parent d055443 commit 83cb51d
Show file tree
Hide file tree
Showing 30 changed files with 918 additions and 53 deletions.
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
}

// DeleteContact 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) DeleteContact(ctx context.Context, ID string) error {
err := a.contactsAPI.DeleteContact(ctx, ID)
if err != nil {
msg := fmt.Sprintf("delete 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("delete 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,11 @@ 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.
* delete-contact-as-admin: Delete contact with a given ID as Admin.
* 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 +34,10 @@ 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.
* 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

delete-contact-as-admin:
desc: "Delete contact with a given ID as Admin."
silent: true
cmds:
- go run ./delete_contact_as_admin/delete_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)
}
23 changes: 23 additions & 0 deletions examples/delete_contact_as_admin/delete_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.DeleteContact(context.Background(), id)
if err != nil {
log.Fatal(err)
}
}
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())
if err != nil {
log.Fatal(err)
}

exampleutil.Print("[HTTP GET] Contacts - api/v1/admin/contacts", page)
}
28 changes: 28 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,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.XPubs(context.Background(), queries.XPubQueryWithPageFilter(filter.Page{
Size: 1,
}))
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)
}
}
Loading

0 comments on commit 83cb51d

Please sign in to comment.