Skip to content

Commit

Permalink
feat(SPV-789): extend PIKE capability
Browse files Browse the repository at this point in the history
  • Loading branch information
pawellewandowski98 committed May 20, 2024
1 parent a69a346 commit 12fc910
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 61 deletions.
6 changes: 5 additions & 1 deletion brfc_definintions.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ const (
BRFCVerifyPublicKeyOwner = "a9f510c16bde" // more info: http://bsvalias.org/05-verify-public-key-owner.html
BRFCBeefTransaction = "5c55a7fdb7bb" // more info: https://bsv.brc.dev/payments/0070

BRFCPike = "8c4ed5ef8ace" // more info: TODO BUX-665
BRFCTemporaryPike = "8c4ed5ef8ace" // Temporary BRFC ID for PIKE

BRFCPike = "935478af7bf2"
BRFCPikeInvite = "invite"
BRFCPikeOutputs = "outputs"
)

// BRFCKnownSpecifications is a running list of all known BRFC specifications
Expand Down
21 changes: 21 additions & 0 deletions pike.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"strings"
"time"
)

type PikeContactRequestResponse struct {
Expand All @@ -17,6 +18,26 @@ type PikeContactRequestPayload struct {
Paymail string `json:"paymail"`
}

// TODO: check if everything is needed after whole PIKE implementation
type PikePaymentDestinationsRequest struct {
SenderName string `json:"senderName"`
SenderPaymail string `json:"senderPaymail"`
Amount uint64 `json:"amount"`
Dt time.Time `json:"dt"`
Reference string `json:"reference"`
Signature string `json:"signature"`
}

type PikePaymentDestinationsResponse struct {
Outputs []PikePaymentDestination `json:"outputs"`
Reference string `json:"reference"`
}

type PikePaymentDestination struct {
Script string `json:"script"`
Satoshis int `json:"satoshis"`
}

func (c *Client) AddContactRequest(url, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) {

if err := c.validateUrlWithPaymail(url, alias, domain); err != nil {
Expand Down
46 changes: 42 additions & 4 deletions server/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package server

import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"strings"

"github.com/gin-gonic/gin"

"github.com/bitcoin-sv/go-paymail"
)

Expand All @@ -16,6 +15,7 @@ type CallableCapability struct {
Handler gin.HandlerFunc
}

type NestedCapabilitiesMap map[string]CallableCapabilitiesMap
type CallableCapabilitiesMap map[string]CallableCapability
type StaticCapabilitiesMap map[string]any

Expand Down Expand Up @@ -80,16 +80,41 @@ func (c *Configuration) SetBeefCapabilities() {
)
}

func (c *Configuration) SetPikeCapabilities() {
func (c *Configuration) SetPikeContactCapabilities() {
_addCapabilities(c.callableCapabilities,
CallableCapabilitiesMap{
paymail.BRFCPike: CallableCapability{
paymail.BRFCTemporaryPike: CallableCapability{
Path: fmt.Sprintf("/pike/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.pikeNewContact,
},
},
)
_addCapabilities(c.nestedCapabilities,
NestedCapabilitiesMap{
paymail.BRFCPike: CallableCapabilitiesMap{
paymail.BRFCPikeInvite: CallableCapability{
Path: fmt.Sprintf("/contact/invite/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.pikeNewContact,
},
},
},
)
}

func (c *Configuration) SetPikePaymentCapabilities() {
_addNestedCapabilities(c.nestedCapabilities,
NestedCapabilitiesMap{
paymail.BRFCPike: CallableCapabilitiesMap{
paymail.BRFCPikeOutputs: CallableCapability{
Path: fmt.Sprintf("/pike/outputs/%s", PaymailAddressTemplate),
Method: http.MethodPost,
Handler: c.pikeGetPaymentDestinations,
},
},
},
)
}

func _addCapabilities[T any](base map[string]T, newCaps map[string]T) {
Expand All @@ -98,6 +123,12 @@ func _addCapabilities[T any](base map[string]T, newCaps map[string]T) {
}
}

func _addNestedCapabilities(base NestedCapabilitiesMap, newCaps NestedCapabilitiesMap) {
for key, val := range newCaps {
_addCapabilities(base[key], val)
}
}

// showCapabilities will return the service discovery results for the server
// and list all active capabilities of the Paymail server
//
Expand Down Expand Up @@ -142,6 +173,13 @@ func (c *Configuration) EnrichCapabilities(host string) (*paymail.CapabilitiesPa
for key, cap := range c.callableCapabilities {
payload.Capabilities[key] = serviceUrl + string(cap.Path)
}
for key, cap := range c.nestedCapabilities {
payload.Capabilities[key] = make(map[string]interface{})
for nestedKey, nestedCap := range cap {
payload.Capabilities[key].(map[string]interface{})[nestedKey] = serviceUrl + string(nestedCap.Path)
//payload.Capabilities[key][nestedKey] = serviceUrl + string(nestedCap.Path)
}
}
return payload, nil
}

Expand Down
22 changes: 15 additions & 7 deletions server/config.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package server

import (
"github.com/rs/zerolog"
"slices"
"strings"
"time"

"github.com/rs/zerolog"

"github.com/bitcoin-sv/go-paymail"
)

Expand All @@ -23,14 +22,17 @@ type Configuration struct {
GenericCapabilitiesEnabled bool `json:"generic_capabilities_enabled"`
P2PCapabilitiesEnabled bool `json:"p2p_capabilities_enabled"`
BeefCapabilitiesEnabled bool `json:"beef_capabilities_enabled"`
PikeCapabilitiesEnabled bool `json:"pike_capabilities_enabled"`
PikeContactCapabilitiesEnabled bool `json:"pike_contact_capabilities_enabled"`
PikePaymentCapabilitiesEnabled bool `json:"pike_payment_capabilities_enabled"`
ServiceName string `json:"service_name"`
Timeout time.Duration `json:"timeout"`
Logger *zerolog.Logger `json:"logger"`

// private
actions PaymailServiceProvider
pikeActions PikeServiceProvider
pikeContactActions PikeContactServiceProvider
pikePaymentActions PikePaymentServiceProvider
nestedCapabilities NestedCapabilitiesMap
callableCapabilities CallableCapabilitiesMap
staticCapabilities StaticCapabilitiesMap
}
Expand Down Expand Up @@ -139,9 +141,15 @@ func NewConfig(serviceProvider *PaymailServiceLocator, opts ...ConfigOps) (*Conf
if config.BeefCapabilitiesEnabled {
config.SetBeefCapabilities()
}
if config.PikeCapabilitiesEnabled {
config.SetPikeCapabilities()
config.pikeActions = serviceProvider.GetPikeService()

if config.PikeContactCapabilitiesEnabled {
config.SetPikeContactCapabilities()
config.pikeContactActions = serviceProvider.GetPikeContactService()
}

if config.PikePaymentCapabilitiesEnabled {
config.SetPikePaymentCapabilities()
config.pikePaymentActions = serviceProvider.GetPikePaymentService()
}

// Validate the configuration
Expand Down
17 changes: 13 additions & 4 deletions server/config_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ func defaultConfigOptions() *Configuration {
GenericCapabilitiesEnabled: true,
P2PCapabilitiesEnabled: false,
BeefCapabilitiesEnabled: false,
PikeCapabilitiesEnabled: false,
PikeContactCapabilitiesEnabled: true,
PikePaymentCapabilitiesEnabled: false,
ServiceName: paymail.DefaultServiceName,
Timeout: DefaultTimeout,
Logger: logging.GetDefaultLogger(),
nestedCapabilities: make(NestedCapabilitiesMap),
callableCapabilities: make(CallableCapabilitiesMap),
staticCapabilities: make(StaticCapabilitiesMap),
}
Expand Down Expand Up @@ -59,10 +61,17 @@ func WithBeefCapabilities() ConfigOps {
}
}

// WithPikeCapabilities will load the PIKE capabilities
func WithPikeCapabilities() ConfigOps {
// WithPikeContactCapabilities will load the PIKE capabilities
func WithPikeContactCapabilities() ConfigOps {
return func(c *Configuration) {
c.PikeCapabilitiesEnabled = true
c.PikeContactCapabilitiesEnabled = true
}
}

// WithPikePaymentCapabilities will load the PIKE capabilities
func WithPikePaymentCapabilities() ConfigOps {
return func(c *Configuration) {
c.PikePaymentCapabilitiesEnabled = true
}
}

Expand Down
40 changes: 31 additions & 9 deletions server/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
)

type PaymailServiceLocator struct {
paymailService PaymailServiceProvider
pikeService PikeServiceProvider
paymailService PaymailServiceProvider
pikeContactService PikeContactServiceProvider
pikePaymentService PikePaymentServiceProvider
}

func (l *PaymailServiceLocator) RegisterPaymailService(s PaymailServiceProvider) {
Expand All @@ -24,16 +25,28 @@ func (l *PaymailServiceLocator) GetPaymailService() PaymailServiceProvider {
return l.paymailService
}

func (l *PaymailServiceLocator) RegisterPikeService(s PikeServiceProvider) {
l.pikeService = s
func (l *PaymailServiceLocator) RegisterPikeContactService(s PikeContactServiceProvider) {
l.pikeContactService = s
}

func (l *PaymailServiceLocator) GetPikeService() PikeServiceProvider {
if l.pikeService == nil {
panic("PikeServiceProvider was not registered")
func (l *PaymailServiceLocator) GetPikeContactService() PikeContactServiceProvider {
if l.pikeContactService == nil {
panic("PikeContactServiceProvider was not registered")
}

return l.pikeService
return l.pikeContactService
}

func (l *PaymailServiceLocator) RegisterPikePaymentService(s PikePaymentServiceProvider) {
l.pikePaymentService = s
}

func (l *PaymailServiceLocator) GetPikePaymentService() PikePaymentServiceProvider {
if l.pikePaymentService == nil {
panic("PikePaymentServiceProvider was not registered")
}

return l.pikePaymentService
}

// PaymailServiceProvider the paymail server interface that needs to be implemented
Expand Down Expand Up @@ -70,10 +83,19 @@ type PaymailServiceProvider interface {
) error
}

type PikeServiceProvider interface {
type PikeContactServiceProvider interface {
AddContact(
ctx context.Context,
requesterPaymail string,
contact *paymail.PikeContactRequestPayload,
) error
}

type PikePaymentServiceProvider interface {
CreatePikeDestinationResponse(
ctx context.Context,
alias, domain string,
satoshis uint64,
metaData *RequestMetadata,
) (*paymail.PikePaymentDestinationsResponse, error)
}
38 changes: 3 additions & 35 deletions server/p2p_payment_destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,21 @@ type p2pDestinationRequestBody struct {
//
// Specs: https://docs.moneybutton.com/docs/paymail-07-p2p-payment-destination.html
func (c *Configuration) p2pDestination(context *gin.Context) {
incomingPaymail := context.Param(PaymailAddressParamName)

// Parse, sanitize and basic validation
alias, domain, paymailAddress := paymail.SanitizePaymail(incomingPaymail)
if len(paymailAddress) == 0 {
ErrorResponse(context, ErrorInvalidParameter, "invalid paymail: "+incomingPaymail, http.StatusBadRequest)
return
} else if !c.IsAllowedDomain(domain) {
ErrorResponse(context, ErrorUnknownDomain, "domain unknown: "+domain, http.StatusBadRequest)
return
}
var b p2pDestinationRequestBody
err := context.Bind(&b)
if err != nil {
ErrorResponse(context, ErrorInvalidParameter, "error decoding body: "+err.Error(), http.StatusBadRequest)
return
}

// Start the PaymentRequest
paymentRequest := &paymail.PaymentRequest{
Satoshis: b.Satoshis,
}

// Did we get some satoshis?
if paymentRequest.Satoshis == 0 {
ErrorResponse(context, ErrorMissingField, "missing parameter: satoshis", http.StatusBadRequest)
return
}

// Create the metadata struct
md := CreateMetadata(context.Request, alias, domain, "")
md.PaymentDestination = paymentRequest

// Get from the data layer
foundPaymail, err := c.actions.GetPaymailByAlias(context.Request.Context(), alias, domain, md)
if err != nil {
ErrorResponse(context, ErrorFindingPaymail, err.Error(), http.StatusExpectationFailed)
return
} else if foundPaymail == nil {
ErrorResponse(context, ErrorPaymailNotFound, "paymail not found", http.StatusNotFound)
alias, domain, md, ok := c.GetPaymailAndCreateMetadata(context, b.Satoshis)
if !ok {
return
}

// Create the response
var response *paymail.PaymentDestinationPayload
if response, err = c.actions.CreateP2PDestinationResponse(
context.Request.Context(), alias, domain, paymentRequest.Satoshis, md,
context.Request.Context(), alias, domain, b.Satoshis, md,
); err != nil {
ErrorResponse(context, ErrorScript, "error creating output script(s): "+err.Error(), http.StatusExpectationFailed)
return
Expand Down
Loading

0 comments on commit 12fc910

Please sign in to comment.