From 249c3a33590a951315c3be47a66a07af5af6b14b Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Wed, 29 May 2024 11:49:57 +0200 Subject: [PATCH 1/5] feat(SPV-813): extend PIKE capability --- capabilities.go | 61 ++++ capabilities_test.go | 297 ++++++++++++++++-- .../get_invite_url_from_pike_template/main.go | 42 +++ .../get_outputs_template_for_pike/main.go | 36 +++ interface.go | 5 +- 5 files changed, 411 insertions(+), 30 deletions(-) create mode 100644 examples/client/get_invite_url_from_pike_template/main.go create mode 100644 examples/client/get_outputs_template_for_pike/main.go diff --git a/capabilities.go b/capabilities.go index b7877de..9458e16 100644 --- a/capabilities.go +++ b/capabilities.go @@ -30,6 +30,18 @@ type CapabilitiesResponse struct { type CapabilitiesPayload struct { BsvAlias string `json:"bsvalias"` // Version of the bsvalias Capabilities map[string]interface{} `json:"capabilities"` // Raw list of the capabilities + Pike *PikeCapability `json:"pike,omitempty"` +} + +// PikeCapability represents the structure of the PIKE capability +type PikeCapability struct { + Invite string `json:"invite,omitempty"` + Outputs string `json:"outputs,omitempty"` +} + +// PikeOutputs represents the structure of the PIKE outputs +type PikeOutputs struct { + URL string `json:"url"` } // Has will check if a BRFC ID (or alternate) is found in the list of capabilities @@ -139,5 +151,54 @@ func (c *Client) GetCapabilities(target string, port int) (response *Capabilitie err = fmt.Errorf("missing %s version", DefaultServiceName) } + // Parse PIKE capability + if pike, ok := response.Capabilities["935478af7bf2"].(map[string]interface{}); ok { + pikeCap := &PikeCapability{ + Invite: pike["invite"].(string), + Outputs: pike["outputs"].(string), + } + response.Pike = pikeCap + } + return } + +// ExtractPikeOutputsURL extracts the outputs URL from the PIKE capability +func (c *CapabilitiesPayload) ExtractPikeOutputsURL() string { + if c.Pike != nil { + return c.Pike.Outputs + } + return "" +} + +// GetOutputsTemplate calls the PIKE capability outputs subcapability +func (c *Client) GetOutputsTemplate(pikeURL string) (response *PikeOutputs, err error) { + var resp StandardResponse + if resp, err = c.getRequest(pikeURL); err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bad response from PIKE outputs: code %d", resp.StatusCode) + } + + outputs := &PikeOutputs{} + if err = json.Unmarshal(resp.Body, outputs); err != nil { + return nil, err + } + + return outputs, nil +} + +// ExtractPikeInviteURL extracts the invite URL from the PIKE capability +func (c *CapabilitiesPayload) ExtractPikeInviteURL() string { + if c.Pike != nil { + return c.Pike.Invite + } + return "" +} + +// AddInviteRequest sends a contact request using the invite URL from capabilities +func (c *Client) AddInviteRequest(inviteURL, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) { + return c.AddContactRequest(inviteURL, alias, domain, request) +} diff --git a/capabilities_test.go b/capabilities_test.go index 62b281d..f1aab2f 100644 --- a/capabilities_test.go +++ b/capabilities_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/jarcoal/httpmock" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -22,9 +21,9 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) - assert.Equal(t, http.StatusOK, response.StatusCode) - assert.Equal(t, true, response.Has(BRFCPki, "")) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) }) t.Run("successful testnet response", func(t *testing.T) { @@ -35,9 +34,9 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) - assert.Equal(t, http.StatusOK, response.StatusCode) - assert.Equal(t, true, response.Has(BRFCPki, "")) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) }) t.Run("successful stn response", func(t *testing.T) { @@ -48,9 +47,9 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) - assert.Equal(t, http.StatusOK, response.StatusCode) - assert.Equal(t, true, response.Has(BRFCPki, "")) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) }) t.Run("status not modified", func(t *testing.T) { @@ -60,10 +59,10 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.NoError(t, err) - assert.NotNil(t, response) - assert.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) - assert.Equal(t, http.StatusNotModified, response.StatusCode) - assert.Equal(t, true, response.Has(BRFCPki, "")) + require.NotNil(t, response) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusNotModified, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) }) t.Run("bad request", func(t *testing.T) { @@ -79,9 +78,9 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.Error(t, err) - assert.NotNil(t, response) - assert.Equal(t, http.StatusBadRequest, response.StatusCode) - assert.Equal(t, 0, len(response.Capabilities)) + require.NotNil(t, response) + require.Equal(t, http.StatusBadRequest, response.StatusCode) + require.Equal(t, 0, len(response.Capabilities)) }) t.Run("missing target", func(t *testing.T) { @@ -97,7 +96,7 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities("", DefaultPort) require.Error(t, err) - assert.Nil(t, response) + require.Nil(t, response) }) t.Run("missing port", func(t *testing.T) { @@ -113,7 +112,7 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, 0) require.Error(t, err) - assert.Nil(t, response) + require.Nil(t, response) }) t.Run("http error", func(t *testing.T) { @@ -126,7 +125,7 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.Error(t, err) - assert.Nil(t, response) + require.Nil(t, response) }) t.Run("bad error in request", func(t *testing.T) { @@ -143,8 +142,8 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.Error(t, err) require.NotNil(t, response) - assert.Equal(t, http.StatusBadRequest, response.StatusCode) - assert.Equal(t, 0, len(response.Capabilities)) + require.Equal(t, http.StatusBadRequest, response.StatusCode) + require.Equal(t, 0, len(response.Capabilities)) }) t.Run("invalid quotes - good response", func(t *testing.T) { @@ -162,9 +161,9 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.NoError(t, err) require.NotNil(t, response) - assert.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) - assert.Equal(t, http.StatusOK, response.StatusCode) - assert.Equal(t, true, response.Has(BRFCPki, "")) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) }) t.Run("invalid alias", func(t *testing.T) { @@ -182,8 +181,8 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.Error(t, err) require.NotNil(t, response) - assert.NotEqual(t, DefaultBsvAliasVersion, response.BsvAlias) - assert.Equal(t, http.StatusNotModified, response.StatusCode) + require.NotEqual(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusNotModified, response.StatusCode) }) t.Run("invalid json", func(t *testing.T) { @@ -201,8 +200,43 @@ func TestClient_GetCapabilities(t *testing.T) { response, err := client.GetCapabilities(testDomain, DefaultPort) require.Error(t, err) require.NotNil(t, response) - assert.Equal(t, http.StatusNotModified, response.StatusCode) - assert.Equal(t, 0, len(response.Capabilities)) + require.Equal(t, http.StatusNotModified, response.StatusCode) + require.Equal(t, 0, len(response.Capabilities)) + }) + + t.Run("successful response with PIKE capability", func(t *testing.T) { + client := newTestClient(t) + + mockCapabilitiesWithPIKE(http.StatusOK) + + response, err := client.GetCapabilities(testDomain, DefaultPort) + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) + + // Check PIKE capability + require.NotNil(t, response.Pike) + require.Equal(t, "https://examples.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}", response.Pike.Outputs) + }) + + t.Run("successful response with PIKE invite capability", func(t *testing.T) { + client := newTestClient(t) + + mockCapabilitiesWithPIKE(http.StatusOK) + + response, err := client.GetCapabilities(testDomain, DefaultPort) + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, DefaultBsvAliasVersion, response.BsvAlias) + require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, true, response.Has(BRFCPki, "")) + + // Check PIKE invite capability + require.NotNil(t, response.Pike) + require.Equal(t, "https://examples.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}", response.Pike.Outputs) + require.Equal(t, "https://examples.com/v1/bsvalias/contact/invite/{alias}@{domain.tld}", response.Pike.Invite) }) } @@ -224,6 +258,28 @@ func mockCapabilitiesNetwork(statusCode int, n Network) { ) } +// mockCapabilitiesWithPIKE is used for mocking the response including the PIKE capability +func mockCapabilitiesWithPIKE(statusCode int) { + httpmock.Reset() + httpmock.RegisterResponder(http.MethodGet, "https://"+testDomain+":443/.well-known/bsvalias", + httpmock.NewStringResponder( + statusCode, + `{ + "bsvalias": "1.0", + "capabilities": { + "6745385c3fc0": false, + "pki": "https://examples.com/{alias}@{domain.tld}/id", + "paymentDestination": "https://examples.com/{alias}@{domain.tld}/payment-destination", + "935478af7bf2": { + "invite": "https://examples.com/v1/bsvalias/contact/invite/{alias}@{domain.tld}", + "outputs": "https://examples.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + } + } + }`, + ), + ) +} + // ExampleClient_GetCapabilities example using GetCapabilities() // // See more examples in /examples/ @@ -578,3 +634,186 @@ func BenchmarkCapabilities_GetString(b *testing.B) { _ = capabilities.GetString("pki", "0c4339ef99c2") } } + +func TestClient_GetOutputsTemplate(t *testing.T) { + client := newTestClient(t) + + t.Run("successful PIKE outputs response", func(t *testing.T) { + mockPIKEOutputs(http.StatusOK) + + outputsURL := "https://" + testDomain + "/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + response, err := client.GetOutputsTemplate(outputsURL) + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, "https://example.com/outputs", response.URL) + }) + + t.Run("PIKE outputs response error", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder(http.MethodGet, "https://exmaple.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}", + httpmock.NewStringResponder(http.StatusBadRequest, `{"message": "bad request"}`), + ) + + outputsURL := "https://domain.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + response, err := client.GetOutputsTemplate(outputsURL) + require.Error(t, err) + require.Nil(t, response) + }) +} + +// mockPIKEOutputs is used for mocking the PIKE outputs response +func mockPIKEOutputs(statusCode int) { + httpmock.Reset() + httpmock.RegisterResponder(http.MethodGet, "https://"+testDomain+"/v1/bsvalias/pike/outputs/%7Balias%7D@%7Bdomain.tld%7D", + httpmock.NewStringResponder( + statusCode, + `{ + "url": "https://example.com/outputs" + }`, + ), + ) +} + +// BenchmarkClient_GetOutputsTemplate benchmarks the method GetOutputsTemplate() +func BenchmarkClient_GetOutputsTemplate(b *testing.B) { + client := newTestClient(nil) + mockPIKEOutputs(http.StatusOK) + outputsURL := "https://domain.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + for i := 0; i < b.N; i++ { + _, _ = client.GetOutputsTemplate(outputsURL) + } +} + +// ExampleCapabilitiesPayload_ExtractPikeOutputsURL example using ExtractPikeOutputsURL() +// +// See more examples in /examples/ +func ExampleCapabilitiesPayload_ExtractPikeOutputsURL() { + capabilities := &CapabilitiesPayload{ + BsvAlias: DefaultServiceName, + Capabilities: map[string]interface{}{ + "6745385c3fc0": false, + "pki": "https://domain.com/" + DefaultServiceName + "/id/{alias}@{domain.tld}", + "935478af7bf2": map[string]interface{}{ + "invite": "https://domain.com/" + DefaultServiceName + "/contact/invite/{alias}@{domain.tld}", + "outputs": "https://domain.com/" + DefaultServiceName + "/pike/outputs/{alias}@{domain.tld}", + }, + }, + Pike: &PikeCapability{ + Invite: "https://domain.com/" + DefaultServiceName + "/contact/invite/{alias}@{domain.tld}", + Outputs: "https://domain.com/" + DefaultServiceName + "/pike/outputs/{alias}@{domain.tld}", + }, + } + + outputsURL := capabilities.ExtractPikeOutputsURL() + fmt.Printf("found PIKE Outputs URL: %v", outputsURL) + // Output:found PIKE Outputs URL: https://domain.com/bsvalias/pike/outputs/{alias}@{domain.tld} +} + +// ExampleClient_GetOutputsTemplate example using GetOutputsTemplate() +// +// See more examples in /examples/ +func ExampleClient_GetOutputsTemplate() { + // Setup a mock HTTP client + client := newTestClient(nil) + mockPIKEOutputs(http.StatusOK) + + // Assume we have a PIKE Outputs URL + pikeOutputsURL := "https://" + testDomain + "/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + + // Get the outputs template from PIKE + outputs, err := client.GetOutputsTemplate(pikeOutputsURL) + if err != nil { + fmt.Printf("error getting outputs template: %s", err.Error()) + return + } + fmt.Printf("found outputs template: %+v", outputs) + // Output:found outputs template: &{URL:https://example.com/outputs} +} + +// TestClient_AddInviteRequest will test the method AddInviteRequest() +func TestClient_AddInviteRequest(t *testing.T) { + client := newTestClient(t) + + t.Run("successful invite request", func(t *testing.T) { + mockInviteRequest(http.StatusOK) + + inviteURL := "https://" + testDomain + "/v1/bsvalias/contact/invite/{alias}@{domain.tld}" + request := &PikeContactRequestPayload{ + FullName: "John Doe", + Paymail: "johndoe@example.com", + } + response, err := client.AddInviteRequest(inviteURL, "alias", "domain.tld", request) + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.StatusCode) + }) + + t.Run("invite request error", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder(http.MethodPost, "https://example.com/v1/bsvalias/contact/invite/%7Balias%7D@%7Bdomain.tld%7D", + httpmock.NewStringResponder(http.StatusBadRequest, `{"message": "bad request"}`), + ) + + inviteURL := "https://example.com/v1/bsvalias/contact/invite/{alias}@{domain.tld}" + request := &PikeContactRequestPayload{ + FullName: "John Doe", + Paymail: "johndoe@example.com", + } + response, err := client.AddInviteRequest(inviteURL, "alias", "domain.tld", request) + require.Error(t, err) + require.Nil(t, response) + }) +} + +// mockInviteRequest is used for mocking the invite request response +func mockInviteRequest(statusCode int) { + httpmock.Reset() + httpmock.RegisterResponder(http.MethodPost, "https://"+testDomain+"/v1/bsvalias/contact/invite/alias@domain.tld", + httpmock.NewStringResponder( + statusCode, + `{ + "statusCode": 200, + "message": "Invite request sent successfully" + }`, + ), + ) +} + +// BenchmarkClient_AddInviteRequest benchmarks the method AddInviteRequest() +func BenchmarkClient_AddInviteRequest(b *testing.B) { + client := newTestClient(nil) + mockInviteRequest(http.StatusOK) + inviteURL := "https://" + testDomain + "/v1/bsvalias/contact/invite/{alias}@{domain.tld}" + request := &PikeContactRequestPayload{ + FullName: "John Doe", + Paymail: "johndoe@example.com", + } + for i := 0; i < b.N; i++ { + _, _ = client.AddInviteRequest(inviteURL, "alias", "domain.tld", request) + } +} + +// ExampleCapabilitiesPayload_ExtractPikeInviteURL example using ExtractPikeInviteURL() +// +// See more examples in /examples/ +func ExampleCapabilitiesPayload_ExtractPikeInviteURL() { + capabilities := &CapabilitiesPayload{ + BsvAlias: DefaultServiceName, + Capabilities: map[string]interface{}{ + "6745385c3fc0": false, + "pki": "https://" + testDomain + "/" + DefaultServiceName + "/id/{alias}@{domain.tld}", + "935478af7bf2": map[string]interface{}{ + "invite": "https://" + testDomain + "/" + DefaultServiceName + "/contact/invite/{alias}@{domain.tld}", + "outputs": "https://" + testDomain + "m/" + DefaultServiceName + "/pike/outputs/{alias}@{domain.tld}", + }, + }, + Pike: &PikeCapability{ + Invite: "https://" + testDomain + "/" + DefaultServiceName + "/contact/invite/{alias}@{domain.tld}", + Outputs: "https://" + testDomain + "/" + DefaultServiceName + "/pike/outputs/{alias}@{domain.tld}", + }, + } + + inviteURL := capabilities.ExtractPikeInviteURL() + fmt.Printf("found PIKE Invite URL: %v", inviteURL) + // Output: found PIKE Invite URL: https://test.com/bsvalias/contact/invite/{alias}@{domain.tld} +} diff --git a/examples/client/get_invite_url_from_pike_template/main.go b/examples/client/get_invite_url_from_pike_template/main.go new file mode 100644 index 0000000..ac3b052 --- /dev/null +++ b/examples/client/get_invite_url_from_pike_template/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "log" + + "github.com/bitcoin-sv/go-paymail" +) + +func main() { + // Load the client + client, err := paymail.NewClient() + if err != nil { + log.Fatalf("error loading client: %s", err.Error()) + } + + // Get the capabilities + var capabilities *paymail.CapabilitiesResponse + if capabilities, err = client.GetCapabilities("auggie.4chain.space", paymail.DefaultPort); err != nil { + log.Fatalf("error getting capabilities: %s", err.Error()) + } + log.Printf("found capabilities: %d", len(capabilities.Capabilities)) + + // Extract the PIKE Invite URL from the capabilities response + pikeInviteURL := capabilities.ExtractPikeInviteURL() + if pikeInviteURL == "" { + log.Fatalf("PIKE invite capability not found") + } + log.Printf("found PIKE Invite URL: %s", pikeInviteURL) + + // Prepare the contact request payload + request := &paymail.PikeContactRequestPayload{ + FullName: "John Doe", + Paymail: "johndoe@example.com", + } + + // Send the contact request using the invite URL + var response *paymail.PikeContactRequestResponse + if response, err = client.AddInviteRequest(pikeInviteURL, "alias", "domain.tld", request); err != nil { + log.Fatalf("error sending invite request: %s", err.Error()) + } + log.Printf("invite request response: %v", response) +} diff --git a/examples/client/get_outputs_template_for_pike/main.go b/examples/client/get_outputs_template_for_pike/main.go new file mode 100644 index 0000000..095bef3 --- /dev/null +++ b/examples/client/get_outputs_template_for_pike/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "log" + + "github.com/bitcoin-sv/go-paymail" +) + +func main() { + // Load the client + client, err := paymail.NewClient() + if err != nil { + log.Fatalf("error loading client: %s", err.Error()) + } + + // Get the capabilities + var capabilities *paymail.CapabilitiesResponse + if capabilities, err = client.GetCapabilities("auggie.4chain.space", paymail.DefaultPort); err != nil { + log.Fatalf("error getting capabilities: %s", err.Error()) + } + log.Printf("found capabilities: %d", len(capabilities.Capabilities)) + + // Extract the PIKE Outputs URL from the capabilities response + pikeOutputsURL := capabilities.ExtractPikeOutputsURL() + if pikeOutputsURL == "" { + log.Fatalf("PIKE outputs capability not found") + } + log.Printf("found PIKE Outputs URL: %s", pikeOutputsURL) + + // Get the outputs template from PIKE + var outputs *paymail.PikeOutputs + if outputs, err = client.GetOutputsTemplate(pikeOutputsURL); err != nil { + log.Fatalf("error getting outputs template: %s", err.Error()) + } + log.Printf("found outputs template: %v", outputs) +} diff --git a/interface.go b/interface.go index 12ce84b..624bc81 100644 --- a/interface.go +++ b/interface.go @@ -4,8 +4,9 @@ import ( "context" "net" - "github.com/bitcoin-sv/go-paymail/interfaces" "github.com/go-resty/resty/v2" + + "github.com/bitcoin-sv/go-paymail/interfaces" ) // ClientInterface is the Paymail client interface @@ -28,4 +29,6 @@ type ClientInterface interface { WithCustomHTTPClient(client *resty.Client) ClientInterface WithCustomResolver(resolver interfaces.DNSResolver) ClientInterface AddContactRequest(url, alias, domain string, request *PikeContactRequestPayload) (response *PikeContactRequestResponse, err error) + AddInviteRequest(inviteURL, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) + GetOutputsTemplate(pikeURL string) (response *PikeOutputs, err error) } From 517a6c3bbc48a6c342450ef137b5279f8242496c Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Wed, 29 May 2024 11:52:09 +0200 Subject: [PATCH 2/5] correction(SPV-813): correcting examples --- examples/client/get_invite_url_from_pike_template/main.go | 2 +- examples/client/get_outputs_template_for_pike/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/client/get_invite_url_from_pike_template/main.go b/examples/client/get_invite_url_from_pike_template/main.go index ac3b052..dbc0e74 100644 --- a/examples/client/get_invite_url_from_pike_template/main.go +++ b/examples/client/get_invite_url_from_pike_template/main.go @@ -15,7 +15,7 @@ func main() { // Get the capabilities var capabilities *paymail.CapabilitiesResponse - if capabilities, err = client.GetCapabilities("auggie.4chain.space", paymail.DefaultPort); err != nil { + if capabilities, err = client.GetCapabilities("example.com", paymail.DefaultPort); err != nil { log.Fatalf("error getting capabilities: %s", err.Error()) } log.Printf("found capabilities: %d", len(capabilities.Capabilities)) diff --git a/examples/client/get_outputs_template_for_pike/main.go b/examples/client/get_outputs_template_for_pike/main.go index 095bef3..dc12845 100644 --- a/examples/client/get_outputs_template_for_pike/main.go +++ b/examples/client/get_outputs_template_for_pike/main.go @@ -15,7 +15,7 @@ func main() { // Get the capabilities var capabilities *paymail.CapabilitiesResponse - if capabilities, err = client.GetCapabilities("auggie.4chain.space", paymail.DefaultPort); err != nil { + if capabilities, err = client.GetCapabilities("example.com", paymail.DefaultPort); err != nil { log.Fatalf("error getting capabilities: %s", err.Error()) } log.Printf("found capabilities: %d", len(capabilities.Capabilities)) From d9ab2e24f9256292d2fa7e38271297606d2cbd29 Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Wed, 29 May 2024 12:48:06 +0200 Subject: [PATCH 3/5] review(SPV-813): addressed comments in reviw to check value in map --- capabilities.go | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/capabilities.go b/capabilities.go index 9458e16..7c70dab 100644 --- a/capabilities.go +++ b/capabilities.go @@ -149,15 +149,12 @@ func (c *Client) GetCapabilities(target string, port int) (response *Capabilitie // Invalid version detected if len(response.BsvAlias) == 0 { err = fmt.Errorf("missing %s version", DefaultServiceName) + return } // Parse PIKE capability - if pike, ok := response.Capabilities["935478af7bf2"].(map[string]interface{}); ok { - pikeCap := &PikeCapability{ - Invite: pike["invite"].(string), - Outputs: pike["outputs"].(string), - } - response.Pike = pikeCap + if err = parsePikeCapability(response); err != nil { + return } return @@ -202,3 +199,35 @@ func (c *CapabilitiesPayload) ExtractPikeInviteURL() string { func (c *Client) AddInviteRequest(inviteURL, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) { return c.AddContactRequest(inviteURL, alias, domain, request) } + +// parsePikeCapability parses the PIKE capability from the capabilities response +func parsePikeCapability(response *CapabilitiesResponse) error { + if pike, ok := response.Capabilities[BRFCPike].(map[string]interface{}); ok { + var ( + invite, outputs string + errMsgs []string + ) + + if inviteStr, ok := pike["invite"].(string); ok { + invite = inviteStr + } else { + errMsgs = append(errMsgs, "missing invite URL in PIKE capability") + } + + if outputsStr, ok := pike["outputs"].(string); ok { + outputs = outputsStr + } else { + errMsgs = append(errMsgs, "missing outputs URL in PIKE capability") + } + + if len(errMsgs) > 0 { + return fmt.Errorf(strings.Join(errMsgs, "; ")) + } + + response.Pike = &PikeCapability{ + Invite: invite, + Outputs: outputs, + } + } + return nil +} From 6bb27e3db719b6e614141df6bd64c245b115e46f Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Wed, 29 May 2024 13:43:13 +0200 Subject: [PATCH 4/5] refactored(SPV-813): per review comments client methods been moved to pike.go and used post --- capabilities.go | 24 ---- capabilities_test.go | 70 ----------- .../get_outputs_template_for_pike/main.go | 10 +- interface.go | 2 +- pike.go | 51 ++++++++ pike_test.go | 110 ++++++++++++++++++ 6 files changed, 171 insertions(+), 96 deletions(-) create mode 100644 pike_test.go diff --git a/capabilities.go b/capabilities.go index 7c70dab..f6e5bf0 100644 --- a/capabilities.go +++ b/capabilities.go @@ -168,25 +168,6 @@ func (c *CapabilitiesPayload) ExtractPikeOutputsURL() string { return "" } -// GetOutputsTemplate calls the PIKE capability outputs subcapability -func (c *Client) GetOutputsTemplate(pikeURL string) (response *PikeOutputs, err error) { - var resp StandardResponse - if resp, err = c.getRequest(pikeURL); err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("bad response from PIKE outputs: code %d", resp.StatusCode) - } - - outputs := &PikeOutputs{} - if err = json.Unmarshal(resp.Body, outputs); err != nil { - return nil, err - } - - return outputs, nil -} - // ExtractPikeInviteURL extracts the invite URL from the PIKE capability func (c *CapabilitiesPayload) ExtractPikeInviteURL() string { if c.Pike != nil { @@ -195,11 +176,6 @@ func (c *CapabilitiesPayload) ExtractPikeInviteURL() string { return "" } -// AddInviteRequest sends a contact request using the invite URL from capabilities -func (c *Client) AddInviteRequest(inviteURL, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) { - return c.AddContactRequest(inviteURL, alias, domain, request) -} - // parsePikeCapability parses the PIKE capability from the capabilities response func parsePikeCapability(response *CapabilitiesResponse) error { if pike, ok := response.Capabilities[BRFCPike].(map[string]interface{}); ok { diff --git a/capabilities_test.go b/capabilities_test.go index f1aab2f..4820285 100644 --- a/capabilities_test.go +++ b/capabilities_test.go @@ -635,55 +635,6 @@ func BenchmarkCapabilities_GetString(b *testing.B) { } } -func TestClient_GetOutputsTemplate(t *testing.T) { - client := newTestClient(t) - - t.Run("successful PIKE outputs response", func(t *testing.T) { - mockPIKEOutputs(http.StatusOK) - - outputsURL := "https://" + testDomain + "/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" - response, err := client.GetOutputsTemplate(outputsURL) - require.NoError(t, err) - require.NotNil(t, response) - require.Equal(t, "https://example.com/outputs", response.URL) - }) - - t.Run("PIKE outputs response error", func(t *testing.T) { - httpmock.Reset() - httpmock.RegisterResponder(http.MethodGet, "https://exmaple.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}", - httpmock.NewStringResponder(http.StatusBadRequest, `{"message": "bad request"}`), - ) - - outputsURL := "https://domain.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" - response, err := client.GetOutputsTemplate(outputsURL) - require.Error(t, err) - require.Nil(t, response) - }) -} - -// mockPIKEOutputs is used for mocking the PIKE outputs response -func mockPIKEOutputs(statusCode int) { - httpmock.Reset() - httpmock.RegisterResponder(http.MethodGet, "https://"+testDomain+"/v1/bsvalias/pike/outputs/%7Balias%7D@%7Bdomain.tld%7D", - httpmock.NewStringResponder( - statusCode, - `{ - "url": "https://example.com/outputs" - }`, - ), - ) -} - -// BenchmarkClient_GetOutputsTemplate benchmarks the method GetOutputsTemplate() -func BenchmarkClient_GetOutputsTemplate(b *testing.B) { - client := newTestClient(nil) - mockPIKEOutputs(http.StatusOK) - outputsURL := "https://domain.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" - for i := 0; i < b.N; i++ { - _, _ = client.GetOutputsTemplate(outputsURL) - } -} - // ExampleCapabilitiesPayload_ExtractPikeOutputsURL example using ExtractPikeOutputsURL() // // See more examples in /examples/ @@ -709,27 +660,6 @@ func ExampleCapabilitiesPayload_ExtractPikeOutputsURL() { // Output:found PIKE Outputs URL: https://domain.com/bsvalias/pike/outputs/{alias}@{domain.tld} } -// ExampleClient_GetOutputsTemplate example using GetOutputsTemplate() -// -// See more examples in /examples/ -func ExampleClient_GetOutputsTemplate() { - // Setup a mock HTTP client - client := newTestClient(nil) - mockPIKEOutputs(http.StatusOK) - - // Assume we have a PIKE Outputs URL - pikeOutputsURL := "https://" + testDomain + "/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" - - // Get the outputs template from PIKE - outputs, err := client.GetOutputsTemplate(pikeOutputsURL) - if err != nil { - fmt.Printf("error getting outputs template: %s", err.Error()) - return - } - fmt.Printf("found outputs template: %+v", outputs) - // Output:found outputs template: &{URL:https://example.com/outputs} -} - // TestClient_AddInviteRequest will test the method AddInviteRequest() func TestClient_AddInviteRequest(t *testing.T) { client := newTestClient(t) diff --git a/examples/client/get_outputs_template_for_pike/main.go b/examples/client/get_outputs_template_for_pike/main.go index dc12845..d9018d2 100644 --- a/examples/client/get_outputs_template_for_pike/main.go +++ b/examples/client/get_outputs_template_for_pike/main.go @@ -27,9 +27,17 @@ func main() { } log.Printf("found PIKE Outputs URL: %s", pikeOutputsURL) + // Prepare the payload + alias := "examplealias" + domain := "example.com" + payload := &paymail.PikePaymentOutputsPayload{ + SenderPaymail: "joedoe@example.com", + Amount: 1000, // Example amount in satoshis + } + // Get the outputs template from PIKE var outputs *paymail.PikeOutputs - if outputs, err = client.GetOutputsTemplate(pikeOutputsURL); err != nil { + if outputs, err = client.GetOutputsTemplate(pikeOutputsURL, alias, domain, payload); err != nil { log.Fatalf("error getting outputs template: %s", err.Error()) } log.Printf("found outputs template: %v", outputs) diff --git a/interface.go b/interface.go index 624bc81..cc27897 100644 --- a/interface.go +++ b/interface.go @@ -30,5 +30,5 @@ type ClientInterface interface { WithCustomResolver(resolver interfaces.DNSResolver) ClientInterface AddContactRequest(url, alias, domain string, request *PikeContactRequestPayload) (response *PikeContactRequestResponse, err error) AddInviteRequest(inviteURL, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) - GetOutputsTemplate(pikeURL string) (response *PikeOutputs, err error) + GetOutputsTemplate(pikeURL, alias, domain string, payload *PikePaymentOutputsPayload) (response *PikeOutputs, err error) } diff --git a/pike.go b/pike.go index 7783b6c..8b78ca7 100644 --- a/pike.go +++ b/pike.go @@ -101,3 +101,54 @@ func (r *PikeContactRequestPayload) validate() error { return ValidatePaymail(r.Paymail) } + +// GetOutputsTemplate calls the PIKE capability outputs subcapability +func (c *Client) GetOutputsTemplate(pikeURL, alias, domain string, payload *PikePaymentOutputsPayload) (response *PikeOutputs, err error) { + // Require a valid URL + if len(pikeURL) == 0 || !strings.Contains(pikeURL, "https://") { + err = fmt.Errorf("invalid url: %s", pikeURL) + return + } + + // Basic requirements for request + if payload == nil { + err = errors.New("payload cannot be nil") + return + } else if payload.Amount == 0 { + err = errors.New("amount is required") + return + } else if len(alias) == 0 { + err = errors.New("missing alias") + return + } else if len(domain) == 0 { + err = errors.New("missing domain") + return + } + + // Set the base URL and path, assuming the URL is from the prior GetCapabilities() request + reqURL := replaceAliasDomain(pikeURL, alias, domain) + + // Fire the POST request + var resp StandardResponse + if resp, err = c.postRequest(reqURL, payload); err != nil { + return + } + + // Test the status code + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bad response from PIKE outputs: code %d", resp.StatusCode) + } + + // Decode the body of the response + outputs := &PikeOutputs{} + if err = json.Unmarshal(resp.Body, outputs); err != nil { + return nil, err + } + + return outputs, nil +} + +// AddInviteRequest sends a contact request using the invite URL from capabilities +func (c *Client) AddInviteRequest(inviteURL, alias, domain string, request *PikeContactRequestPayload) (*PikeContactRequestResponse, error) { + return c.AddContactRequest(inviteURL, alias, domain, request) +} diff --git a/pike_test.go b/pike_test.go new file mode 100644 index 0000000..a7c022d --- /dev/null +++ b/pike_test.go @@ -0,0 +1,110 @@ +package paymail + +import ( + "fmt" + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" +) + +// ExampleClient_GetOutputsTemplate example using GetOutputsTemplate() +// +// See more examples in /examples/ +func ExampleClient_GetOutputsTemplate() { + // Activate httpmock + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + // Setup a mock HTTP client + client := newTestClient(nil) + mockPIKEOutputs(http.StatusOK) + + // Assume we have a PIKE Outputs URL + pikeOutputsURL := "https://test.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + + // Prepare the payload + payload := &PikePaymentOutputsPayload{ + SenderPaymail: "joedoe@example.com", + Amount: 1000, // Example amount in satoshis + } + + // Get the outputs template from PIKE + outputs, err := client.GetOutputsTemplate(pikeOutputsURL, "alias", "domain.tld", payload) + if err != nil { + fmt.Printf("error getting outputs template: %s", err.Error()) + return + } + fmt.Printf("found outputs template: %+v", outputs) + // Output: found outputs template: &{URL:https://example.com/outputs} +} + +// TestClient_GetOutputsTemplate will test the method GetOutputsTemplate() +func TestClient_GetOutputsTemplate(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + client := newTestClient(t) + + t.Run("successful PIKE outputs response", func(t *testing.T) { + mockPIKEOutputs(http.StatusOK) + + outputsURL := "https://" + testDomain + "/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + payload := &PikePaymentOutputsPayload{ + SenderPaymail: "joedoe@example.com", + Amount: 1000, + } + response, err := client.GetOutputsTemplate(outputsURL, "alias", "domain.tld", payload) + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, "https://example.com/outputs", response.URL) + }) + + t.Run("PIKE outputs response error", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder(http.MethodPost, "https://example.com/v1/bsvalias/pike/outputs/alias@domain.tld", + httpmock.NewStringResponder(http.StatusBadRequest, `{"message": "bad request"}`), + ) + + outputsURL := "https://example.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + payload := &PikePaymentOutputsPayload{ + SenderPaymail: "joedoe@example.com", + Amount: 1000, + } + response, err := client.GetOutputsTemplate(outputsURL, "alias", "domain.tld", payload) + require.Error(t, err) + require.Nil(t, response) + }) +} + +// mockPIKEOutputs is used for mocking the PIKE outputs response +func mockPIKEOutputs(statusCode int) { + httpmock.RegisterResponder(http.MethodPost, "https://"+testDomain+"/v1/bsvalias/pike/outputs/alias@domain.tld", + httpmock.NewStringResponder( + statusCode, + `{ + "url": "https://example.com/outputs" + }`, + ), + ) +} + +// BenchmarkClient_GetOutputsTemplate benchmarks the method GetOutputsTemplate() +func BenchmarkClient_GetOutputsTemplate(b *testing.B) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + client := newTestClient(nil) + mockPIKEOutputs(http.StatusOK) + + outputsURL := "https://example.com/v1/bsvalias/pike/outputs/{alias}@{domain.tld}" + payload := &PikePaymentOutputsPayload{ + SenderPaymail: "joedoe@example.com", + Amount: 1000, // Example amount in satoshis + } + + for i := 0; i < b.N; i++ { + _, _ = client.GetOutputsTemplate(outputsURL, "alias", "domain.tld", payload) + } +} From ac314a30badb65f057e7961defccaf4239bca58e Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Mon, 3 Jun 2024 09:35:19 +0200 Subject: [PATCH 5/5] refactored(SPV-813): removing check on invite --- capabilities.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/capabilities.go b/capabilities.go index f6e5bf0..0f8166f 100644 --- a/capabilities.go +++ b/capabilities.go @@ -186,8 +186,6 @@ func parsePikeCapability(response *CapabilitiesResponse) error { if inviteStr, ok := pike["invite"].(string); ok { invite = inviteStr - } else { - errMsgs = append(errMsgs, "missing invite URL in PIKE capability") } if outputsStr, ok := pike["outputs"].(string); ok {