From 6c48d68a07db3239cec71331a75317e5d21ccc1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:53:57 +0000 Subject: [PATCH 01/11] build(deps): bump github.com/bitcoin-sv/spv-wallet/models Bumps [github.com/bitcoin-sv/spv-wallet/models](https://github.com/bitcoin-sv/spv-wallet) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/bitcoin-sv/spv-wallet/releases) - [Commits](https://github.com/bitcoin-sv/spv-wallet/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: github.com/bitcoin-sv/spv-wallet/models dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b58a63..f929d6e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bitcoin-sv/spv-wallet-go-client go 1.21 require ( - github.com/bitcoin-sv/spv-wallet/models v0.24.0 + github.com/bitcoin-sv/spv-wallet/models v0.25.0 github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 github.com/libsv/go-bk v0.1.6 github.com/libsv/go-bt/v2 v2.2.5 diff --git a/go.sum b/go.sum index c87e53f..143f349 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bitcoin-sv/spv-wallet/models v0.24.0 h1:cIfi2noDfRWaFt0VMOECwF8H9OsJFdP0T03XK4BULe0= -github.com/bitcoin-sv/spv-wallet/models v0.24.0/go.mod h1:P8vXF1mPg1Zh3xSvB9yqwuPJfOR8Tt/SAG2FYztwENI= +github.com/bitcoin-sv/spv-wallet/models v0.25.0 h1:tdUxF8fTDp23i2rvDma4/gUeeZePa+WFEceQLUSZhe4= +github.com/bitcoin-sv/spv-wallet/models v0.25.0/go.mod h1:P8vXF1mPg1Zh3xSvB9yqwuPJfOR8Tt/SAG2FYztwENI= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fMuWYhNoZa1tck= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 h1:2yTIV9u7H0BhRDGXH5xrAwAz7XibWJtX2dNezMeNsUo= From d3bf225d222c500a42b0bbe675aa0f9cddf91ad4 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Mon, 18 Mar 2024 17:39:35 +0100 Subject: [PATCH 02/11] feat: adds accept and reject contact functions --- contacts.go | 17 ++++++++++++++ contacts_test.go | 52 +++++++++++++++++++++++++++++++++++++++++ transports/http.go | 22 +++++++++++++++++ transports/interface.go | 7 ++++++ 4 files changed, 98 insertions(+) create mode 100644 contacts.go create mode 100644 contacts_test.go diff --git a/contacts.go b/contacts.go new file mode 100644 index 0000000..c0944c6 --- /dev/null +++ b/contacts.go @@ -0,0 +1,17 @@ +package walletclient + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet-go-client/transports" +) + +// AcceptContact accepts a contact by paymail +func (b *WalletClient) AcceptContact(ctx context.Context, paymail string) transports.ResponseError { + return b.transport.AcceptContact(ctx, paymail) +} + +// RejectContact rejects a contact by paymail +func (b *WalletClient) RejectContact(ctx context.Context, paymail string) transports.ResponseError { + return b.transport.RejectContact(ctx, paymail) +} diff --git a/contacts_test.go b/contacts_test.go new file mode 100644 index 0000000..596d83a --- /dev/null +++ b/contacts_test.go @@ -0,0 +1,52 @@ +package walletclient + +import ( + "context" + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/stretchr/testify/assert" +) + +// TestAcceptContact will test the AcceptContact method +func TestAcceptContact(t *testing.T) { + transportHandler := testTransportHandler{ + Type: fixtures.RequestType, + Path: "/contact/accept/", + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + t.Run("AcceptContact", func(t *testing.T) { + // given + client := getTestWalletClient(transportHandler, true) + + // when + err := client.AcceptContact(context.Background(), fixtures.PaymailAddress) + + // then + assert.NoError(t, err) + }) +} + +// TestRejectContact will test the RejectContact method +func TestRejectContact(t *testing.T) { + transportHandler := testTransportHandler{ + Type: fixtures.RequestType, + Path: "/contact/reject/", + Result: fixtures.MarshallForTestHandler(""), + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + t.Run("RejectContact", func(t *testing.T) { + // given + client := getTestWalletClient(transportHandler, true) + + // when + err := client.RejectContact(context.Background(), fixtures.PaymailAddress) + + // then + assert.NoError(t, err) + }) +} diff --git a/transports/http.go b/transports/http.go index c827953..5b25c7d 100644 --- a/transports/http.go +++ b/transports/http.go @@ -639,3 +639,25 @@ func (h *TransportHTTP) authenticateWithXpriv(sign bool, req *http.Request, xPri func (h *TransportHTTP) authenticateWithAccessKey(req *http.Request, rawJSON []byte) ResponseError { return SetSignatureFromAccessKey(&req.Header, hex.EncodeToString(h.accessKey.Serialise()), string(rawJSON)) } + +// AcceptContact will accept a contact by paymail +func (h *TransportHTTP) AcceptContact(ctx context.Context, paymail string) ResponseError { + if err := h.doHTTPRequest( + ctx, http.MethodPatch, "/contact/accept/"+paymail, nil, h.xPriv, h.signRequest, nil, + ); err != nil { + return err + } + + return nil +} + +// RejectContact will accept a contact by paymail +func (h *TransportHTTP) RejectContact(ctx context.Context, paymail string) ResponseError { + if err := h.doHTTPRequest( + ctx, http.MethodPatch, "/contact/reject/"+paymail, nil, h.xPriv, h.signRequest, nil, + ); err != nil { + return err + } + + return nil +} diff --git a/transports/interface.go b/transports/interface.go index dcec8ee..f98664e 100644 --- a/transports/interface.go +++ b/transports/interface.go @@ -49,6 +49,12 @@ type TransactionService interface { GetUtxosCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) } +// ContactService is the contact related requests +type ContactService interface { + AcceptContact(ctx context.Context, paymail string) ResponseError + RejectContact(ctx context.Context, paymail string) ResponseError +} + // AdminService is the admin related requests type AdminService interface { AdminGetStatus(ctx context.Context) (bool, ResponseError) @@ -79,6 +85,7 @@ type AdminService interface { type TransportService interface { AccessKeyService AdminService + ContactService DestinationService TransactionService XpubService From bbe0b0fc39dd2d0a1d18244a17e9f2a1f608bab6 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Tue, 2 Apr 2024 14:10:21 +0200 Subject: [PATCH 03/11] feat(BUX-639): adjust accept/reject contact routings; add confirm/search contacts functions --- contacts.go | 15 +++++++- contacts_test.go | 83 +++++++++++++++++++++++------------------ transports/http.go | 42 ++++++++++++++++++--- transports/interface.go | 2 + 4 files changed, 98 insertions(+), 44 deletions(-) diff --git a/contacts.go b/contacts.go index c0944c6..02c392a 100644 --- a/contacts.go +++ b/contacts.go @@ -4,14 +4,25 @@ import ( "context" "github.com/bitcoin-sv/spv-wallet-go-client/transports" + "github.com/bitcoin-sv/spv-wallet/models" ) -// AcceptContact accepts a contact by paymail +// AcceptContact will accept the contact associated with the paymail func (b *WalletClient) AcceptContact(ctx context.Context, paymail string) transports.ResponseError { return b.transport.AcceptContact(ctx, paymail) } -// RejectContact rejects a contact by paymail +// RejectContact will reject the contact associated with the paymail func (b *WalletClient) RejectContact(ctx context.Context, paymail string) transports.ResponseError { return b.transport.RejectContact(ctx, paymail) } + +// ConfirmContact will confirm the contact associated with the paymail +func (b *WalletClient) ConfirmContact(ctx context.Context, paymail string) transports.ResponseError { + return b.transport.ConfirmContact(ctx, paymail) +} + +// GetContacts will get contacts by conditions +func (b *WalletClient) GetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *transports.QueryParams) ([]*models.Contact, transports.ResponseError) { + return b.transport.GetContacts(ctx, conditions, metadata, queryParams) +} diff --git a/contacts_test.go b/contacts_test.go index 596d83a..0f3740b 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -8,45 +8,54 @@ import ( "github.com/stretchr/testify/assert" ) -// TestAcceptContact will test the AcceptContact method -func TestAcceptContact(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/contact/accept/", - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - t.Run("AcceptContact", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - err := client.AcceptContact(context.Background(), fixtures.PaymailAddress) - - // then - assert.NoError(t, err) - }) -} - // TestRejectContact will test the RejectContact method -func TestRejectContact(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/contact/reject/", - Result: fixtures.MarshallForTestHandler(""), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, +func TestContactActionsRouting(t *testing.T) { + tcs := []struct { + name string + route string + f func(c *WalletClient) error + }{ + { + name: "RejectContact", + route: "/contact/rejected/", + f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) }, + }, + { + name: "AcceptContact", + route: "/contact/accepted/", + f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, + }, + { + name: "ConfirmContact", + route: "/contact/confirmed/", + f: func(c *WalletClient) error { return c.ConfirmContact(context.Background(), fixtures.PaymailAddress) }, + }, + { + name: "GetContacts", + route: "/contact/search/", + f: func(c *WalletClient) error { _, err := c.GetContacts(context.Background(), nil, nil, nil); return err }, + }, } - t.Run("RejectContact", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - err := client.RejectContact(context.Background(), fixtures.PaymailAddress) + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + // given + tmq := testTransportHandler{ + Type: fixtures.RequestType, + Path: tc.route, + Result: "[]", + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + client := getTestWalletClient(tmq, true) + + // when + err := tc.f(client) + + // then + assert.NoError(t, err) + }) + } - // then - assert.NoError(t, err) - }) } diff --git a/transports/http.go b/transports/http.go index 5b25c7d..04a52dd 100644 --- a/transports/http.go +++ b/transports/http.go @@ -329,7 +329,7 @@ func (h *TransportHTTP) GetTransaction(ctx context.Context, txID string) (*model return &transaction, nil } -// GetTransactions will get a transactions by conditions +// GetTransactions will get transactions by conditions func (h *TransportHTTP) GetTransactions(ctx context.Context, conditions map[string]interface{}, metadataConditions *models.Metadata, queryParams *QueryParams, ) ([]*models.Transaction, ResponseError) { @@ -640,10 +640,10 @@ func (h *TransportHTTP) authenticateWithAccessKey(req *http.Request, rawJSON []b return SetSignatureFromAccessKey(&req.Header, hex.EncodeToString(h.accessKey.Serialise()), string(rawJSON)) } -// AcceptContact will accept a contact by paymail +// AcceptContact will accept the contact associated with the paymail func (h *TransportHTTP) AcceptContact(ctx context.Context, paymail string) ResponseError { if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/contact/accept/"+paymail, nil, h.xPriv, h.signRequest, nil, + ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, h.xPriv, h.signRequest, nil, ); err != nil { return err } @@ -651,13 +651,45 @@ func (h *TransportHTTP) AcceptContact(ctx context.Context, paymail string) Respo return nil } -// RejectContact will accept a contact by paymail +// RejectContact will reject the contact associated with the paymail func (h *TransportHTTP) RejectContact(ctx context.Context, paymail string) ResponseError { if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/contact/reject/"+paymail, nil, h.xPriv, h.signRequest, nil, + ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, h.xPriv, h.signRequest, nil, ); err != nil { return err } return nil } + +// ConfirmContact will confirm the contact associated with the paymail +func (h *TransportHTTP) ConfirmContact(ctx context.Context, paymail string) ResponseError { + if err := h.doHTTPRequest( + ctx, http.MethodPatch, "/contact/confirmed/"+paymail, nil, h.xPriv, h.signRequest, nil, + ); err != nil { + return err + } + + return nil +} + +// GetContacts will get contacts by conditions +func (h *TransportHTTP) GetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Contact, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + FieldQueryParams: queryParams, + }) + if err != nil { + return nil, WrapError(err) + } + + var result []*models.Contact + if err := h.doHTTPRequest( + ctx, http.MethodPost, "/contact/search", jsonStr, h.xPriv, h.signRequest, &result, + ); err != nil { + return nil, err + } + + return result, nil +} diff --git a/transports/interface.go b/transports/interface.go index f98664e..73c83c2 100644 --- a/transports/interface.go +++ b/transports/interface.go @@ -53,6 +53,8 @@ type TransactionService interface { type ContactService interface { AcceptContact(ctx context.Context, paymail string) ResponseError RejectContact(ctx context.Context, paymail string) ResponseError + ConfirmContact(ctx context.Context, paymail string) ResponseError + GetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Contact, ResponseError) } // AdminService is the admin related requests From f43bb1d61e7cd83fcb75c3389ca236754929e8f5 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Tue, 2 Apr 2024 14:48:51 +0200 Subject: [PATCH 04/11] feat(BUX-639): add upsert contact --- contacts.go | 10 +++++++ contacts_test.go | 58 +++++++++++++++++++++++++++++------------ transports/http.go | 26 ++++++++++++++++++ transports/interface.go | 1 + 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/contacts.go b/contacts.go index 02c392a..0c11107 100644 --- a/contacts.go +++ b/contacts.go @@ -7,6 +7,16 @@ import ( "github.com/bitcoin-sv/spv-wallet/models" ) +// UpsertContact add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user in their contacts. +func (b *WalletClient) UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata) (*models.Contact, transports.ResponseError) { + return b.transport.UpsertContact(ctx, paymail, fullName, metadata, "") +} + +// UpsertContactForPaymail add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user specified paymail in their contacts. +func (b *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, transports.ResponseError) { + return b.transport.UpsertContact(ctx, paymail, fullName, metadata, requesterPaymail) +} + // AcceptContact will accept the contact associated with the paymail func (b *WalletClient) AcceptContact(ctx context.Context, paymail string) transports.ResponseError { return b.transport.AcceptContact(ctx, paymail) diff --git a/contacts_test.go b/contacts_test.go index 0f3740b..ab468bb 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -11,29 +11,55 @@ import ( // TestRejectContact will test the RejectContact method func TestContactActionsRouting(t *testing.T) { tcs := []struct { - name string - route string - f func(c *WalletClient) error + name string + route string + responsePayload string + f func(c *WalletClient) error }{ { - name: "RejectContact", - route: "/contact/rejected/", - f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) }, + name: "RejectContact", + route: "/contact/rejected/", + responsePayload: "{}", + f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) }, }, { - name: "AcceptContact", - route: "/contact/accepted/", - f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, + name: "AcceptContact", + route: "/contact/accepted/", + responsePayload: "{}", + f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, }, { - name: "ConfirmContact", - route: "/contact/confirmed/", - f: func(c *WalletClient) error { return c.ConfirmContact(context.Background(), fixtures.PaymailAddress) }, + name: "ConfirmContact", + route: "/contact/confirmed/", + responsePayload: "{}", + f: func(c *WalletClient) error { return c.ConfirmContact(context.Background(), fixtures.PaymailAddress) }, }, { - name: "GetContacts", - route: "/contact/search/", - f: func(c *WalletClient) error { _, err := c.GetContacts(context.Background(), nil, nil, nil); return err }, + name: "GetContacts", + route: "/contact/search/", + responsePayload: "[]", + f: func(c *WalletClient) error { + _, err := c.GetContacts(context.Background(), nil, nil, nil) + return err + }, + }, + { + name: "UpsertContact", + route: "/contact/", + responsePayload: "{}", + f: func(c *WalletClient) error { + _, err := c.UpsertContact(context.Background(), "", "", nil) + return err + }, + }, + { + name: "UpsertContactForPaymail", + route: "/contact/", + responsePayload: "{}", + f: func(c *WalletClient) error { + _, err := c.UpsertContactForPaymail(context.Background(), "", "", nil, "") + return err + }, }, } @@ -43,7 +69,7 @@ func TestContactActionsRouting(t *testing.T) { tmq := testTransportHandler{ Type: fixtures.RequestType, Path: tc.route, - Result: "[]", + Result: tc.responsePayload, ClientURL: fixtures.ServerURL, Client: WithHTTPClient, } diff --git a/transports/http.go b/transports/http.go index 04a52dd..b124a74 100644 --- a/transports/http.go +++ b/transports/http.go @@ -693,3 +693,29 @@ func (h *TransportHTTP) GetContacts(ctx context.Context, conditions map[string]i return result, nil } + +// UpsertContact add or update contact. When adding a new contact, the system utilizes Paymail's PIKE capability to dispatch an invitation request, asking the counterparty to include the current user in their contacts. +func (h *TransportHTTP) UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, ResponseError) { + payload := map[string]interface{}{ + "fullName": fullName, + FieldMetadata: processMetadata(metadata), + } + + if requesterPaymail != "" { + payload["requesterPaymail"] = requesterPaymail + } + + jsonStr, err := json.Marshal(payload) + if err != nil { + return nil, WrapError(err) + } + + var result models.Contact + if err := h.doHTTPRequest( + ctx, http.MethodPut, "/contact/"+paymail, jsonStr, h.xPriv, h.signRequest, &result, + ); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/transports/interface.go b/transports/interface.go index 73c83c2..3ae040b 100644 --- a/transports/interface.go +++ b/transports/interface.go @@ -55,6 +55,7 @@ type ContactService interface { RejectContact(ctx context.Context, paymail string) ResponseError ConfirmContact(ctx context.Context, paymail string) ResponseError GetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Contact, ResponseError) + UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, ResponseError) } // AdminService is the admin related requests From 78e3d522a6ec46eafd2210ad7cc39f4717a738dc Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Tue, 2 Apr 2024 13:05:26 +0200 Subject: [PATCH 05/11] feat(BUX-400): add totp creation and validation --- go.mod | 2 ++ go.sum | 7 ++++ totp.go | 70 ++++++++++++++++++++++++++++++++++++++ totp/totp.go | 57 +++++++++++++++++++++++++++++++ totp/totp_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100644 totp.go create mode 100644 totp/totp.go create mode 100644 totp/totp_test.go diff --git a/go.mod b/go.mod index f929d6e..508b13c 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,13 @@ require ( github.com/libsv/go-bk v0.1.6 github.com/libsv/go-bt/v2 v2.2.5 github.com/pkg/errors v0.9.1 + github.com/pquerna/otp v1.4.0 github.com/stretchr/testify v1.8.4 ) require ( github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.17.0 // indirect diff --git a/go.sum b/go.sum index 143f349..3d5c870 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,9 @@ github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fM github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 h1:2yTIV9u7H0BhRDGXH5xrAwAz7XibWJtX2dNezMeNsUo= github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173/go.mod h1:BZ1UcC9+tmcDEcdVXgpt13hMczwJxWzpAn68wNs7zRA= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -18,8 +21,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= diff --git a/totp.go b/totp.go new file mode 100644 index 0000000..7c034e1 --- /dev/null +++ b/totp.go @@ -0,0 +1,70 @@ +package walletclient + +import ( + "errors" + "fmt" + + "github.com/bitcoin-sv/spv-wallet-go-client/totp" + "github.com/bitcoin-sv/spv-wallet-go-client/utils" + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bip32" +) + +func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validationPeriod, digits uint) (string, error) { + if b.xPriv == nil { + return "", errors.New("init client with xPriv first") + } + + xpriv, err := deriveXprivForPki(b.xPriv) + if err != nil { + return "", err + } + + cXpub, err := bip32.NewKeyFromString(contact.PubKey) + if err != nil { + return "", fmt.Errorf("contact's PubKey is invalid: %w", err) + } + + ts := &totp.Service{ + Period: validationPeriod, + Digits: digits, + } + + return ts.GenarateTotp(xpriv, cXpub) +} + +func (b *WalletClient) ConfirmTotpForContact(contact *models.Contact, passcode string, validationPeriod, digits uint) (bool, error) { + if b.xPriv == nil { + return false, errors.New("init client with xPriv first") + } + + xpriv, err := deriveXprivForPki(b.xPriv) + if err != nil { + return false, err + } + + cXpub, err := bip32.NewKeyFromString(contact.PubKey) + if err != nil { + return false, fmt.Errorf("contact's PubKey is invalid: %w", err) + } + + ts := &totp.Service{ + Period: validationPeriod, + Digits: digits, + } + + return ts.ValidateTotp(xpriv, cXpub, passcode) +} + +func deriveXprivForPki(xpriv *bip32.ExtendedKey) (*bip32.ExtendedKey, error) { + // derive xpriv for current PKI + // TODO: currently we don't support PKI rotation + // PKI derivation path: m/0/0/0 + pkiXpriv, err := bitcoin.GetHDKeyByPath(xpriv, utils.ChainExternal, 0) + if err != nil { + return nil, err + } + + return pkiXpriv.Child(0) +} diff --git a/totp/totp.go b/totp/totp.go new file mode 100644 index 0000000..ec26d49 --- /dev/null +++ b/totp/totp.go @@ -0,0 +1,57 @@ +package totp + +import ( + "encoding/base32" + "time" + + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/bip32" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +type Service struct { + Period uint + Digits uint +} + +func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { + secret, err := sharedSecret(xPriv, xPub) + if err != nil { + return "", err + } + + return totp.GenerateCodeCustom(string(secret), time.Now(), s.getOpts()) +} + +func (s *Service) ValidateTotp(xPriv, xPub *bip32.ExtendedKey, passcode string) (bool, error) { + secret, err := sharedSecret(xPriv, xPub) + if err != nil { + return false, err + } + + return totp.ValidateCustom(passcode, string(secret), time.Now(), s.getOpts()) +} + +func (s *Service) getOpts() totp.ValidateOpts { + return totp.ValidateOpts{ + Period: s.Period, + Digits: otp.Digits(s.Digits), + } +} + +func sharedSecret(xPriv, xPub *bip32.ExtendedKey) (string, error) { + privKey, err := xPriv.ECPrivKey() + if err != nil { + return "", err + } + + pubKey, err := xPub.ECPubKey() + if err != nil { + return "", err + } + + x, _ := bec.S256().ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) + + return base32.StdEncoding.EncodeToString(x.Bytes()), nil +} diff --git a/totp/totp_test.go b/totp/totp_test.go new file mode 100644 index 0000000..1dbefe0 --- /dev/null +++ b/totp/totp_test.go @@ -0,0 +1,86 @@ +package totp + +import ( + "testing" + "time" + + "github.com/libsv/go-bk/bip32" + "github.com/stretchr/testify/require" +) + +func TestTotpService(t *testing.T) { + const a_xprivStr = "xprv9s21ZrQH143K4BNj7w6PX1kTzUDMrKGc9VUEHhguqdbdyPVdP6t6NJNNMxuksetwDCFiauEvwNtNyt5xkXPS6eDtvf7e1GAcDERdoeApfGc" + const b_xprivStr = "xprv9s21ZrQH143K2yLE69dKjrRmHE5atymAuVzqWj7gZcBDdhSZQcGZiFmMBwpXDC36bvMV398HXwVK4UUCwB5oddU5dU93QKD7JFyyizYwLDp" + + t.Run("Passcode has the exact number of digits", func(t *testing.T) { + // given + const givenDigits = 4 + + a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) + b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) + b_xpub, _ := b_xpriv.Neuter() + + sut := &Service{ + Digits: givenDigits, + } + + // when + pc, err := sut.GenarateTotp(a_xpriv, b_xpub) + + // then + require.NoError(t, err) + require.Len(t, pc, givenDigits) + }) + + t.Run("Passcode is valid", func(t *testing.T) { + // given + a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) + a_xpub, _ := a_xpriv.Neuter() + b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) + b_xpub, _ := b_xpriv.Neuter() + + sut := &Service{ + Digits: 2, + } + a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) + require.NoError(t, err) + + // when + isValid, err := sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + require.NoError(t, err) + + // then + require.NoError(t, err) + require.True(t, isValid) + }) + + t.Run("Passcode is invalid after given seconds", func(t *testing.T) { + // given + const givenSeconds = 3 + + a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) + a_xpub, _ := a_xpriv.Neuter() + b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) + b_xpub, _ := b_xpriv.Neuter() + + sut := &Service{ + Digits: 2, + } + a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) + require.NoError(t, err) + + // when + isValid, err := sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + require.NoError(t, err) + require.True(t, isValid) + + time.Sleep(givenSeconds * time.Second) + + isValid, err = sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + + // then + require.NoError(t, err) + require.False(t, isValid) + }) + +} From 9cf6e97d1793c8657a071a8c47c7ca1d6ab141a8 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Thu, 4 Apr 2024 10:35:42 +0200 Subject: [PATCH 06/11] feat(BUX-400): confirm contact after totp validation --- contacts.go | 17 ++++++++++++++--- contacts_test.go | 6 ------ go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/contacts.go b/contacts.go index 0c11107..ae7d36d 100644 --- a/contacts.go +++ b/contacts.go @@ -2,6 +2,8 @@ package walletclient import ( "context" + "errors" + "fmt" "github.com/bitcoin-sv/spv-wallet-go-client/transports" "github.com/bitcoin-sv/spv-wallet/models" @@ -27,9 +29,18 @@ func (b *WalletClient) RejectContact(ctx context.Context, paymail string) transp return b.transport.RejectContact(ctx, paymail) } -// ConfirmContact will confirm the contact associated with the paymail -func (b *WalletClient) ConfirmContact(ctx context.Context, paymail string) transports.ResponseError { - return b.transport.ConfirmContact(ctx, paymail) +// ConfirmContact will try to confirm the contact +func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode string, validationPeriod, digits uint) transports.ResponseError { + isTotpValid, err := b.ConfirmTotpForContact(contact, passcode, validationPeriod, digits) + if err != nil { + return transports.WrapError(fmt.Errorf("totp validation failed: %w", err)) + } + + if !isTotpValid { + return transports.WrapError(errors.New("totp is invalid")) + } + + return b.transport.ConfirmContact(ctx, contact.Paymail) } // GetContacts will get contacts by conditions diff --git a/contacts_test.go b/contacts_test.go index ab468bb..ed91fa3 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -28,12 +28,6 @@ func TestContactActionsRouting(t *testing.T) { responsePayload: "{}", f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, }, - { - name: "ConfirmContact", - route: "/contact/confirmed/", - responsePayload: "{}", - f: func(c *WalletClient) error { return c.ConfirmContact(context.Background(), fixtures.PaymailAddress) }, - }, { name: "GetContacts", route: "/contact/search/", diff --git a/go.mod b/go.mod index 508b13c..c880bfb 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bitcoin-sv/spv-wallet-go-client go 1.21 require ( - github.com/bitcoin-sv/spv-wallet/models v0.25.0 + github.com/bitcoin-sv/spv-wallet/models v0.26.0 github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 github.com/libsv/go-bk v0.1.6 github.com/libsv/go-bt/v2 v2.2.5 diff --git a/go.sum b/go.sum index 3d5c870..0462d9c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bitcoin-sv/spv-wallet/models v0.25.0 h1:tdUxF8fTDp23i2rvDma4/gUeeZePa+WFEceQLUSZhe4= -github.com/bitcoin-sv/spv-wallet/models v0.25.0/go.mod h1:P8vXF1mPg1Zh3xSvB9yqwuPJfOR8Tt/SAG2FYztwENI= +github.com/bitcoin-sv/spv-wallet/models v0.26.0 h1:BUWBPguOp/qV1RTCnKqjuqduhygLhe2zrJQf4Sl77kg= +github.com/bitcoin-sv/spv-wallet/models v0.26.0/go.mod h1:P8vXF1mPg1Zh3xSvB9yqwuPJfOR8Tt/SAG2FYztwENI= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fMuWYhNoZa1tck= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 h1:2yTIV9u7H0BhRDGXH5xrAwAz7XibWJtX2dNezMeNsUo= From e72d1e36309b90b42ecc125ed444b9c0afee6679 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Thu, 4 Apr 2024 10:52:05 +0200 Subject: [PATCH 07/11] feat(BUX-400): add comments to exported functions --- contacts.go | 2 +- totp.go | 8 +++++--- totp/totp.go | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/contacts.go b/contacts.go index ae7d36d..24a36de 100644 --- a/contacts.go +++ b/contacts.go @@ -31,7 +31,7 @@ func (b *WalletClient) RejectContact(ctx context.Context, paymail string) transp // ConfirmContact will try to confirm the contact func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode string, validationPeriod, digits uint) transports.ResponseError { - isTotpValid, err := b.ConfirmTotpForContact(contact, passcode, validationPeriod, digits) + isTotpValid, err := b.ValidateTotpForContact(contact, passcode, validationPeriod, digits) if err != nil { return transports.WrapError(fmt.Errorf("totp validation failed: %w", err)) } diff --git a/totp.go b/totp.go index 7c034e1..a4ba40a 100644 --- a/totp.go +++ b/totp.go @@ -11,6 +11,7 @@ import ( "github.com/libsv/go-bk/bip32" ) +// GenerateTotpForContact creates one time-based one-time password based on secret shared between the user and the contact func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validationPeriod, digits uint) (string, error) { if b.xPriv == nil { return "", errors.New("init client with xPriv first") @@ -34,7 +35,8 @@ func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validatio return ts.GenarateTotp(xpriv, cXpub) } -func (b *WalletClient) ConfirmTotpForContact(contact *models.Contact, passcode string, validationPeriod, digits uint) (bool, error) { +// ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact +func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode string, validationPeriod, digits uint) (bool, error) { if b.xPriv == nil { return false, errors.New("init client with xPriv first") } @@ -58,9 +60,9 @@ func (b *WalletClient) ConfirmTotpForContact(contact *models.Contact, passcode s } func deriveXprivForPki(xpriv *bip32.ExtendedKey) (*bip32.ExtendedKey, error) { - // derive xpriv for current PKI - // TODO: currently we don't support PKI rotation // PKI derivation path: m/0/0/0 + // NOTICE: we currently do not support PKI rotation; however, adjustments will be made if and when we decide to implement it + pkiXpriv, err := bitcoin.GetHDKeyByPath(xpriv, utils.ChainExternal, 0) if err != nil { return nil, err diff --git a/totp/totp.go b/totp/totp.go index ec26d49..f2cee51 100644 --- a/totp/totp.go +++ b/totp/totp.go @@ -10,11 +10,13 @@ import ( "github.com/pquerna/otp/totp" ) +// Time-base one-time password service type Service struct { Period uint Digits uint } +// GenerateTotp creates one time-based one-time password based on secrets calculated from the keys func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { secret, err := sharedSecret(xPriv, xPub) if err != nil { @@ -24,6 +26,7 @@ func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { return totp.GenerateCodeCustom(string(secret), time.Now(), s.getOpts()) } +// ValidateTotp checks if given one-time password is valid func (s *Service) ValidateTotp(xPriv, xPub *bip32.ExtendedKey, passcode string) (bool, error) { secret, err := sharedSecret(xPriv, xPub) if err != nil { From addb240995469dc22dd2c444e56ab6e2bbdafba2 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Thu, 4 Apr 2024 11:10:41 +0200 Subject: [PATCH 08/11] feat(BUX-400): fix test --- totp/totp_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/totp/totp_test.go b/totp/totp_test.go index 1dbefe0..1f6827d 100644 --- a/totp/totp_test.go +++ b/totp/totp_test.go @@ -65,6 +65,7 @@ func TestTotpService(t *testing.T) { sut := &Service{ Digits: 2, + Period: givenSeconds, } a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) require.NoError(t, err) From 79438de820cea066183f99523a2197e8089b86bd Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Fri, 5 Apr 2024 08:44:34 +0200 Subject: [PATCH 09/11] feat(BUX-400): add tests --- contacts_test.go | 67 +++++++++++++++++++++++++++++- fixtures/fixtures.go | 1 + totp.go | 23 +++++++--- totp/totp.go | 15 +++---- totp/totp_test.go | 38 +++++++++-------- totp_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 totp_test.go diff --git a/contacts_test.go b/contacts_test.go index ed91fa3..f57172d 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -5,10 +5,12 @@ import ( "testing" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// TestRejectContact will test the RejectContact method +// TestContactActionsRouting will test routing func TestContactActionsRouting(t *testing.T) { tcs := []struct { name string @@ -55,6 +57,16 @@ func TestContactActionsRouting(t *testing.T) { return err }, }, + {name: "ConfirmContact", + route: "/contact/confirmed/", + responsePayload: "{}", + f: func(c *WalletClient) error { + contact := models.Contact{PubKey: fixtures.PubKey} + + passcode, _ := c.GenerateTotpForContact(&contact, 30, 2) + return c.ConfirmContact(context.Background(), &contact, passcode, 30, 2) + }, + }, } for _, tc := range tcs { @@ -68,7 +80,7 @@ func TestContactActionsRouting(t *testing.T) { Client: WithHTTPClient, } - client := getTestWalletClient(tmq, true) + client := getTestWalletClient(tmq, false) // when err := tc.f(client) @@ -79,3 +91,54 @@ func TestContactActionsRouting(t *testing.T) { } } + +func TestConfirmContact(t *testing.T) { + t.Run("TOTP is valid - call Confirm Action", func(t *testing.T) { + // given + tmq := testTransportHandler{ + Type: fixtures.RequestType, + Path: "/contact/confirmed/", + Result: "{}", + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + client := getTestWalletClient(tmq, false) + + contact := &models.Contact{PubKey: fixtures.PubKey} + totp, err := client.GenerateTotpForContact(contact, 30, 2) + require.NoError(t, err) + + // when + result := client.ConfirmContact(context.Background(), contact, totp, 30, 2) + + // then + require.Nil(t, result) + }) + + t.Run("TOTP is invalid - do not call Confirm Action", func(t *testing.T) { + // given + tmq := testTransportHandler{ + Type: fixtures.RequestType, + Path: "/unknown/", + Result: "{}", + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + client := getTestWalletClient(tmq, false) + + alice := &models.Contact{PubKey: "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde"} + a_totp, err := client.GenerateTotpForContact(alice, 30, 2) + require.NoError(t, err) + + bob := &models.Contact{PubKey: "02dde493752f7bc89822ed8a13e0e4aa04550c6c4430800e4be1e5e5c2556cf65b"} + + // when + result := client.ConfirmContact(context.Background(), bob, a_totp, 30, 2) + + // then + require.NotNil(t, result) + require.Equal(t, result.Error(), "totp is invalid") + }) +} diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index c9e494c..356517b 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -14,6 +14,7 @@ const ( XPrivString = "xprv9s21ZrQH143K3N6qVJQAu4EP51qMcyrKYJLkLgmYXgz58xmVxVLSsbx2DfJUtjcnXK8NdvkHMKfmmg5AJT2nqqRWUrjSHX29qEJwBgBPkJQ" AccessKeyString = "7779d24ca6f8821f225042bf55e8f80aa41b08b879b72827f51e41e6523b9cd0" PaymailAddress = "address@paymail.com" + PubKey = "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" ) func MarshallForTestHandler(object any) string { diff --git a/totp.go b/totp.go index a4ba40a..f82469a 100644 --- a/totp.go +++ b/totp.go @@ -1,6 +1,7 @@ package walletclient import ( + "encoding/hex" "errors" "fmt" @@ -8,13 +9,16 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/utils" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" ) +var ErrClientInitNoXpriv = errors.New("init client with xPriv first") + // GenerateTotpForContact creates one time-based one-time password based on secret shared between the user and the contact func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validationPeriod, digits uint) (string, error) { if b.xPriv == nil { - return "", errors.New("init client with xPriv first") + return "", ErrClientInitNoXpriv } xpriv, err := deriveXprivForPki(b.xPriv) @@ -22,7 +26,7 @@ func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validatio return "", err } - cXpub, err := bip32.NewKeyFromString(contact.PubKey) + cXpub, err := convertPubKey(contact.PubKey) if err != nil { return "", fmt.Errorf("contact's PubKey is invalid: %w", err) } @@ -32,13 +36,13 @@ func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validatio Digits: digits, } - return ts.GenarateTotp(xpriv, cXpub) + return ts.GenerateTotp(xpriv, cXpub) } // ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode string, validationPeriod, digits uint) (bool, error) { if b.xPriv == nil { - return false, errors.New("init client with xPriv first") + return false, ErrClientInitNoXpriv } xpriv, err := deriveXprivForPki(b.xPriv) @@ -46,7 +50,7 @@ func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode return false, err } - cXpub, err := bip32.NewKeyFromString(contact.PubKey) + cXpub, err := convertPubKey(contact.PubKey) if err != nil { return false, fmt.Errorf("contact's PubKey is invalid: %w", err) } @@ -70,3 +74,12 @@ func deriveXprivForPki(xpriv *bip32.ExtendedKey) (*bip32.ExtendedKey, error) { return pkiXpriv.Child(0) } + +func convertPubKey(pubKey string) (*bec.PublicKey, error) { + hex, err := hex.DecodeString(pubKey) + if err != nil { + return nil, err + } + + return bec.ParsePubKey(hex, bec.S256()) +} diff --git a/totp/totp.go b/totp/totp.go index f2cee51..3d65258 100644 --- a/totp/totp.go +++ b/totp/totp.go @@ -17,8 +17,8 @@ type Service struct { } // GenerateTotp creates one time-based one-time password based on secrets calculated from the keys -func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { - secret, err := sharedSecret(xPriv, xPub) +func (s *Service) GenerateTotp(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey) (string, error) { + secret, err := sharedSecret(xPriv, pubKey) if err != nil { return "", err } @@ -27,8 +27,8 @@ func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { } // ValidateTotp checks if given one-time password is valid -func (s *Service) ValidateTotp(xPriv, xPub *bip32.ExtendedKey, passcode string) (bool, error) { - secret, err := sharedSecret(xPriv, xPub) +func (s *Service) ValidateTotp(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey, passcode string) (bool, error) { + secret, err := sharedSecret(xPriv, pubKey) if err != nil { return false, err } @@ -43,17 +43,12 @@ func (s *Service) getOpts() totp.ValidateOpts { } } -func sharedSecret(xPriv, xPub *bip32.ExtendedKey) (string, error) { +func sharedSecret(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey) (string, error) { privKey, err := xPriv.ECPrivKey() if err != nil { return "", err } - pubKey, err := xPub.ECPubKey() - if err != nil { - return "", err - } - x, _ := bec.S256().ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) return base32.StdEncoding.EncodeToString(x.Bytes()), nil diff --git a/totp/totp_test.go b/totp/totp_test.go index 1f6827d..738c80c 100644 --- a/totp/totp_test.go +++ b/totp/totp_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" "github.com/stretchr/testify/require" ) @@ -16,16 +17,15 @@ func TestTotpService(t *testing.T) { // given const givenDigits = 4 - a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) - b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) - b_xpub, _ := b_xpriv.Neuter() + a_xpriv, _ := getKeyPair(a_xprivStr) + _, b_pk := getKeyPair(b_xprivStr) sut := &Service{ Digits: givenDigits, } // when - pc, err := sut.GenarateTotp(a_xpriv, b_xpub) + pc, err := sut.GenerateTotp(a_xpriv, b_pk) // then require.NoError(t, err) @@ -34,19 +34,17 @@ func TestTotpService(t *testing.T) { t.Run("Passcode is valid", func(t *testing.T) { // given - a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) - a_xpub, _ := a_xpriv.Neuter() - b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) - b_xpub, _ := b_xpriv.Neuter() + a_xpriv, a_pk := getKeyPair(a_xprivStr) + b_xpriv, b_pk := getKeyPair(b_xprivStr) sut := &Service{ Digits: 2, } - a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) + a_passcode, err := sut.GenerateTotp(a_xpriv, b_pk) require.NoError(t, err) // when - isValid, err := sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + isValid, err := sut.ValidateTotp(b_xpriv, a_pk, a_passcode) require.NoError(t, err) // then @@ -58,26 +56,24 @@ func TestTotpService(t *testing.T) { // given const givenSeconds = 3 - a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) - a_xpub, _ := a_xpriv.Neuter() - b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) - b_xpub, _ := b_xpriv.Neuter() + a_xpriv, a_pk := getKeyPair(a_xprivStr) + b_xpriv, b_pk := getKeyPair(b_xprivStr) sut := &Service{ Digits: 2, Period: givenSeconds, } - a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) + a_passcode, err := sut.GenerateTotp(a_xpriv, b_pk) require.NoError(t, err) // when - isValid, err := sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + isValid, err := sut.ValidateTotp(b_xpriv, a_pk, a_passcode) require.NoError(t, err) require.True(t, isValid) time.Sleep(givenSeconds * time.Second) - isValid, err = sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + isValid, err = sut.ValidateTotp(b_xpriv, a_pk, a_passcode) // then require.NoError(t, err) @@ -85,3 +81,11 @@ func TestTotpService(t *testing.T) { }) } + +func getKeyPair(xprivStr string) (xpriv *bip32.ExtendedKey, pk *bec.PublicKey) { + xpriv, _ = bip32.NewKeyFromString(xprivStr) + xpub, _ := xpriv.Neuter() + pk, _ = xpub.ECPubKey() + + return +} diff --git a/totp_test.go b/totp_test.go new file mode 100644 index 0000000..f528d5f --- /dev/null +++ b/totp_test.go @@ -0,0 +1,99 @@ +package walletclient + +import ( + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" +) + +func TestGenerateTotpForContact(t *testing.T) { + t.Run("success", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: fixtures.PubKey} + + // when + pass, err := sut.GenerateTotpForContact(&contact, 30, 2) + + // then + require.NoError(t, err) + require.Len(t, pass, 2) + }) + + t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPub(fixtures.XPubString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + // when + _, err = sut.GenerateTotpForContact(nil, 30, 2) + + // then + require.ErrorIs(t, err, ErrClientInitNoXpriv) + }) + + t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: "invalid-pk-format"} + + // when + _, err = sut.GenerateTotpForContact(&contact, 30, 2) + + // then + require.ErrorContains(t, err, "contact's PubKey is invalid:") + + }) +} + +func TestValidateTotpForContact(t *testing.T) { + t.Run("success", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: fixtures.PubKey} + pass, err := sut.GenerateTotpForContact(&contact, 30, 2) + require.NoError(t, err) + + // when + result, err := sut.ValidateTotpForContact(&contact, pass, 30, 2) + + // then + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPub(fixtures.XPubString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + // when + _, err = sut.ValidateTotpForContact(nil, "", 30, 2) + + // then + require.ErrorIs(t, err, ErrClientInitNoXpriv) + }) + + t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: "invalid-pk-format"} + + // when + _, err = sut.ValidateTotpForContact(&contact, "", 30, 2) + + // then + require.ErrorContains(t, err, "contact's PubKey is invalid:") + + }) +} From c94aacca455479e274066d65d9ea1d29769079b8 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Mon, 8 Apr 2024 09:38:17 +0200 Subject: [PATCH 10/11] feat(BUX-400): remove totp service --- contacts.go | 4 +-- contacts_test.go | 9 ++--- totp.go | 64 +++++++++++++++++++++------------ totp/totp.go | 55 ---------------------------- totp/totp_test.go | 91 ----------------------------------------------- totp_test.go | 12 +++++++ 6 files changed, 60 insertions(+), 175 deletions(-) delete mode 100644 totp/totp.go delete mode 100644 totp/totp_test.go diff --git a/contacts.go b/contacts.go index 24a36de..8ddc9b3 100644 --- a/contacts.go +++ b/contacts.go @@ -30,8 +30,8 @@ func (b *WalletClient) RejectContact(ctx context.Context, paymail string) transp } // ConfirmContact will try to confirm the contact -func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode string, validationPeriod, digits uint) transports.ResponseError { - isTotpValid, err := b.ValidateTotpForContact(contact, passcode, validationPeriod, digits) +func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode string, period, digits uint) transports.ResponseError { + isTotpValid, err := b.ValidateTotpForContact(contact, passcode, period, digits) if err != nil { return transports.WrapError(fmt.Errorf("totp validation failed: %w", err)) } diff --git a/contacts_test.go b/contacts_test.go index f57172d..80226f9 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -57,7 +57,8 @@ func TestContactActionsRouting(t *testing.T) { return err }, }, - {name: "ConfirmContact", + { + name: "ConfirmContact", route: "/contact/confirmed/", responsePayload: "{}", f: func(c *WalletClient) error { @@ -80,7 +81,7 @@ func TestContactActionsRouting(t *testing.T) { Client: WithHTTPClient, } - client := getTestWalletClient(tmq, false) + client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString)) // when err := tc.f(client) @@ -103,7 +104,7 @@ func TestConfirmContact(t *testing.T) { Client: WithHTTPClient, } - client := getTestWalletClient(tmq, false) + client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString)) contact := &models.Contact{PubKey: fixtures.PubKey} totp, err := client.GenerateTotpForContact(contact, 30, 2) @@ -126,7 +127,7 @@ func TestConfirmContact(t *testing.T) { Client: WithHTTPClient, } - client := getTestWalletClient(tmq, false) + client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString)) alice := &models.Contact{PubKey: "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde"} a_totp, err := client.GenerateTotpForContact(alice, 30, 2) diff --git a/totp.go b/totp.go index f82469a..8893660 100644 --- a/totp.go +++ b/totp.go @@ -1,66 +1,84 @@ package walletclient import ( + "encoding/base32" "encoding/hex" "errors" "fmt" + "time" - "github.com/bitcoin-sv/spv-wallet-go-client/totp" "github.com/bitcoin-sv/spv-wallet-go-client/utils" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" ) var ErrClientInitNoXpriv = errors.New("init client with xPriv first") // GenerateTotpForContact creates one time-based one-time password based on secret shared between the user and the contact -func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validationPeriod, digits uint) (string, error) { - if b.xPriv == nil { - return "", ErrClientInitNoXpriv - } - - xpriv, err := deriveXprivForPki(b.xPriv) +func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, period, digits uint) (string, error) { + secret, err := sharedSecret(b, contact) if err != nil { return "", err } - cXpub, err := convertPubKey(contact.PubKey) + opts := totp.ValidateOpts{ + Period: period, + Digits: otp.Digits(digits), + } + + return totp.GenerateCodeCustom(string(secret), time.Now(), opts) +} + +// ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact +func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode string, period, digits uint) (bool, error) { + secret, err := sharedSecret(b, contact) if err != nil { - return "", fmt.Errorf("contact's PubKey is invalid: %w", err) + return false, err } - ts := &totp.Service{ - Period: validationPeriod, - Digits: digits, + opts := totp.ValidateOpts{ + Period: period, + Digits: otp.Digits(digits), } - return ts.GenerateTotp(xpriv, cXpub) + return totp.ValidateCustom(passcode, string(secret), time.Now(), opts) } -// ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact -func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode string, validationPeriod, digits uint) (bool, error) { +func sharedSecret(b *WalletClient, c *models.Contact) (string, error) { + privKey, pubKey, err := getSharedSecretFactors(b, c) + if err != nil { + return "", err + } + + x, _ := bec.S256().ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) + return base32.StdEncoding.EncodeToString(x.Bytes()), nil +} + +func getSharedSecretFactors(b *WalletClient, c *models.Contact) (*bec.PrivateKey, *bec.PublicKey, error) { if b.xPriv == nil { - return false, ErrClientInitNoXpriv + return nil, nil, ErrClientInitNoXpriv } xpriv, err := deriveXprivForPki(b.xPriv) if err != nil { - return false, err + return nil, nil, err } - cXpub, err := convertPubKey(contact.PubKey) + privKey, err := xpriv.ECPrivKey() if err != nil { - return false, fmt.Errorf("contact's PubKey is invalid: %w", err) + return nil, nil, err } - ts := &totp.Service{ - Period: validationPeriod, - Digits: digits, + pubKey, err := convertPubKey(c.PubKey) + if err != nil { + return nil, nil, fmt.Errorf("contact's PubKey is invalid: %w", err) } - return ts.ValidateTotp(xpriv, cXpub, passcode) + return privKey, pubKey, nil } func deriveXprivForPki(xpriv *bip32.ExtendedKey) (*bip32.ExtendedKey, error) { diff --git a/totp/totp.go b/totp/totp.go deleted file mode 100644 index 3d65258..0000000 --- a/totp/totp.go +++ /dev/null @@ -1,55 +0,0 @@ -package totp - -import ( - "encoding/base32" - "time" - - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" - "github.com/pquerna/otp" - "github.com/pquerna/otp/totp" -) - -// Time-base one-time password service -type Service struct { - Period uint - Digits uint -} - -// GenerateTotp creates one time-based one-time password based on secrets calculated from the keys -func (s *Service) GenerateTotp(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey) (string, error) { - secret, err := sharedSecret(xPriv, pubKey) - if err != nil { - return "", err - } - - return totp.GenerateCodeCustom(string(secret), time.Now(), s.getOpts()) -} - -// ValidateTotp checks if given one-time password is valid -func (s *Service) ValidateTotp(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey, passcode string) (bool, error) { - secret, err := sharedSecret(xPriv, pubKey) - if err != nil { - return false, err - } - - return totp.ValidateCustom(passcode, string(secret), time.Now(), s.getOpts()) -} - -func (s *Service) getOpts() totp.ValidateOpts { - return totp.ValidateOpts{ - Period: s.Period, - Digits: otp.Digits(s.Digits), - } -} - -func sharedSecret(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey) (string, error) { - privKey, err := xPriv.ECPrivKey() - if err != nil { - return "", err - } - - x, _ := bec.S256().ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) - - return base32.StdEncoding.EncodeToString(x.Bytes()), nil -} diff --git a/totp/totp_test.go b/totp/totp_test.go deleted file mode 100644 index 738c80c..0000000 --- a/totp/totp_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package totp - -import ( - "testing" - "time" - - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" - "github.com/stretchr/testify/require" -) - -func TestTotpService(t *testing.T) { - const a_xprivStr = "xprv9s21ZrQH143K4BNj7w6PX1kTzUDMrKGc9VUEHhguqdbdyPVdP6t6NJNNMxuksetwDCFiauEvwNtNyt5xkXPS6eDtvf7e1GAcDERdoeApfGc" - const b_xprivStr = "xprv9s21ZrQH143K2yLE69dKjrRmHE5atymAuVzqWj7gZcBDdhSZQcGZiFmMBwpXDC36bvMV398HXwVK4UUCwB5oddU5dU93QKD7JFyyizYwLDp" - - t.Run("Passcode has the exact number of digits", func(t *testing.T) { - // given - const givenDigits = 4 - - a_xpriv, _ := getKeyPair(a_xprivStr) - _, b_pk := getKeyPair(b_xprivStr) - - sut := &Service{ - Digits: givenDigits, - } - - // when - pc, err := sut.GenerateTotp(a_xpriv, b_pk) - - // then - require.NoError(t, err) - require.Len(t, pc, givenDigits) - }) - - t.Run("Passcode is valid", func(t *testing.T) { - // given - a_xpriv, a_pk := getKeyPair(a_xprivStr) - b_xpriv, b_pk := getKeyPair(b_xprivStr) - - sut := &Service{ - Digits: 2, - } - a_passcode, err := sut.GenerateTotp(a_xpriv, b_pk) - require.NoError(t, err) - - // when - isValid, err := sut.ValidateTotp(b_xpriv, a_pk, a_passcode) - require.NoError(t, err) - - // then - require.NoError(t, err) - require.True(t, isValid) - }) - - t.Run("Passcode is invalid after given seconds", func(t *testing.T) { - // given - const givenSeconds = 3 - - a_xpriv, a_pk := getKeyPair(a_xprivStr) - b_xpriv, b_pk := getKeyPair(b_xprivStr) - - sut := &Service{ - Digits: 2, - Period: givenSeconds, - } - a_passcode, err := sut.GenerateTotp(a_xpriv, b_pk) - require.NoError(t, err) - - // when - isValid, err := sut.ValidateTotp(b_xpriv, a_pk, a_passcode) - require.NoError(t, err) - require.True(t, isValid) - - time.Sleep(givenSeconds * time.Second) - - isValid, err = sut.ValidateTotp(b_xpriv, a_pk, a_passcode) - - // then - require.NoError(t, err) - require.False(t, isValid) - }) - -} - -func getKeyPair(xprivStr string) (xpriv *bip32.ExtendedKey, pk *bec.PublicKey) { - xpriv, _ = bip32.NewKeyFromString(xprivStr) - xpub, _ := xpriv.Neuter() - pk, _ = xpub.ECPubKey() - - return -} diff --git a/totp_test.go b/totp_test.go index f528d5f..440f054 100644 --- a/totp_test.go +++ b/totp_test.go @@ -36,6 +36,18 @@ func TestGenerateTotpForContact(t *testing.T) { require.ErrorIs(t, err, ErrClientInitNoXpriv) }) + t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPub(fixtures.XPubString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + // when + _, err = sut.GenerateTotpForContact(nil, 30, 2) + + // then + require.ErrorIs(t, err, ErrClientInitNoXpriv) + }) + t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { // given sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) From 7c3aea0cf366f7db9bfb47918febb37125b3b818 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Mon, 8 Apr 2024 13:50:22 +0200 Subject: [PATCH 11/11] feat(BUX-400): add default values for totp config --- totp.go | 38 ++++++++++++++++++++++++++------------ totp_test.go | 12 ------------ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/totp.go b/totp.go index 8893660..ef7c571 100644 --- a/totp.go +++ b/totp.go @@ -18,6 +18,13 @@ import ( var ErrClientInitNoXpriv = errors.New("init client with xPriv first") +const ( + // Default number of seconds a TOTP is valid for. + TotpDefaultPeriod uint = 30 + // Default TOTP length + TotpDefaultDigits uint = 2 +) + // GenerateTotpForContact creates one time-based one-time password based on secret shared between the user and the contact func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, period, digits uint) (string, error) { secret, err := sharedSecret(b, contact) @@ -25,12 +32,8 @@ func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, period, d return "", err } - opts := totp.ValidateOpts{ - Period: period, - Digits: otp.Digits(digits), - } - - return totp.GenerateCodeCustom(string(secret), time.Now(), opts) + opts := getTotpOpts(period, digits) + return totp.GenerateCodeCustom(string(secret), time.Now(), *opts) } // ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact @@ -40,12 +43,8 @@ func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode return false, err } - opts := totp.ValidateOpts{ - Period: period, - Digits: otp.Digits(digits), - } - - return totp.ValidateCustom(passcode, string(secret), time.Now(), opts) + opts := getTotpOpts(period, digits) + return totp.ValidateCustom(passcode, string(secret), time.Now(), *opts) } func sharedSecret(b *WalletClient, c *models.Contact) (string, error) { @@ -58,6 +57,21 @@ func sharedSecret(b *WalletClient, c *models.Contact) (string, error) { return base32.StdEncoding.EncodeToString(x.Bytes()), nil } +func getTotpOpts(period, digits uint) *totp.ValidateOpts { + if period == 0 { + period = TotpDefaultPeriod + } + + if digits == 0 { + digits = TotpDefaultDigits + } + + return &totp.ValidateOpts{ + Period: period, + Digits: otp.Digits(digits), + } +} + func getSharedSecretFactors(b *WalletClient, c *models.Contact) (*bec.PrivateKey, *bec.PublicKey, error) { if b.xPriv == nil { return nil, nil, ErrClientInitNoXpriv diff --git a/totp_test.go b/totp_test.go index 440f054..f528d5f 100644 --- a/totp_test.go +++ b/totp_test.go @@ -36,18 +36,6 @@ func TestGenerateTotpForContact(t *testing.T) { require.ErrorIs(t, err, ErrClientInitNoXpriv) }) - t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { - // given - sut, err := New(WithXPub(fixtures.XPubString), WithHTTP("localhost:3001")) - require.NoError(t, err) - - // when - _, err = sut.GenerateTotpForContact(nil, 30, 2) - - // then - require.ErrorIs(t, err, ErrClientInitNoXpriv) - }) - t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { // given sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001"))