Skip to content

Commit

Permalink
feat(SPV-1284): add admin method to create contact between two parties (
Browse files Browse the repository at this point in the history
  • Loading branch information
wregulski authored Dec 12, 2024
1 parent 57d1951 commit de1faa4
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 0 deletions.
38 changes: 38 additions & 0 deletions actions/admin/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,44 @@ func contactsAccept(c *gin.Context, _ *reqctx.AdminContext) {
c.JSON(http.StatusOK, contract)
}

// contactsCreate will perform create contact action for the given paymail
// @Summary Create contact
// @Description Create contact
// @Tags Admin
// @Produce json
// @Param paymail path string false "Contact paymail"
// @Success 200 {object} response.Contact "Changed contact"
// @Failure 400 "Bad request - Error while getting paymail from path"
// @Failure 404 "Not found - Error while getting contact requester by paymail"
// @Failure 409 "Contact already exists - Unable to add duplicate contact"
// @Failure 500 "Internal server error - Error while adding new contact"
// @Router /api/v1/admin/contacts/{paymail} [post]
// @Security x-auth-xpub
func contactsCreate(c *gin.Context, _ *reqctx.AdminContext) {
logger := reqctx.Logger(c)
contactPaymail := c.Param("paymail")
if contactPaymail == "" {
spverrors.ErrorResponse(c, spverrors.ErrMissingContactPaymailParam, logger)
return
}

var req *CreateContact
if err := c.Bind(&req); err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest.WithTrace(err), logger)
return
}

metadata := mappings.MapToMetadata(req.Metadata)
contact, err := reqctx.Engine(c).AdminCreateContact(c, contactPaymail, req.CreatorPaymail, req.FullName, metadata)
if err != nil {
spverrors.ErrorResponse(c, err, logger)
return
}

contract := mappings.MapToContactContract(contact)
c.JSON(http.StatusOK, contract)
}

// contactsConfirm will perform Confirm action on contacts with the given xpub ids and paymails
// Perform confirm action on contacts godoc
// @Summary Confirm contacts pair
Expand Down
10 changes: 10 additions & 0 deletions actions/admin/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ type CreateXpub struct {
Key string `json:"key" example:"xpub661MyMwAqRbcGpZVrSHU..."`
}

// CreateContact is the model for creating a contact by admin
type CreateContact struct {
// Paymail address of the creator (Person A) who owns the contact being added.
CreatorPaymail string `json:"creatorPaymail"`
// The complete name of the contact, including first name, and last name.
FullName string `json:"fullName"`
// Accepts a JSON object for embedding custom metadata, enabling arbitrary additional information to be associated with the resource
Metadata engine.Metadata `json:"metadata" swaggertype:"object,string" example:"key:value,key2:value2"`
}

// UpdateContact is the model for updating a contact
type UpdateContact struct {
// Accepts a JSON object for embedding custom metadata, enabling arbitrary additional information to be associated with the resource
Expand Down
2 changes: 2 additions & 0 deletions actions/admin/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func RegisterRoutes(handlersManager *handlers.Manager) {
adminGroupOld.POST("/access-keys/count", handlers.AsAdmin(accessKeysCount))
adminGroupOld.POST("/contact/search", handlers.AsAdmin(contactsSearchOld))
adminGroupOld.PATCH("/contact/:id", handlers.AsAdmin(contactsUpdateOld))
adminGroupOld.POST("/contact/:paymail", handlers.AsAdmin(contactsCreate))
adminGroupOld.DELETE("/contact/:id", handlers.AsAdmin(contactsDeleteOld))
adminGroupOld.PATCH("/contact/accepted/:id", handlers.AsAdmin(contactsAcceptOld))
adminGroupOld.PATCH("/contact/rejected/:id", handlers.AsAdmin(contactsRejectOld))
Expand Down Expand Up @@ -54,6 +55,7 @@ func RegisterRoutes(handlersManager *handlers.Manager) {
adminGroup.DELETE("/invitations/:id", handlers.AsAdmin(contactsReject))
adminGroup.DELETE("/contacts/:id", handlers.AsAdmin(contactsDelete))
adminGroup.PUT("/contacts/:id", handlers.AsAdmin(contactsUpdate))
adminGroup.POST("/contacts/:paymail", handlers.AsAdmin(contactsCreate))
adminGroup.POST("/contacts/confirmations", handlers.AsAdmin(contactsConfirm))

// access keys
Expand Down
45 changes: 45 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,51 @@ const docTemplate = `{
}
}
},
"/api/v1/admin/contacts/{paymail}": {
"post": {
"security": [
{
"x-auth-xpub": []
}
],
"description": "Create contact",
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "Create contact",
"parameters": [
{
"type": "string",
"description": "Contact paymail",
"name": "paymail",
"in": "path"
}
],
"responses": {
"200": {
"description": "Changed contact",
"schema": {
"$ref": "#/definitions/response.Contact"
}
},
"400": {
"description": "Bad request - Error while getting paymail from path"
},
"404": {
"description": "Not found - Error while getting contact requester by paymail"
},
"409": {
"description": "Contact already exists - Unable to add duplicate contact"
},
"500": {
"description": "Internal server error - Error while adding new contact"
}
}
}
},
"/api/v1/admin/invitations/{id}": {
"delete": {
"security": [
Expand Down
45 changes: 45 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,51 @@
}
}
},
"/api/v1/admin/contacts/{paymail}": {
"post": {
"security": [
{
"x-auth-xpub": []
}
],
"description": "Create contact",
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "Create contact",
"parameters": [
{
"type": "string",
"description": "Contact paymail",
"name": "paymail",
"in": "path"
}
],
"responses": {
"200": {
"description": "Changed contact",
"schema": {
"$ref": "#/definitions/response.Contact"
}
},
"400": {
"description": "Bad request - Error while getting paymail from path"
},
"404": {
"description": "Not found - Error while getting contact requester by paymail"
},
"409": {
"description": "Contact already exists - Unable to add duplicate contact"
},
"500": {
"description": "Internal server error - Error while adding new contact"
}
}
}
},
"/api/v1/admin/invitations/{id}": {
"delete": {
"security": [
Expand Down
28 changes: 28 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2798,6 +2798,34 @@ paths:
summary: Update contact FullName or Metadata
tags:
- Admin
/api/v1/admin/contacts/{paymail}:
post:
description: Create contact
parameters:
- description: Contact paymail
in: path
name: paymail
type: string
produces:
- application/json
responses:
"200":
description: Changed contact
schema:
$ref: '#/definitions/response.Contact'
"400":
description: Bad request - Error while getting paymail from path
"404":
description: Not found - Error while getting contact requester by paymail
"409":
description: Contact already exists - Unable to add duplicate contact
"500":
description: Internal server error - Error while adding new contact
security:
- x-auth-xpub: []
summary: Create contact
tags:
- Admin
/api/v1/admin/contacts/confirmations:
post:
description: Marks the contact entries as mutually confirmed, after ensuring
Expand Down
74 changes: 74 additions & 0 deletions engine/action_contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package engine
import (
"context"
"fmt"
"strings"

"github.com/bitcoin-sv/go-paymail"
"github.com/bitcoin-sv/spv-wallet/engine/datastore"
Expand Down Expand Up @@ -163,6 +164,79 @@ func (c *Client) UpdateContact(ctx context.Context, id, fullName string, metadat
return contact, nil
}

// AdminCreateContact creates a new contact - xpubId is retrieved by the creatorPaymail.
func (c *Client) AdminCreateContact(ctx context.Context, contactPaymail, creatorPaymail, fullName string, metadata *Metadata) (*Contact, error) {
if err := validateNewContactReqFields(fullName, creatorPaymail); err != nil {
return nil, err
}

creatorPaymailAddr, err := getPaymailAddress(ctx, creatorPaymail, c.DefaultModelOptions()...)
if err != nil {
return nil, spverrors.ErrCouldNotFindPaymail.Wrap(err)
}

if creatorPaymailAddr == nil {
return nil, spverrors.ErrCouldNotFindPaymail
}

creatorXPub, err := getXpubByID(ctx, creatorPaymailAddr.XpubID, c.DefaultModelOptions()...)
if err != nil {
return nil, spverrors.ErrCouldNotFindXpub.Wrap(err)
}

newContactSanitisedPaymail, err := c.PaymailService().GetSanitizedPaymail(contactPaymail)
if err != nil {
return nil, spverrors.Wrapf(err, "requested duplicate paymail is invalid")
}

pkiNewContact, err := c.PaymailService().GetPkiForPaymail(ctx, newContactSanitisedPaymail)
if err != nil {
return nil, spverrors.ErrGettingPKIFailed.Wrap(err)
}

duplicate, err := getContact(ctx, contactPaymail, creatorXPub.ID, c.DefaultModelOptions()...)
if err != nil {
return nil, err
}
if duplicate != nil {
return nil, spverrors.ErrContactAlreadyExists
}

opts := c.DefaultModelOptions()
if metadata != nil {
for key, value := range *metadata {
opts = append(opts, WithMetadata(key, value))
}
}

contact := newContact(
fullName,
contactPaymail,
pkiNewContact.PubKey,
creatorXPub.ID,
// newly created contact should be in the status of ContactNotConfirmed - initial state
ContactNotConfirmed,
opts...,
)
if err = contact.Save(ctx); err != nil {
return nil, spverrors.ErrSaveContact.Wrap(err)
}

return contact, nil
}

func validateNewContactReqFields(fullName, creatorPaymail string) error {
if strings.TrimSpace(fullName) == "" {
return spverrors.ErrMissingContactFullName
}

if strings.TrimSpace(creatorPaymail) == "" {
return spverrors.ErrMissingContactCreatorPaymail
}

return nil
}

// AdminChangeContactStatus changes the status of the contact, should be used only by the admin.
func (c *Client) AdminChangeContactStatus(ctx context.Context, id string, status ContactStatus) (*Contact, error) {
contact, err := getContactByID(ctx, id, c.DefaultModelOptions()...)
Expand Down
Loading

0 comments on commit de1faa4

Please sign in to comment.