From 16a8da3b92a77db46a20c7a5169fa3ae21888a30 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Fri, 10 May 2024 07:52:09 +0200 Subject: [PATCH 01/27] init refactoring --- client_options.go | 181 ++++++++++++++++++++++++++--------- walletclient.go | 140 +++++++++++++-------------- walletclient2.go | 118 +++++++++++++++++++++++ walletclient_configurator.go | 1 + 4 files changed, 325 insertions(+), 115 deletions(-) create mode 100644 walletclient2.go create mode 100644 walletclient_configurator.go diff --git a/client_options.go b/client_options.go index 0d0494c..155e5fd 100644 --- a/client_options.go +++ b/client_options.go @@ -6,65 +6,160 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/transports" ) -// WithXPriv will set xPrivString on the client -func WithXPriv(xPrivString string) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.xPrivString = xPrivString - } - } +// // WithXPriv will set xPrivString on the client +// func WithXPriv(xPrivString string) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.xPrivString = xPrivString +// } +// } +// } + +// // WithXPub will set xPubString on the client +// func WithXPub(xPubString string) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.xPubString = xPubString +// } +// } +// } + +// WithAccessKey will set the access key on the client +// func WithAccessKey(accessKeyString string) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.accessKeyString = accessKeyString +// } +// } +// } + +// // WithHTTP will overwrite the default client with a custom client +// func WithHTTP(serverURL string) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.transportOptions = append(c.transportOptions, transports.WithHTTP(serverURL)) +// } +// } +// } + +// // WithHTTPClient will overwrite the default client with a custom client +// func WithHTTPClient(serverURL string, httpClient *http.Client) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.transportOptions = append(c.transportOptions, transports.WithHTTPClient(serverURL, httpClient)) +// } +// } +// } + +// // WithAdminKey will set the admin key for admin requests +// func WithAdminKey(adminKey string) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.transportOptions = append(c.transportOptions, transports.WithAdminKey(adminKey)) +// } +// } +// } + +// // WithSignRequest will set whether to sign all requests +// func WithSignRequest(signRequest bool) ClientOps { +// return func(c *WalletClient) { +// if c != nil { +// c.transportOptions = append(c.transportOptions, transports.WithSignRequest(signRequest)) +// } +// } +// } + +// WalletClientConfigurator is the interface for configuring WalletClient +type WalletClientConfigurator interface { + Configure(c *WalletClient) } -// WithXPub will set xPubString on the client -func WithXPub(xPubString string) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.xPubString = xPubString - } - } +// WithXPriv sets the xPrivString field of a WalletClient +type WithXPriv struct { + XPrivString string } -// WithAccessKey will set the access key on the client -func WithAccessKey(accessKeyString string) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.accessKeyString = accessKeyString - } - } +// NewConfigWithXPriv creates new configuration for configurator with xpriv key +func NewConfigWithXPriv(xPrivString string) *WithXPriv { + return &WithXPriv{xPrivString} } -// WithHTTP will overwrite the default client with a custom client -func WithHTTP(serverURL string) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.transportOptions = append(c.transportOptions, transports.WithHTTP(serverURL)) - } - } +// WithXPriv will set xPrivString on the client +func (w *WithXPriv) Configure(c *WalletClient) { + c.xPrivString = w.XPrivString } -// WithHTTPClient will overwrite the default client with a custom client -func WithHTTPClient(serverURL string, httpClient *http.Client) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.transportOptions = append(c.transportOptions, transports.WithHTTPClient(serverURL, httpClient)) - } +// WithHTTP sets the URL for the HTTP transport of a WalletClient +type WithHTTP struct { + ServerURL string +} + +func (w *WithHTTP) Configure(c *WalletClient) { + if c.transportOptions == nil { + c.transportOptions = []transports.ClientOps{} } + c.transportOptions = append(c.transportOptions, transports.WithHTTP(w.ServerURL)) +} + +// WithAdminKey sets the admin key for creating new xpubs +type WithAdminKey struct { + AdminKeyString string } // WithAdminKey will set the admin key for admin requests -func WithAdminKey(adminKey string) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.transportOptions = append(c.transportOptions, transports.WithAdminKey(adminKey)) - } +func (w *WithAdminKey) Configure(c *WalletClient) { + if c.transportOptions == nil { + c.transportOptions = []transports.ClientOps{} } + c.transportOptions = append(c.transportOptions, transports.WithAdminKey(w.AdminKeyString)) +} + +// WithSignRequest configures whether to sign HTTP requests +type WithSignRequest struct { + Sign bool } // WithSignRequest will set whether to sign all requests -func WithSignRequest(signRequest bool) ClientOps { - return func(c *WalletClient) { - if c != nil { - c.transportOptions = append(c.transportOptions, transports.WithSignRequest(signRequest)) +func (w *WithSignRequest) Configure(c *WalletClient) { + if c.transportOptions == nil { + c.transportOptions = []transports.ClientOps{} + } + c.transportOptions = append(c.transportOptions, transports.WithSignRequest(w.Sign)) +} + +// WithXPub sets the xPubString on the client +type WithXPub struct { + XPubString string +} + +// WithXPub will set xPubString on the client +func (w *WithXPub) Configure(c *WalletClient) { + c.xPubString = w.XPubString +} + +// WithAccessKey sets the accessKeyString on the client. +type WithAccessKey struct { + AccessKeyString string +} + +// WithAccessKey will set the access key on the client +func (w *WithAccessKey) Configure(c *WalletClient) { + c.accessKeyString = w.AccessKeyString +} + +// WithHTTPClient sets a custom HTTP client and server URL for the transport of a WalletClient. +type WithHTTPClient struct { + ServerURL string + HTTPClient *http.Client +} + +// WithHTTPClient will overwrite the default client with a custom client +func (w *WithHTTPClient) Configure(c *WalletClient) { + if c != nil { + if c.transportOptions == nil { + c.transportOptions = []transports.ClientOps{} } + // Append the custom HTTP client configuration to the transport options + c.transportOptions = append(c.transportOptions, transports.WithHTTPClient(w.ServerURL, w.HTTPClient)) } } diff --git a/walletclient.go b/walletclient.go index d3924b8..74dc162 100644 --- a/walletclient.go +++ b/walletclient.go @@ -1,19 +1,16 @@ -// Package walletclient is a Go client for interacting with Spv Wallet. package walletclient import ( - "github.com/bitcoin-sv/spv-wallet-go-client/transports" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" "github.com/libsv/go-bk/wif" "github.com/pkg/errors" -) -// ClientOps are used for client options -type ClientOps func(c *WalletClient) + "github.com/bitcoin-sv/spv-wallet-go-client/transports" +) -// WalletClient is the spv wallet go client representation. +// WalletClient is the spv wallet Go client representation. type WalletClient struct { transports.TransportService accessKey *bec.PrivateKey @@ -26,61 +23,21 @@ type WalletClient struct { xPubString string } -// New create a new wallet client -func New(opts ...ClientOps) (*WalletClient, error) { +// New creates a new WalletClient using the provided configuration options. +func New(configurators ...WalletClientConfigurator) (*WalletClient, error) { client := &WalletClient{} - for _, opt := range opts { - opt(client) + for _, configurator := range configurators { + configurator.Configure(client) } - var err error - if client.xPrivString != "" { - if client.xPriv, err = bitcoin.GenerateHDKeyFromString(client.xPrivString); err != nil { - return nil, err - } - if client.xPub, err = client.xPriv.Neuter(); err != nil { - return nil, err - } - } else if client.xPubString != "" { - client.xPriv = nil - if client.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(client.xPubString); err != nil { - return nil, err - } - } else if client.accessKeyString != "" { - client.xPriv = nil - client.xPub = nil - - var privateKey *bec.PrivateKey - var decodedWIF *wif.WIF - if decodedWIF, err = wif.DecodeWIF(client.accessKeyString); err != nil { - // try as a hex string - var errHex error - if privateKey, errHex = bitcoin.PrivateKeyFromString(client.accessKeyString); errHex != nil { - return nil, errors.Wrap(err, errHex.Error()) - } - } else { - privateKey = decodedWIF.PrivKey - } - client.accessKey = privateKey - } else { - return nil, errors.New("no keys available") - } - - transportOptions := make([]transports.ClientOps, 0) - if client.xPriv != nil { - transportOptions = append(transportOptions, transports.WithXPriv(client.xPriv)) - transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) - } else if client.xPub != nil { - transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) - } else if client.accessKey != nil { - transportOptions = append(transportOptions, transports.WithAccessKey(client.accessKey)) - } - if len(client.transportOptions) > 0 { - transportOptions = append(transportOptions, client.transportOptions...) + // Initialize keys based on provided strings + if err := client.initializeKeys(); err != nil { + return nil, err } - if client.transport, err = transports.NewTransport(transportOptions...); err != nil { + // Setup transport based on initialized keys + if err := client.setupTransport(); err != nil { return nil, err } @@ -89,29 +46,68 @@ func New(opts ...ClientOps) (*WalletClient, error) { return client, nil } -// SetAdminKey set the admin key to use to create new xpubs -func (b *WalletClient) SetAdminKey(adminKeyString string) error { - adminKey, err := bip32.NewKeyFromString(adminKeyString) - if err != nil { - return err +// initializeKeys handles the initialization of keys based on the existing fields. +func (c *WalletClient) initializeKeys() error { + var err error + switch { + case c.xPrivString != "": + if c.xPriv, err = bitcoin.GenerateHDKeyFromString(c.xPrivString); err != nil { + return err + } + if c.xPub, err = c.xPriv.Neuter(); err != nil { + return err + } + case c.xPubString != "": + if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(c.xPubString); err != nil { + return err + } + case c.accessKeyString != "": + return c.initializeAccessKey() + default: + return errors.New("no keys provided for initialization") } + return nil +} + +// initializeAccessKey handles the specific initialization of the access key. +func (c *WalletClient) initializeAccessKey() error { + var err error + var privateKey *bec.PrivateKey + var decodedWIF *wif.WIF - b.transport.SetAdminKey(adminKey) + if decodedWIF, err = wif.DecodeWIF(c.accessKeyString); err != nil { + if privateKey, err = bitcoin.PrivateKeyFromString(c.accessKeyString); err != nil { + return errors.Wrap(err, "failed to decode access key") + } + } else { + privateKey = decodedWIF.PrivKey + } + c.accessKey = privateKey return nil } -// SetSignRequest turn the signing of the http request on or off -func (b *WalletClient) SetSignRequest(signRequest bool) { - b.transport.SetSignRequest(signRequest) -} +// setupTransport configures the transport service based on the available keys. +func (c *WalletClient) setupTransport() error { + var err error + transportOptions := make([]transports.ClientOps, 0) -// IsSignRequest return whether to sign all requests -func (b *WalletClient) IsSignRequest() bool { - return b.transport.IsSignRequest() -} + if c.xPriv != nil { + transportOptions = append(transportOptions, transports.WithXPriv(c.xPriv)) + transportOptions = append(transportOptions, transports.WithXPub(c.xPub)) + } else if c.xPub != nil { + transportOptions = append(transportOptions, transports.WithXPub(c.xPub)) + } else if c.accessKey != nil { + transportOptions = append(transportOptions, transports.WithAccessKey(c.accessKey)) + } + + if len(c.transportOptions) > 0 { + transportOptions = append(transportOptions, c.transportOptions...) + } -// GetTransport returns the current transport service -func (b *WalletClient) GetTransport() *transports.TransportService { - return &b.transport + if c.transport, err = transports.NewTransport(transportOptions...); err != nil { + return err + } + + return nil } diff --git a/walletclient2.go b/walletclient2.go new file mode 100644 index 0000000..a6ff50d --- /dev/null +++ b/walletclient2.go @@ -0,0 +1,118 @@ +// Package walletclient is a Go client for interacting with Spv Wallet. +package walletclient + +import ( + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/bip32" + "github.com/libsv/go-bk/wif" + "github.com/pkg/errors" + + "github.com/bitcoin-sv/spv-wallet-go-client/transports" +) + +// ClientOps are used for client options +type ClientOps func(c *WalletClient) + +// // WalletClient is the spv wallet go client representation. +// type WalletClient struct { +// transports.TransportService +// accessKey *bec.PrivateKey +// accessKeyString string +// transport transports.TransportService +// transportOptions []transports.ClientOps +// xPriv *bip32.ExtendedKey +// xPrivString string +// xPub *bip32.ExtendedKey +// xPubString string +// } + +// New create a new wallet client +func NewOld(opts ...ClientOps) (*WalletClient, error) { + client := &WalletClient{} + + for _, opt := range opts { + opt(client) + } + + var err error + if client.xPrivString != "" { + if client.xPriv, err = bitcoin.GenerateHDKeyFromString(client.xPrivString); err != nil { + return nil, err + } + if client.xPub, err = client.xPriv.Neuter(); err != nil { + return nil, err + } + } else if client.xPubString != "" { + client.xPriv = nil + if client.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(client.xPubString); err != nil { + return nil, err + } + } else if client.accessKeyString != "" { + client.xPriv = nil + client.xPub = nil + + var privateKey *bec.PrivateKey + var decodedWIF *wif.WIF + if decodedWIF, err = wif.DecodeWIF(client.accessKeyString); err != nil { + // try as a hex string + var errHex error + if privateKey, errHex = bitcoin.PrivateKeyFromString(client.accessKeyString); errHex != nil { + return nil, errors.Wrap(err, errHex.Error()) + } + } else { + privateKey = decodedWIF.PrivKey + } + client.accessKey = privateKey + } else { + return nil, errors.New("no keys available") + } + + transportOptions := make([]transports.ClientOps, 0) + if client.xPriv != nil { + transportOptions = append(transportOptions, transports.WithXPriv(client.xPriv)) + transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) + } else if client.xPub != nil { + transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) + } else if client.accessKey != nil { + transportOptions = append(transportOptions, transports.WithAccessKey(client.accessKey)) + } + if len(client.transportOptions) > 0 { + transportOptions = append(transportOptions, client.transportOptions...) + } + + if client.transport, err = transports.NewTransport(transportOptions...); err != nil { + return nil, err + } + + client.TransportService = client.transport + + return client, nil +} + +// SetAdminKey set the admin key to use to create new xpubs +func (b *WalletClient) SetAdminKey(adminKeyString string) error { + adminKey, err := bip32.NewKeyFromString(adminKeyString) + if err != nil { + return err + } + + b.transport.SetAdminKey(adminKey) + + return nil +} + +// SetSignRequest turn the signing of the http request on or off +func (b *WalletClient) SetSignRequest(signRequest bool) { + b.transport.SetSignRequest(signRequest) +} + +// IsSignRequest return whether to sign all requests +func (b *WalletClient) IsSignRequest() bool { + return b.transport.IsSignRequest() +} + +// GetTransport returns the current transport service +func (b *WalletClient) GetTransport() *transports.TransportService { + return &b.transport +} diff --git a/walletclient_configurator.go b/walletclient_configurator.go new file mode 100644 index 0000000..a5b925f --- /dev/null +++ b/walletclient_configurator.go @@ -0,0 +1 @@ +package walletclient From fab2f8e556e7112ea2ee7871313a96e1f2a8dfc9 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Sun, 12 May 2024 14:18:21 +0200 Subject: [PATCH 02/27] debugging client --- access_keys.go | 39 +- admin_contacts.go | 56 +- authentication.go | 217 +++++++ walletclient_configurator.go => client.go | 0 client_options.go | 151 +---- config.go | 97 +++ contacts.go | 86 ++- destinations.go | 105 ++- errors.go | 72 +++ examples/tt/admin.go | 168 +++++ http.go | 704 ++++++++++++++++++++ http_admin.go | 371 +++++++++++ transactions.go | 169 +++-- walletclient.go | 61 +- walletclient2.go | 254 ++++---- walletclient_test.go | 750 +++++++++++----------- xpubs.go | 62 +- 17 files changed, 2488 insertions(+), 874 deletions(-) create mode 100644 authentication.go rename walletclient_configurator.go => client.go (100%) create mode 100644 config.go create mode 100644 errors.go create mode 100644 examples/tt/admin.go create mode 100644 http.go create mode 100644 http_admin.go diff --git a/access_keys.go b/access_keys.go index 5bab2cb..30e1544 100644 --- a/access_keys.go +++ b/access_keys.go @@ -1,28 +1,21 @@ package walletclient -import ( - "context" +// // GetAccessKey gets the access key given by id +// func (b *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, transports.ResponseError) { +// return b.transport.GetAccessKey(ctx, id) +// } - "github.com/bitcoin-sv/spv-wallet-go-client/transports" - "github.com/bitcoin-sv/spv-wallet/models" -) +// // GetAccessKeys gets all the access keys filtered by the metadata +// func (b *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, transports.ResponseError) { +// return b.transport.GetAccessKeys(ctx, metadataConditions) +// } -// GetAccessKey gets the access key given by id -func (b *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, transports.ResponseError) { - return b.transport.GetAccessKey(ctx, id) -} +// // CreateAccessKey creates new access key +// func (b *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, transports.ResponseError) { +// return b.transport.CreateAccessKey(ctx, metadata) +// } -// GetAccessKeys gets all the access keys filtered by the metadata -func (b *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, transports.ResponseError) { - return b.transport.GetAccessKeys(ctx, metadataConditions) -} - -// CreateAccessKey creates new access key -func (b *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, transports.ResponseError) { - return b.transport.CreateAccessKey(ctx, metadata) -} - -// RevokeAccessKey revoked the access key given by id -func (b *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, transports.ResponseError) { - return b.transport.RevokeAccessKey(ctx, id) -} +// // RevokeAccessKey revoked the access key given by id +// func (b *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, transports.ResponseError) { +// return b.transport.RevokeAccessKey(ctx, id) +// } diff --git a/admin_contacts.go b/admin_contacts.go index e8c2d53..9c01e02 100644 --- a/admin_contacts.go +++ b/admin_contacts.go @@ -1,34 +1,26 @@ package walletclient -import ( - "context" - - "github.com/bitcoin-sv/spv-wallet/models" - - "github.com/bitcoin-sv/spv-wallet-go-client/transports" -) - -// AdminGetContacts retrieves a list of contacts based on the provided conditions, metadata, and query parameters. -func (wc *WalletClient) AdminGetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *transports.QueryParams) ([]*models.Contact, transports.ResponseError) { - return wc.transport.AdminGetContacts(ctx, conditions, metadata, queryParams) -} - -// AdminUpdateContact updates a contact's details such as their full name using the specified contact ID and metadata. -func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, transports.ResponseError) { - return wc.transport.AdminUpdateContact(ctx, id, fullName, metadata) -} - -// AdminDeleteContact removes a contact from the system using the specified contact ID. -func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) transports.ResponseError { - return wc.transport.AdminDeleteContact(ctx, id) -} - -// AdminAcceptContact marks a contact as accepted using the specified contact ID. -func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, transports.ResponseError) { - return wc.transport.AdminAcceptContact(ctx, id) -} - -// AdminRejectContact marks a contact as rejected using the specified contact ID. -func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, transports.ResponseError) { - return wc.transport.AdminRejectContact(ctx, id) -} +// // AdminGetContacts retrieves a list of contacts based on the provided conditions, metadata, and query parameters. +// func (wc *WalletClient) AdminGetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *transports.QueryParams) ([]*models.Contact, transports.ResponseError) { +// return wc.transport.AdminGetContacts(ctx, conditions, metadata, queryParams) +// } + +// // AdminUpdateContact updates a contact's details such as their full name using the specified contact ID and metadata. +// func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, transports.ResponseError) { +// return wc.transport.AdminUpdateContact(ctx, id, fullName, metadata) +// } + +// // AdminDeleteContact removes a contact from the system using the specified contact ID. +// func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) transports.ResponseError { +// return wc.transport.AdminDeleteContact(ctx, id) +// } + +// // AdminAcceptContact marks a contact as accepted using the specified contact ID. +// func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, transports.ResponseError) { +// return wc.transport.AdminAcceptContact(ctx, id) +// } + +// // AdminRejectContact marks a contact as rejected using the specified contact ID. +// func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, transports.ResponseError) { +// return wc.transport.AdminRejectContact(ctx, id) +// } diff --git a/authentication.go b/authentication.go new file mode 100644 index 0000000..e6e52a6 --- /dev/null +++ b/authentication.go @@ -0,0 +1,217 @@ +package walletclient + +import ( + "encoding/hex" + "fmt" + "net/http" + "time" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/apierrors" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/bip32" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" + + "github.com/bitcoin-sv/spv-wallet-go-client/utils" +) + +// SetSignature will set the signature on the header for the request +func setSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) ResponseError { + // Create the signature + authData, err := createSignature(xPriv, bodyString) + if err != nil { + return WrapError(err) + } + + // Set the auth header + header.Set(models.AuthHeader, authData.XPub) + + return setSignatureHeaders(header, authData) +} + +// GetSignedHex will sign all the inputs using the given xPriv key +func GetSignedHex(dt *models.DraftTransaction, xPriv *bip32.ExtendedKey) (signedHex string, err error) { + var tx *bt.Tx + if tx, err = bt.NewTxFromString(dt.Hex); err != nil { + return + } + + // Enrich inputs + for index, draftInput := range dt.Configuration.Inputs { + tx.Inputs[index].PreviousTxSatoshis = draftInput.Satoshis + + dst := draftInput.Destination + if err = setPreviousTxScript(tx, uint32(index), &dst); err != nil { + return + } + + if err = setUnlockingScript(tx, uint32(index), xPriv, &dst); err != nil { + return + } + } + + // Return the signed hex + signedHex = tx.String() + return +} + +func setPreviousTxScript(tx *bt.Tx, inputIndex uint32, dst *models.Destination) (err error) { + var ls *bscript.Script + if ls, err = bscript.NewFromHexString(dst.LockingScript); err != nil { + return + } + + tx.Inputs[inputIndex].PreviousTxScript = ls + return +} + +func setUnlockingScript(tx *bt.Tx, inputIndex uint32, xPriv *bip32.ExtendedKey, dst *models.Destination) (err error) { + var key *bec.PrivateKey + if key, err = getDerivedKeyForDestination(xPriv, dst); err != nil { + return + } + + var s *bscript.Script + if s, err = getUnlockingScript(tx, inputIndex, key); err != nil { + return + } + + tx.Inputs[inputIndex].UnlockingScript = s + return +} + +func getDerivedKeyForDestination(xPriv *bip32.ExtendedKey, dst *models.Destination) (key *bec.PrivateKey, err error) { + // Derive the child key (m/chain/num) + var derivedKey *bip32.ExtendedKey + if derivedKey, err = bitcoin.GetHDKeyByPath(xPriv, dst.Chain, dst.Num); err != nil { + return + } + + // Derive key for paymail destination (m/chain/num/paymailNum) + if dst.PaymailExternalDerivationNum != nil { + if derivedKey, err = derivedKey.Child( + *dst.PaymailExternalDerivationNum, + ); err != nil { + return + } + } + + if key, err = bitcoin.GetPrivateKeyFromHDKey(derivedKey); err != nil { + return + } + + return +} + +// GetUnlockingScript will generate an unlocking script +func getUnlockingScript(tx *bt.Tx, inputIndex uint32, privateKey *bec.PrivateKey) (*bscript.Script, error) { + sigHashFlags := sighash.AllForkID + + sigHash, err := tx.CalcInputSignatureHash(inputIndex, sigHashFlags) + if err != nil { + return nil, err + } + + var sig *bec.Signature + if sig, err = privateKey.Sign(sigHash); err != nil { + return nil, err + } + + pubKey := privateKey.PubKey().SerialiseCompressed() + signature := sig.Serialise() + + var script *bscript.Script + if script, err = bscript.NewP2PKHUnlockingScript(pubKey, signature, sigHashFlags); err != nil { + return nil, err + } + + return script, nil +} + +// createSignature will create a signature for the given key & body contents +func createSignature(xPriv *bip32.ExtendedKey, bodyString string) (payload *models.AuthPayload, err error) { + // No key? + if xPriv == nil { + err = apierrors.ErrMissingXPriv + return + } + + // Get the xPub + payload = new(models.AuthPayload) + if payload.XPub, err = bitcoin.GetExtendedPublicKey( + xPriv, + ); err != nil { // Should never error if key is correct + return + } + + // auth_nonce is a random unique string to seed the signing message + // this can be checked server side to make sure the request is not being replayed + if payload.AuthNonce, err = utils.RandomHex(32); err != nil { // Should never error if key is correct + return + } + + // Derive the address for signing + var key *bip32.ExtendedKey + if key, err = utils.DeriveChildKeyFromHex( + xPriv, payload.AuthNonce, + ); err != nil { + return + } + + var privateKey *bec.PrivateKey + if privateKey, err = bitcoin.GetPrivateKeyFromHDKey(key); err != nil { + return // Should never error if key is correct + } + + return createSignatureCommon(payload, bodyString, privateKey) +} + +// createSignatureCommon will create a signature +func createSignatureCommon(payload *models.AuthPayload, bodyString string, privateKey *bec.PrivateKey) (*models.AuthPayload, error) { + // Create the auth header hash + payload.AuthHash = utils.Hash(bodyString) + + // auth_time is the current time and makes sure a request can not be sent after 30 secs + payload.AuthTime = time.Now().UnixMilli() + + key := payload.XPub + if key == "" && payload.AccessKey != "" { + key = payload.AccessKey + } + + // Signature, using bitcoin signMessage + var err error + if payload.Signature, err = bitcoin.SignMessage( + hex.EncodeToString(privateKey.Serialise()), + getSigningMessage(key, payload), + true, + ); err != nil { + return nil, err + } + + return payload, nil +} + +// getSigningMessage will build the signing message string +func getSigningMessage(xPub string, auth *models.AuthPayload) string { + return fmt.Sprintf("%s%s%s%d", xPub, auth.AuthHash, auth.AuthNonce, auth.AuthTime) +} + +func setSignatureHeaders(header *http.Header, authData *models.AuthPayload) ResponseError { + // Create the auth header hash + header.Set(models.AuthHeaderHash, authData.AuthHash) + + // Set the nonce + header.Set(models.AuthHeaderNonce, authData.AuthNonce) + + // Set the time + header.Set(models.AuthHeaderTime, fmt.Sprintf("%d", authData.AuthTime)) + + // Set the signature + header.Set(models.AuthSignature, authData.Signature) + + return nil +} diff --git a/walletclient_configurator.go b/client.go similarity index 100% rename from walletclient_configurator.go rename to client.go diff --git a/client_options.go b/client_options.go index 155e5fd..9f34b2f 100644 --- a/client_options.go +++ b/client_options.go @@ -1,74 +1,10 @@ package walletclient import ( + "fmt" "net/http" - - "github.com/bitcoin-sv/spv-wallet-go-client/transports" ) -// // WithXPriv will set xPrivString on the client -// func WithXPriv(xPrivString string) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.xPrivString = xPrivString -// } -// } -// } - -// // WithXPub will set xPubString on the client -// func WithXPub(xPubString string) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.xPubString = xPubString -// } -// } -// } - -// WithAccessKey will set the access key on the client -// func WithAccessKey(accessKeyString string) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.accessKeyString = accessKeyString -// } -// } -// } - -// // WithHTTP will overwrite the default client with a custom client -// func WithHTTP(serverURL string) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.transportOptions = append(c.transportOptions, transports.WithHTTP(serverURL)) -// } -// } -// } - -// // WithHTTPClient will overwrite the default client with a custom client -// func WithHTTPClient(serverURL string, httpClient *http.Client) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.transportOptions = append(c.transportOptions, transports.WithHTTPClient(serverURL, httpClient)) -// } -// } -// } - -// // WithAdminKey will set the admin key for admin requests -// func WithAdminKey(adminKey string) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.transportOptions = append(c.transportOptions, transports.WithAdminKey(adminKey)) -// } -// } -// } - -// // WithSignRequest will set whether to sign all requests -// func WithSignRequest(signRequest bool) ClientOps { -// return func(c *WalletClient) { -// if c != nil { -// c.transportOptions = append(c.transportOptions, transports.WithSignRequest(signRequest)) -// } -// } -// } - // WalletClientConfigurator is the interface for configuring WalletClient type WalletClientConfigurator interface { Configure(c *WalletClient) @@ -79,87 +15,56 @@ type WithXPriv struct { XPrivString string } -// NewConfigWithXPriv creates new configuration for configurator with xpriv key -func NewConfigWithXPriv(xPrivString string) *WithXPriv { - return &WithXPriv{xPrivString} -} - -// WithXPriv will set xPrivString on the client func (w *WithXPriv) Configure(c *WalletClient) { + fmt.Printf("withXpriv configure: %#v\n", w) c.xPrivString = w.XPrivString } -// WithHTTP sets the URL for the HTTP transport of a WalletClient -type WithHTTP struct { - ServerURL string -} - -func (w *WithHTTP) Configure(c *WalletClient) { - if c.transportOptions == nil { - c.transportOptions = []transports.ClientOps{} - } - c.transportOptions = append(c.transportOptions, transports.WithHTTP(w.ServerURL)) -} - -// WithAdminKey sets the admin key for creating new xpubs -type WithAdminKey struct { - AdminKeyString string -} - -// WithAdminKey will set the admin key for admin requests -func (w *WithAdminKey) Configure(c *WalletClient) { - if c.transportOptions == nil { - c.transportOptions = []transports.ClientOps{} - } - c.transportOptions = append(c.transportOptions, transports.WithAdminKey(w.AdminKeyString)) -} - -// WithSignRequest configures whether to sign HTTP requests -type WithSignRequest struct { - Sign bool -} - -// WithSignRequest will set whether to sign all requests -func (w *WithSignRequest) Configure(c *WalletClient) { - if c.transportOptions == nil { - c.transportOptions = []transports.ClientOps{} - } - c.transportOptions = append(c.transportOptions, transports.WithSignRequest(w.Sign)) -} - // WithXPub sets the xPubString on the client type WithXPub struct { XPubString string } -// WithXPub will set xPubString on the client func (w *WithXPub) Configure(c *WalletClient) { c.xPubString = w.XPubString } -// WithAccessKey sets the accessKeyString on the client. +// WithAccessKey sets the accessKeyString on the client type WithAccessKey struct { AccessKeyString string } -// WithAccessKey will set the access key on the client func (w *WithAccessKey) Configure(c *WalletClient) { c.accessKeyString = w.AccessKeyString } -// WithHTTPClient sets a custom HTTP client and server URL for the transport of a WalletClient. -type WithHTTPClient struct { +// WithAdminKey sets the admin key for creating new xpubs +type WithAdminKey struct { + AdminKeyString string +} + +func (w *WithAdminKey) Configure(c *WalletClient) { + fmt.Printf("withAdminKey configure: %#v\n", w) + fmt.Printf("withAdminKey configure adminxpriv: %v \n", w.AdminKeyString) + c.adminXPriv = w.AdminKeyString +} + +// WithHTTP sets the URL and HTTP client of a WalletClient +type WithHTTP struct { ServerURL string HTTPClient *http.Client } -// WithHTTPClient will overwrite the default client with a custom client -func (w *WithHTTPClient) Configure(c *WalletClient) { - if c != nil { - if c.transportOptions == nil { - c.transportOptions = []transports.ClientOps{} - } - // Append the custom HTTP client configuration to the transport options - c.transportOptions = append(c.transportOptions, transports.WithHTTPClient(w.ServerURL, w.HTTPClient)) - } +func (w *WithHTTP) Configure(c *WalletClient) { + c.server = w.ServerURL + c.httpClient = w.HTTPClient +} + +// WithSignRequest configures whether to sign HTTP requests +type WithSignRequest struct { + Sign bool +} + +func (w *WithSignRequest) Configure(c *WalletClient) { + c.signRequest = w.Sign } diff --git a/config.go b/config.go new file mode 100644 index 0000000..99f0c4b --- /dev/null +++ b/config.go @@ -0,0 +1,97 @@ +package walletclient + +import "github.com/bitcoin-sv/spv-wallet/models" + +// TransportType the type of transport being used ('http' for usage or 'mock' for testing) +type TransportType string + +// SPVWalletUserAgent the spv wallet user agent sent to the spv wallet. +const SPVWalletUserAgent = "SPVWallet: go-client" + +const ( + // SPVWalletTransportHTTP uses the http transport for all spv-wallet actions + SPVWalletTransportHTTP TransportType = "http" + + // SPVWalletTransportMock uses the mock transport for all spv-wallet actions + SPVWalletTransportMock TransportType = "mock" +) + +// Recipients is a struct for recipients +type Recipients struct { + OpReturn *models.OpReturn `json:"op_return"` + Satoshis uint64 `json:"satoshis"` + To string `json:"to"` +} + +// QueryParams object to use when limiting and sorting database query results +type QueryParams struct { + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` + OrderByField string `json:"order_by_field,omitempty"` + SortDirection string `json:"sort_direction,omitempty"` +} + +const ( + // FieldMetadata is the field name for metadata + FieldMetadata = "metadata" + + // FieldQueryParams is the field name for the query params + FieldQueryParams = "params" + + // FieldXpubKey is the field name for xpub key + FieldXpubKey = "key" + + // FieldXpubID is the field name for xpub id + FieldXpubID = "xpub_id" + + // FieldAddress is the field name for paymail address + FieldAddress = "address" + + // FieldPublicName is the field name for (paymail) public name + FieldPublicName = "public_name" + + // FieldAvatar is the field name for (paymail) avatar + FieldAvatar = "avatar" + + // FieldConditions is the field name for conditions + FieldConditions = "conditions" + + // FieldTo is the field name for "to" + FieldTo = "to" + + // FieldSatoshis is the field name for "satoshis" + FieldSatoshis = "satoshis" + + // FieldOpReturn is the field name for "op_return" + FieldOpReturn = "op_return" + + // FieldConfig is the field name for "config" + FieldConfig = "config" + + // FieldOutputs is the field name for "outputs" + FieldOutputs = "outputs" + + // FieldHex is the field name for "hex" + FieldHex = "hex" + + // FieldReferenceID is the field name for "reference_id" + FieldReferenceID = "reference_id" + + // FieldID is the id field for most models + FieldID = "id" + + // FieldLockingScript is the field for locking script + FieldLockingScript = "locking_script" + + // FieldUserAgent is the field for storing the user agent + FieldUserAgent = "user_agent" + + // FieldTransactionConfig is the field for the config of a new transaction + FieldTransactionConfig = "transaction_config" + + // FieldTransactionID is the field for transaction ID + FieldTransactionID = "tx_id" + + // FieldOutputIndex is the field for "output_index" + FieldOutputIndex = "output_index" +) diff --git a/contacts.go b/contacts.go index e1e47e5..8121a7d 100644 --- a/contacts.go +++ b/contacts.go @@ -1,50 +1,40 @@ package walletclient -import ( - "context" - "errors" - "fmt" - - "github.com/bitcoin-sv/spv-wallet/models" - - "github.com/bitcoin-sv/spv-wallet-go-client/transports" -) - -// 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) -} - -// 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 try to confirm the contact -func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) transports.ResponseError { - isTotpValid, err := b.ValidateTotpForContact(contact, passcode, requesterPaymail, period, 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 -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) -} +// // 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) +// } + +// // 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 try to confirm the contact +// func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) transports.ResponseError { +// isTotpValid, err := b.ValidateTotpForContact(contact, passcode, requesterPaymail, period, 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 +// 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/destinations.go b/destinations.go index a0fd6c0..7fc22b0 100644 --- a/destinations.go +++ b/destinations.go @@ -1,58 +1,51 @@ package walletclient -import ( - "context" - - "github.com/bitcoin-sv/spv-wallet-go-client/transports" - "github.com/bitcoin-sv/spv-wallet/models" -) - -// GetDestinationByID gets the destination by id -func (b *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, transports.ResponseError) { - return b.transport.GetDestinationByID(ctx, id) -} - -// GetDestinationByAddress gets the destination by address -func (b *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, transports.ResponseError) { - return b.transport.GetDestinationByAddress(ctx, address) -} - -// GetDestinationByLockingScript gets the destination by locking script -func (b *WalletClient) GetDestinationByLockingScript(ctx context.Context, - lockingScript string, -) (*models.Destination, transports.ResponseError) { - return b.transport.GetDestinationByLockingScript(ctx, lockingScript) -} - -// GetDestinations gets all destinations that match the metadata filter -func (b *WalletClient) GetDestinations(ctx context.Context, - metadataConditions *models.Metadata, -) ([]*models.Destination, transports.ResponseError) { - return b.transport.GetDestinations(ctx, metadataConditions) -} - -// NewDestination create a new destination and return it -func (b *WalletClient) NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, transports.ResponseError) { - return b.transport.NewDestination(ctx, metadata) -} - -// UpdateDestinationMetadataByID updates the destination metadata by id -func (b *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, - metadata *models.Metadata, -) (*models.Destination, transports.ResponseError) { - return b.transport.UpdateDestinationMetadataByID(ctx, id, metadata) -} - -// UpdateDestinationMetadataByAddress updates the destination metadata by address -func (b *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, - metadata *models.Metadata, -) (*models.Destination, transports.ResponseError) { - return b.transport.UpdateDestinationMetadataByAddress(ctx, address, metadata) -} - -// UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script -func (b *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, - metadata *models.Metadata, -) (*models.Destination, transports.ResponseError) { - return b.transport.UpdateDestinationMetadataByLockingScript(ctx, lockingScript, metadata) -} +// // GetDestinationByID gets the destination by id +// func (b *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, transports.ResponseError) { +// return b.transport.GetDestinationByID(ctx, id) +// } + +// // GetDestinationByAddress gets the destination by address +// func (b *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, transports.ResponseError) { +// return b.transport.GetDestinationByAddress(ctx, address) +// } + +// // GetDestinationByLockingScript gets the destination by locking script +// func (b *WalletClient) GetDestinationByLockingScript(ctx context.Context, +// lockingScript string, +// ) (*models.Destination, transports.ResponseError) { +// return b.transport.GetDestinationByLockingScript(ctx, lockingScript) +// } + +// // GetDestinations gets all destinations that match the metadata filter +// func (b *WalletClient) GetDestinations(ctx context.Context, +// metadataConditions *models.Metadata, +// ) ([]*models.Destination, transports.ResponseError) { +// return b.transport.GetDestinations(ctx, metadataConditions) +// } + +// // NewDestination create a new destination and return it +// func (b *WalletClient) NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, transports.ResponseError) { +// return b.transport.NewDestination(ctx, metadata) +// } + +// // UpdateDestinationMetadataByID updates the destination metadata by id +// func (b *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, +// metadata *models.Metadata, +// ) (*models.Destination, transports.ResponseError) { +// return b.transport.UpdateDestinationMetadataByID(ctx, id, metadata) +// } + +// // UpdateDestinationMetadataByAddress updates the destination metadata by address +// func (b *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, +// metadata *models.Metadata, +// ) (*models.Destination, transports.ResponseError) { +// return b.transport.UpdateDestinationMetadataByAddress(ctx, address, metadata) +// } + +// // UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script +// func (b *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, +// metadata *models.Metadata, +// ) (*models.Destination, transports.ResponseError) { +// return b.transport.UpdateDestinationMetadataByLockingScript(ctx, lockingScript, metadata) +// } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..e4e22c8 --- /dev/null +++ b/errors.go @@ -0,0 +1,72 @@ +package walletclient + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +// ErrAdminKey admin key not set +var ErrAdminKey = errors.New("an admin key must be set to be able to create an xpub") + +// ErrNoClientSet is when no client is set +var ErrNoClientSet = errors.New("no transport client set") + +// ResError is a struct which contain information about error +type ResError struct { + StatusCode int + Message string +} + +// ResponseError is an interface for error +type ResponseError interface { + Error() string + GetStatusCode() int +} + +// WrapError wraps an error into ResponseError +func WrapError(err error) ResponseError { + if err == nil { + return nil + } + + return &ResError{ + StatusCode: http.StatusInternalServerError, + Message: err.Error(), + } +} + +// WrapResponseError wraps a http response into ResponseError +func WrapResponseError(res *http.Response) ResponseError { + if res == nil { + return nil + } + + var errorMsg string + + err := json.NewDecoder(res.Body).Decode(&errorMsg) + if err != nil { + // if EOF, then body is empty and we return response status as error message + if !errors.Is(err, io.EOF) { + errorMsg = fmt.Sprintf("spv-wallet error message can't be decoded. Reason: %s", err.Error()) + } + errorMsg = res.Status + } + + return &ResError{ + StatusCode: res.StatusCode, + Message: errorMsg, + } +} + +// Error returns the error message +func (e *ResError) Error() string { + return e.Message +} + +// GetStatusCode returns the status code of error +func (e *ResError) GetStatusCode() int { + return e.StatusCode +} diff --git a/examples/tt/admin.go b/examples/tt/admin.go new file mode 100644 index 0000000..1bc8b22 --- /dev/null +++ b/examples/tt/admin.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/bitcoin-sv/spv-wallet/models" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" +) + +const serverURL = "http://localhost:3003/v1" +const adminKey = "xprv9s21ZrQH143K3CbJXirfrtpLvhT3Vgusdo8coBritQ3rcS7Jy7sxWhatuxG5h2y1Cqj8FKmPp69536gmjYRpfga2MJdsGyBsnB12E19CESK" + +func main() { + clientAdmin, err := setupAdminClient() + if err != nil { + log.Fatalf("Failed to setup admin client: %v", err) + } + log.Println("is sign req: ", clientAdmin.IsSignRequest()) + + ctx := context.Background() + + // Example of creating users and interacting with wallet functions + handleUsers(ctx, clientAdmin) +} + +func setupAdminClient() (*walletclient.WalletClient, error) { + + // Set up a client with administrative privileges + return walletclient.New( + &walletclient.WithXPriv{XPrivString: adminKey}, + &walletclient.WithAdminKey{AdminKeyString: adminKey}, + &walletclient.WithHTTP{ServerURL: serverURL}, + &walletclient.WithSignRequest{Sign: true}, + ) +} + +func handleUsers(ctx context.Context, clientAdmin *walletclient.WalletClient) { + aliceName, bobName := "alice", "bob" + aliceClient, _, err := createUser(ctx, aliceName, clientAdmin) + if err != nil { + log.Fatalf("Failed to create user %s: %v", aliceName, err) + } + bobClient, bobPaymail, err := createUser(ctx, bobName, clientAdmin) + if err != nil { + log.Fatalf("Failed to create user %s: %v", bobName, err) + } + + fmt.Println() + log.Println(" **** Admin Get Contacts ****") + c, err := clientAdmin.AdminGetContacts(ctx, nil, nil, &walletclient.QueryParams{}) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(c) == 0 { + log.Printf("empty list of contacts") + return + } + log.Printf("admin got contacts: %#v\n", c) + + fmt.Println() + log.Println(" **** Admin Update Contact ****") + bobToUpdate := c[0] + log.Printf("Bob before update [%v]", c[0].FullName) + updatedContact, err := clientAdmin.AdminUpdateContact(ctx, bobToUpdate.ID, fmt.Sprintf("%s %s", bobName, time.Now().Local().String()), nil) + if err != nil { + log.Panicf("error updating contact id: [%s] name: [%s] - %s", bobToUpdate.ID, bobToUpdate.FullName, err) + } + log.Printf("updated contact full name [%v]", updatedContact.FullName) + + fmt.Println() + log.Println(" **** Admin Reject Contact ****") + aliceContacts, err := bobClient.GetContacts(ctx, nil, nil, nil) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(aliceContacts) == 0 { + log.Printf("empty list of contacts") + return + } + log.Printf("status should be awaiting == [%v]", aliceContacts[0].Status) + + rejectAliceContact, err := clientAdmin.AdminRejectContact(ctx, aliceContacts[0].ID) + if err != nil { + log.Panicf("contact not accepted id: %v - %s", aliceContacts[0].ID, err) + } + log.Printf("status should change to rejected == [%v]", rejectAliceContact.Status) + + fmt.Println() + log.Println(" **** Admin Accept Contact ****") + if _, err := aliceClient.UpsertContact(ctx, bobPaymail, bobName, nil, ""); err != nil { + panic(err) + } + + aliceContacts, err = bobClient.GetContacts(ctx, nil, nil, nil) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(aliceContacts) == 0 { + log.Printf("empty list of contacts") + return + } + + log.Printf("status should be awaiting == [%v]", aliceContacts[0].Status) + acceptAliceContact, err := clientAdmin.AdminAcceptContact(ctx, aliceContacts[0].ID) + if err != nil { + log.Panicf("contact not accepted id: %v - %s", aliceContacts[0].ID, err) + } + log.Printf("status should change to unconfirmed == [%v]", acceptAliceContact.Status) + + fmt.Println() + log.Println(" **** Admin Delete Contact ****") + + err = clientAdmin.AdminDeleteContact(ctx, aliceContacts[0].ID) + if err != nil { + log.Panicf("contact not accepted id: %v - %s", aliceContacts[0].ID, err) + } + if len(aliceContacts) > 0 { + log.Println("removed id:", aliceContacts[0].ID) + } else { + log.Println("alice contacts are empty") + } + aliceContacts, err = bobClient.GetContacts(ctx, nil, nil, nil) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(aliceContacts) == 0 { + log.Printf("empty list of contacts") + } + for _, c := range aliceContacts { + log.Println("alice contacts id:", c.ID, c.FullName, c.DeletedAt) + } +} + +func createUser(ctx context.Context, name string, adminClient *walletclient.WalletClient) (*walletclient.WalletClient, string, error) { + keys, err := xpriv.Generate() + if err != nil { + return nil, "", fmt.Errorf("failed to generate keys: %v", err) + } + + timestamp := time.Now().UnixMicro() + examplePaymail := fmt.Sprintf("contacttest_%d_%s@auggie.4chain.space", timestamp, name) + + metadata := make(models.Metadata) + metadata["name"] = name + + if err := adminClient.AdminNewXpub(ctx, keys.XPub().String(), &metadata); err != nil { + return nil, "", fmt.Errorf("failed to create new xpub: %v", err) + } + if _, err := adminClient.AdminCreatePaymail(ctx, keys.XPub().String(), examplePaymail, name, ""); err != nil { + return nil, "", fmt.Errorf("failed to create paymail: %v", err) + } + + userClient, err := walletclient.New( + &walletclient.WithXPriv{XPrivString: keys.XPriv()}, + &walletclient.WithHTTP{ServerURL: serverURL}, + &walletclient.WithSignRequest{Sign: true}, + ) + if err != nil { + return nil, "", fmt.Errorf("failed to create user client: %v", err) + } + + return userClient, examplePaymail, nil +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..23bc180 --- /dev/null +++ b/http.go @@ -0,0 +1,704 @@ +package walletclient + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/apierrors" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/bip32" + + "github.com/bitcoin-sv/spv-wallet-go-client/utils" +) + +type transportHTTP struct { + accessKey *bec.PrivateKey + adminXPriv *bip32.ExtendedKey + httpClient *http.Client + server string + signRequest bool + xPriv *bip32.ExtendedKey + xPub *bip32.ExtendedKey +} + +// SetSignRequest turn the signing of the http request on or off +func (wc *WalletClient) SetSignRequest(signRequest bool) { + wc.signRequest = signRequest +} + +// IsSignRequest return whether to sign all requests +func (wc *WalletClient) IsSignRequest() bool { + return wc.signRequest +} + +// SetAdminKey set the admin key +func (wc *WalletClient) SetAdminKey(adminKey *bip32.ExtendedKey) { + wc.adminXPriv = adminKey +} + +// GetXPub will get the xpub of the current xpub +func (wc *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, ResponseError) { + var xPub models.Xpub + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/xpub", nil, wc.xPriv, true, &xPub, + ); err != nil { + return nil, err + } + + return &xPub, nil +} + +// UpdateXPubMetadata update the metadata of the logged in xpub +func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + + var xPub models.Xpub + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/xpub", jsonStr, wc.xPriv, true, &xPub, + ); err != nil { + return nil, err + } + + return &xPub, nil +} + +// GetAccessKey will get an access key by id +func (wc *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { + var accessKey models.AccessKey + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, + ); err != nil { + return nil, err + } + + return &accessKey, nil +} + +// GetAccessKeys will get all access keys matching the metadata filter +func (wc *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadataConditions), + }) + if err != nil { + return nil, WrapError(err) + } + var accessKey []*models.AccessKey + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/access-key/search", jsonStr, wc.xPriv, true, &accessKey, + ); err != nil { + return nil, err + } + + return accessKey, nil +} + +// GetAccessKeysCount will get the count of access keys +func (wc *WalletClient) GetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/access-key/count", jsonStr, wc.xPriv, true, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// RevokeAccessKey will revoke an access key by id +func (wc *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { + var accessKey models.AccessKey + if err := wc.doHTTPRequest( + ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, + ); err != nil { + return nil, err + } + + return &accessKey, nil +} + +// CreateAccessKey will create new access key +func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + var accessKey models.AccessKey + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/access-key", jsonStr, wc.xPriv, true, &accessKey, + ); err != nil { + return nil, err + } + + return &accessKey, nil +} + +// GetDestinationByID will get a destination by id +func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, ResponseError) { + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// GetDestinationByAddress will get a destination by address +func (wc *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, ResponseError) { + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// GetDestinationByLockingScript will get a destination by locking script +func (wc *WalletClient) GetDestinationByLockingScript(ctx context.Context, lockingScript string) (*models.Destination, ResponseError) { + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// GetDestinations will get all destinations matching the metadata filter +func (wc *WalletClient) GetDestinations(ctx context.Context, metadataConditions *models.Metadata) ([]*models.Destination, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadataConditions), + }) + if err != nil { + return nil, WrapError(err) + } + var destinations []*models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/destination/search", jsonStr, wc.xPriv, true, &destinations, + ); err != nil { + return nil, err + } + + return destinations, nil +} + +// GetDestinationsCount will get the count of destinations matching the metadata filter +func (wc *WalletClient) GetDestinationsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/destination/count", jsonStr, wc.xPriv, true, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// NewDestination will create a new destination and return it +func (wc *WalletClient) NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/destination", jsonStr, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// UpdateDestinationMetadataByID updates the destination metadata by id +func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, + metadata *models.Metadata, +) (*models.Destination, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldID: id, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// UpdateDestinationMetadataByAddress updates the destination metadata by address +func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, + metadata *models.Metadata, +) (*models.Destination, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script +func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, + metadata *models.Metadata, +) (*models.Destination, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldLockingScript: lockingScript, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + + var destination models.Destination + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, + ); err != nil { + return nil, err + } + + return &destination, nil +} + +// GetTransactions will get transactions by conditions +func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, + metadataConditions *models.Metadata, queryParams *QueryParams, +) ([]*models.Transaction, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadataConditions), + FieldQueryParams: queryParams, + }) + if err != nil { + return nil, WrapError(err) + } + + var transactions []*models.Transaction + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/transaction/search", jsonStr, wc.xPriv, wc.signRequest, &transactions, + ); err != nil { + return nil, err + } + + return transactions, nil +} + +// GetTransactionsCount get number of user transactions +func (wc *WalletClient) GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/transaction/count", jsonStr, wc.xPriv, wc.signRequest, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// DraftToRecipients is a draft transaction to a slice of recipients +func (wc *WalletClient) DraftToRecipients(ctx context.Context, recipients []*Recipients, + metadata *models.Metadata, +) (*models.DraftTransaction, ResponseError) { + outputs := make([]map[string]interface{}, 0) + for _, recipient := range recipients { + outputs = append(outputs, map[string]interface{}{ + FieldTo: recipient.To, + FieldSatoshis: recipient.Satoshis, + FieldOpReturn: recipient.OpReturn, + }) + } + + return wc.createDraftTransaction(ctx, map[string]interface{}{ + FieldConfig: map[string]interface{}{ + FieldOutputs: outputs, + }, + FieldMetadata: processMetadata(metadata), + }) +} + +// DraftTransaction is a draft transaction +func (wc *WalletClient) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, + metadata *models.Metadata, +) (*models.DraftTransaction, ResponseError) { + return wc.createDraftTransaction(ctx, map[string]interface{}{ + FieldConfig: transactionConfig, + FieldMetadata: processMetadata(metadata), + }) +} + +// createDraftTransaction will create a draft transaction +func (wc *WalletClient) createDraftTransaction(ctx context.Context, + jsonData map[string]interface{}, +) (*models.DraftTransaction, ResponseError) { + jsonStr, err := json.Marshal(jsonData) + if err != nil { + return nil, WrapError(err) + } + + var draftTransaction *models.DraftTransaction + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/transaction", jsonStr, wc.xPriv, true, &draftTransaction, + ); err != nil { + return nil, err + } + if draftTransaction == nil { + return nil, WrapError(apierrors.ErrDraftNotFound) + } + + return draftTransaction, nil +} + +// RecordTransaction will record a transaction +func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID string, + metadata *models.Metadata, +) (*models.Transaction, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldHex: hex, + FieldReferenceID: referenceID, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + + var transaction models.Transaction + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/transaction/record", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ); err != nil { + return nil, err + } + + return &transaction, nil +} + +// UpdateTransactionMetadata update the metadata of a transaction +func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID string, + metadata *models.Metadata, +) (*models.Transaction, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldID: txID, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + + var transaction models.Transaction + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/transaction", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ); err != nil { + return nil, err + } + + return &transaction, nil +} + +// SetSignatureFromAccessKey will set the signature on the header for the request from an access key +func SetSignatureFromAccessKey(header *http.Header, privateKeyHex, bodyString string) ResponseError { + // Create the signature + authData, err := createSignatureAccessKey(privateKeyHex, bodyString) + if err != nil { + return WrapError(err) + } + + // Set the auth header + header.Set(models.AuthAccessKey, authData.AccessKey) + + return setSignatureHeaders(header, authData) +} + +// GetUtxo will get a utxo by transaction ID +func (wc *WalletClient) GetUtxo(ctx context.Context, txID string, outputIndex uint32) (*models.Utxo, ResponseError) { + outputIndexStr := strconv.FormatUint(uint64(outputIndex), 10) + + url := fmt.Sprintf("/utxo?%s=%s&%s=%s", FieldTransactionID, txID, FieldOutputIndex, outputIndexStr) + + var utxo models.Utxo + if err := wc.doHTTPRequest( + ctx, http.MethodGet, url, nil, wc.xPriv, true, &utxo, + ); err != nil { + return nil, err + } + + return &utxo, nil +} + +// GetUtxos will get a list of utxos filtered by conditions and metadata +func (wc *WalletClient) GetUtxos(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Utxo, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + FieldQueryParams: queryParams, + }) + if err != nil { + return nil, WrapError(err) + } + + var utxos []*models.Utxo + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/utxo/search", jsonStr, wc.xPriv, wc.signRequest, &utxos, + ); err != nil { + return nil, err + } + + return utxos, nil +} + +// GetUtxosCount will get the count of utxos filtered by conditions and metadata +func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/utxo/count", jsonStr, wc.xPriv, wc.signRequest, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// createSignatureAccessKey will create a signature for the given access key & body contents +func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *models.AuthPayload, err error) { + // No key? + if privateKeyHex == "" { + err = apierrors.ErrMissingAccessKey + return + } + + var privateKey *bec.PrivateKey + if privateKey, err = bitcoin.PrivateKeyFromString( + privateKeyHex, + ); err != nil { + return + } + publicKey := privateKey.PubKey() + + // Get the xPub + payload = new(models.AuthPayload) + payload.AccessKey = hex.EncodeToString(publicKey.SerialiseCompressed()) + + // auth_nonce is a random unique string to seed the signing message + // this can be checked server side to make sure the request is not being replayed + payload.AuthNonce, err = utils.RandomHex(32) + if err != nil { + return nil, err + } + + return createSignatureCommon(payload, bodyString, privateKey) +} + +// doHTTPRequest will create and submit the HTTP request +func (wc *WalletClient) doHTTPRequest(ctx context.Context, method string, path string, + rawJSON []byte, xPriv *bip32.ExtendedKey, sign bool, responseJSON interface{}, +) ResponseError { + req, err := http.NewRequestWithContext(ctx, method, wc.server+path, bytes.NewBuffer(rawJSON)) + if err != nil { + return WrapError(err) + } + req.Header.Set("Content-Type", "application/json") + + if xPriv != nil { + err := wc.authenticateWithXpriv(sign, req, xPriv, rawJSON) + if err != nil { + return err + } + } else { + err := wc.authenticateWithAccessKey(req, rawJSON) + if err != nil { + return err + } + } + + var resp *http.Response + defer func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }() + if resp, err = wc.httpClient.Do(req); err != nil { + return WrapError(err) + } + if resp.StatusCode >= http.StatusBadRequest { + return WrapResponseError(resp) + } + + if responseJSON == nil { + return nil + } + + err = json.NewDecoder(resp.Body).Decode(&responseJSON) + if err != nil { + return WrapError(err) + } + return nil +} + +func (wc *WalletClient) authenticateWithXpriv(sign bool, req *http.Request, xPriv *bip32.ExtendedKey, rawJSON []byte) ResponseError { + if sign { + if err := addSignature(&req.Header, xPriv, string(rawJSON)); err != nil { + return err + } + } else { + var xPub string + xPub, err := bitcoin.GetExtendedPublicKey(xPriv) + if err != nil { + return WrapError(err) + } + req.Header.Set(models.AuthHeader, xPub) + req.Header.Set("", xPub) + } + return nil +} + +func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []byte) ResponseError { + return SetSignatureFromAccessKey(&req.Header, hex.EncodeToString(wc.accessKey.Serialise()), string(rawJSON)) +} + +// AcceptContact will accept the contact associated with the paymail +func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) ResponseError { + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ); err != nil { + return err + } + + return nil +} + +// RejectContact will reject the contact associated with the paymail +func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) ResponseError { + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ); err != nil { + return err + } + + return nil +} + +// ConfirmContact will confirm the contact associated with the paymail +func (wc *WalletClient) ConfirmContact(ctx context.Context, paymail string) ResponseError { + if err := wc.doHTTPRequest( + ctx, http.MethodPatch, "/contact/confirmed/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ); err != nil { + return err + } + + return nil +} + +// GetContacts will get contacts by conditions +func (wc *WalletClient) 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 := wc.doHTTPRequest( + ctx, http.MethodPost, "/contact/search", jsonStr, wc.xPriv, wc.signRequest, &result, + ); err != nil { + return nil, err + } + + 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 (wc *WalletClient) 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 := wc.doHTTPRequest( + ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.xPriv, wc.signRequest, &result, + ); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/http_admin.go b/http_admin.go new file mode 100644 index 0000000..7840a72 --- /dev/null +++ b/http_admin.go @@ -0,0 +1,371 @@ +package walletclient + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/bitcoin-sv/spv-wallet/models" +) + +// AdminNewXpub will register an xPub +func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError { + // Adding a xpub needs to be signed by an admin key + fmt.Printf("here1 %+v\n\n", wc) + if wc.adminXPriv == nil { + return WrapError(ErrAdminKey) + } + fmt.Println("here") + + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadata), + FieldXpubKey: rawXPub, + }) + if err != nil { + return WrapError(err) + } + + var xPubData models.Xpub + + return wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/xpub", jsonStr, wc.adminXPriv, true, &xPubData, + ) +} + +// AdminGetStatus get whether admin key is valid +func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError) { + var status bool + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/status", nil, wc.xPriv, true, &status, + ); err != nil { + return false, err + } + + return status, nil +} + +// AdminGetStats get admin stats +func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) { + var stats *models.AdminStats + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/stats", nil, wc.xPriv, true, &stats, + ); err != nil { + return nil, err + } + + return stats, nil +} + +// AdminGetAccessKeys get all access keys filtered by conditions +func (wc *WalletClient) AdminGetAccessKeys(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.AccessKey, ResponseError) { + var models []*models.AccessKey + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/access-keys/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetAccessKeysCount get a count of all the access keys filtered by conditions +func (wc *WalletClient) AdminGetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/access-keys/count") +} + +// AdminGetBlockHeaders get all block headers filtered by conditions +func (wc *WalletClient) AdminGetBlockHeaders(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.BlockHeader, ResponseError) { + var models []*models.BlockHeader + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions +func (wc *WalletClient) AdminGetBlockHeadersCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/block-headers/count") +} + +// AdminGetDestinations get all block destinations filtered by conditions +func (wc *WalletClient) AdminGetDestinations(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Destination, ResponseError) { + var models []*models.Destination + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/destinations/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetDestinationsCount get a count of all the destinations filtered by conditions +func (wc *WalletClient) AdminGetDestinationsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/destinations/count") +} + +// AdminGetPaymail get a paymail by address +func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + }) + if err != nil { + return nil, WrapError(err) + } + + var model *models.PaymailAddress + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.xPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminGetPaymails get all block paymails filtered by conditions +func (wc *WalletClient) AdminGetPaymails(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.PaymailAddress, ResponseError) { + var models []*models.PaymailAddress + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/paymails/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetPaymailsCount get a count of all the paymails filtered by conditions +func (wc *WalletClient) AdminGetPaymailsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/paymails/count") +} + +// AdminCreatePaymail create a new paymail for a xpub +func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldXpubKey: rawXPub, + FieldAddress: address, + FieldPublicName: publicName, + FieldAvatar: avatar, + }) + if err != nil { + return nil, WrapError(err) + } + + var model *models.PaymailAddress + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.xPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminDeletePaymail delete a paymail address from the database +func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) ResponseError { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + }) + if err != nil { + return WrapError(err) + } + + if err := wc.doHTTPRequest( + ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.xPriv, true, nil, + ); err != nil { + return err + } + + return nil +} + +// AdminGetTransactions get all block transactions filtered by conditions +func (wc *WalletClient) AdminGetTransactions(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Transaction, ResponseError) { + var models []*models.Transaction + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/transactions/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetTransactionsCount get a count of all the transactions filtered by conditions +func (wc *WalletClient) AdminGetTransactionsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/transactions/count") +} + +// AdminGetUtxos get all block utxos filtered by conditions +func (wc *WalletClient) AdminGetUtxos(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Utxo, ResponseError) { + var models []*models.Utxo + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/utxos/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetUtxosCount get a count of all the utxos filtered by conditions +func (wc *WalletClient) AdminGetUtxosCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/utxos/count") +} + +// AdminGetXPubs get all block xpubs filtered by conditions +func (wc *WalletClient) AdminGetXPubs(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Xpub, ResponseError) { + var models []*models.Xpub + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/xpubs/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetXPubsCount get a count of all the xpubs filtered by conditions +func (wc *WalletClient) AdminGetXPubsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/xpubs/count") +} + +func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, path string, models interface{}, +) ResponseError { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + FieldQueryParams: queryParams, + }) + if err != nil { + return WrapError(err) + } + + if err := wc.doHTTPRequest( + ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &models, + ); err != nil { + return err + } + + return nil +} + +func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, path string) (int64, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// AdminRecordTransaction will record a transaction as an admin +func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldHex: hex, + }) + if err != nil { + return nil, WrapError(err) + } + + var transaction models.Transaction + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ); err != nil { + return nil, err + } + + return &transaction, nil +} + +// AdminGetSharedConfig gets the shared config +func (wc *WalletClient) AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) { + var model *models.SharedConfig + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/shared-config", nil, wc.xPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminGetContacts executes an HTTP POST request to search for contacts based on specified conditions, metadata, and query parameters. +func (wc *WalletClient) AdminGetContacts(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 contacts []*models.Contact + err = wc.doHTTPRequest(ctx, http.MethodPost, "/admin/contact/search", jsonStr, wc.adminXPriv, true, &contacts) + return contacts, WrapError(err) +} + +// AdminUpdateContact executes an HTTP PATCH request to update a specific contact's full name using their ID. +func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + "fullName": fullName, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + var contact models.Contact + err = wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/%s", id), jsonStr, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminDeleteContact executes an HTTP DELETE request to remove a contact using their ID. +func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) ResponseError { + err := wc.doHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/admin/contact/%s", id), nil, wc.adminXPriv, true, nil) + return WrapError(err) +} + +// AdminAcceptContact executes an HTTP PATCH request to mark a contact as accepted using their ID. +func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, ResponseError) { + var contact models.Contact + err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/accepted/%s", id), nil, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminRejectContact executes an HTTP PATCH request to mark a contact as rejected using their ID. +func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, ResponseError) { + var contact models.Contact + err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} diff --git a/transactions.go b/transactions.go index a4e256f..e5bffb4 100644 --- a/transactions.go +++ b/transactions.go @@ -1,82 +1,117 @@ package walletclient -import ( - "context" +// import ( +// "context" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// - "github.com/bitcoin-sv/spv-wallet-go-client/transports" - "github.com/bitcoin-sv/spv-wallet/models" -) +// "github.com/bitcoin-sv/spv-wallet/models" -// GetTransaction get a transaction by id -func (b *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, transports.ResponseError) { - return b.transport.GetTransaction(ctx, txID) -} +// "github.com/bitcoin-sv/spv-wallet-go-client/transports" +// ) -// GetTransactions get all transactions matching search criteria -func (b *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *transports.QueryParams, -) ([]*models.Transaction, transports.ResponseError) { - return b.transport.GetTransactions(ctx, conditions, metadata, queryParams) -} +// // GetTransaction get a transaction by id +// func (b *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, transports.ResponseError) { +// return b.transport.GetTransaction(ctx, txID) +// } -// GetTransactionsCount get number of user transactions -func (b *WalletClient) GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, transports.ResponseError) { - return b.transport.GetTransactionsCount(ctx, conditions, metadata) -} +// // GetTransactions get all transactions matching search criteria +// func (b *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, +// metadata *models.Metadata, queryParams *transports.QueryParams, +// ) ([]*models.Transaction, transports.ResponseError) { +// return b.transport.GetTransactions(ctx, conditions, metadata, queryParams) +// } -// DraftToRecipients initialize a new P2PKH draft transaction to a list of recipients -func (b *WalletClient) DraftToRecipients(ctx context.Context, recipients []*transports.Recipients, - metadata *models.Metadata, -) (*models.DraftTransaction, transports.ResponseError) { - return b.transport.DraftToRecipients(ctx, recipients, metadata) -} +// // GetTransactionsCount get number of user transactions +// func (b *WalletClient) GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, +// metadata *models.Metadata, +// ) (int64, transports.ResponseError) { +// return b.transport.GetTransactionsCount(ctx, conditions, metadata) +// } -// DraftTransaction initialize a new draft transaction -func (b *WalletClient) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, - metadata *models.Metadata, -) (*models.DraftTransaction, transports.ResponseError) { - return b.transport.DraftTransaction(ctx, transactionConfig, metadata) -} +// // DraftToRecipients initialize a new P2PKH draft transaction to a list of recipients +// func (b *WalletClient) DraftToRecipients(ctx context.Context, recipients []*transports.Recipients, +// metadata *models.Metadata, +// ) (*models.DraftTransaction, transports.ResponseError) { +// return b.transport.DraftToRecipients(ctx, recipients, metadata) +// } -// RecordTransaction record a new transaction -func (b *WalletClient) RecordTransaction(ctx context.Context, hex, draftID string, - metadata *models.Metadata, -) (*models.Transaction, transports.ResponseError) { - return b.transport.RecordTransaction(ctx, hex, draftID, metadata) -} +// // DraftTransaction initialize a new draft transaction +// func (b *WalletClient) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, +// metadata *models.Metadata, +// ) (*models.DraftTransaction, transports.ResponseError) { +// return b.transport.DraftTransaction(ctx, transactionConfig, metadata) +// } -// UpdateTransactionMetadata update the metadata of a transaction -func (b *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID string, - metadata *models.Metadata, -) (*models.Transaction, transports.ResponseError) { - return b.transport.UpdateTransactionMetadata(ctx, txID, metadata) -} +// // RecordTransaction record a new transaction +// func (b *WalletClient) RecordTransaction(ctx context.Context, hex, draftID string, +// metadata *models.Metadata, +// ) (*models.Transaction, transports.ResponseError) { +// return b.transport.RecordTransaction(ctx, hex, draftID, metadata) +// } -// FinalizeTransaction will finalize the transaction -func (b *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, transports.ResponseError) { - res, err := transports.GetSignedHex(draft, b.xPriv) - if err != nil { - return "", transports.WrapError(err) - } +// // UpdateTransactionMetadata update the metadata of a transaction +// func (b *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID string, +// metadata *models.Metadata, +// ) (*models.Transaction, transports.ResponseError) { +// return b.transport.UpdateTransactionMetadata(ctx, txID, metadata) +// } - return res, nil -} +// // FinalizeTransaction will finalize the transaction +// func (b *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, transports.ResponseError) { +// res, err := transports.GetSignedHex(draft, b.xPriv) +// if err != nil { +// return "", transports.WrapError(err) +// } -// SendToRecipients send to recipients -func (b *WalletClient) SendToRecipients(ctx context.Context, recipients []*transports.Recipients, - metadata *models.Metadata, -) (*models.Transaction, transports.ResponseError) { - draft, err := b.DraftToRecipients(ctx, recipients, metadata) - if err != nil { - return nil, err - } +// return res, nil +// } - var hex string - if hex, err = b.FinalizeTransaction(draft); err != nil { - return nil, err - } +// // SendToRecipients send to recipients +// func (b *WalletClient) SendToRecipients(ctx context.Context, recipients []*transports.Recipients, +// metadata *models.Metadata, +// ) (*models.Transaction, transports.ResponseError) { +// draft, err := b.DraftToRecipients(ctx, recipients, metadata) +// if err != nil { +// return nil, err +// } - return b.RecordTransaction(ctx, hex, draft.ID, metadata) -} +// var hex string +// if hex, err = b.FinalizeTransaction(draft); err != nil { +// return nil, err +// } + +// return b.RecordTransaction(ctx, hex, draft.ID, metadata) +// } diff --git a/walletclient.go b/walletclient.go index 74dc162..10ec25b 100644 --- a/walletclient.go +++ b/walletclient.go @@ -1,26 +1,28 @@ package walletclient import ( + "net/http" + + "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/libsv/go-bk/wif" "github.com/pkg/errors" - - "github.com/bitcoin-sv/spv-wallet-go-client/transports" ) // WalletClient is the spv wallet Go client representation. type WalletClient struct { - transports.TransportService - accessKey *bec.PrivateKey - accessKeyString string - transport transports.TransportService - transportOptions []transports.ClientOps - xPriv *bip32.ExtendedKey - xPrivString string - xPub *bip32.ExtendedKey - xPubString string + accessKeyString string + xPrivString string + xPubString string + accessKey *bec.PrivateKey + adminXPriv *bip32.ExtendedKey + httpClient *http.Client + server string + signRequest bool + xPriv *bip32.ExtendedKey + xPub *bip32.ExtendedKey } // New creates a new WalletClient using the provided configuration options. @@ -36,13 +38,6 @@ func New(configurators ...WalletClientConfigurator) (*WalletClient, error) { return nil, err } - // Setup transport based on initialized keys - if err := client.setupTransport(); err != nil { - return nil, err - } - - client.TransportService = client.transport - return client, nil } @@ -87,27 +82,17 @@ func (c *WalletClient) initializeAccessKey() error { return nil } -// setupTransport configures the transport service based on the available keys. -func (c *WalletClient) setupTransport() error { - var err error - transportOptions := make([]transports.ClientOps, 0) - - if c.xPriv != nil { - transportOptions = append(transportOptions, transports.WithXPriv(c.xPriv)) - transportOptions = append(transportOptions, transports.WithXPub(c.xPub)) - } else if c.xPub != nil { - transportOptions = append(transportOptions, transports.WithXPub(c.xPub)) - } else if c.accessKey != nil { - transportOptions = append(transportOptions, transports.WithAccessKey(c.accessKey)) - } - - if len(c.transportOptions) > 0 { - transportOptions = append(transportOptions, c.transportOptions...) +// processMetadata will process the metadata +func processMetadata(metadata *models.Metadata) *models.Metadata { + if metadata == nil { + m := make(models.Metadata) + metadata = &m } - if c.transport, err = transports.NewTransport(transportOptions...); err != nil { - return err - } + return metadata +} - return nil +// addSignature will add the signature to the request +func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) ResponseError { + return setSignature(header, xPriv, bodyString) } diff --git a/walletclient2.go b/walletclient2.go index a6ff50d..b380996 100644 --- a/walletclient2.go +++ b/walletclient2.go @@ -1,118 +1,144 @@ -// Package walletclient is a Go client for interacting with Spv Wallet. +// // Package walletclient is a Go client for interacting with Spv Wallet. package walletclient -import ( - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" - "github.com/libsv/go-bk/wif" - "github.com/pkg/errors" - - "github.com/bitcoin-sv/spv-wallet-go-client/transports" -) - -// ClientOps are used for client options -type ClientOps func(c *WalletClient) - -// // WalletClient is the spv wallet go client representation. -// type WalletClient struct { -// transports.TransportService -// accessKey *bec.PrivateKey -// accessKeyString string -// transport transports.TransportService -// transportOptions []transports.ClientOps -// xPriv *bip32.ExtendedKey -// xPrivString string -// xPub *bip32.ExtendedKey -// xPubString string +// import ( +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// "github.com/bitcoinschema/go-bitcoin/v2" +// "github.com/libsv/go-bk/bec" +// "github.com/libsv/go-bk/bip32" +// "github.com/libsv/go-bk/wif" +// "github.com/pkg/errors" + +// "github.com/bitcoin-sv/spv-wallet-go-client/transports" +// ) + +// // ClientOps are used for client options +// type ClientOps func(c *WalletClient) + +// // // WalletClient is the spv wallet go client representation. +// // type WalletClient struct { +// // transports.TransportService +// // accessKey *bec.PrivateKey +// // accessKeyString string +// // transport transports.TransportService +// // transportOptions []transports.ClientOps +// // xPriv *bip32.ExtendedKey +// // xPrivString string +// // xPub *bip32.ExtendedKey +// // xPubString string +// // } + +// // New create a new wallet client +// func NewOld(opts ...ClientOps) (*WalletClient, error) { +// client := &WalletClient{} + +// for _, opt := range opts { +// opt(client) +// } + +// var err error +// if client.xPrivString != "" { +// if client.xPriv, err = bitcoin.GenerateHDKeyFromString(client.xPrivString); err != nil { +// return nil, err +// } +// if client.xPub, err = client.xPriv.Neuter(); err != nil { +// return nil, err +// } +// } else if client.xPubString != "" { +// client.xPriv = nil +// if client.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(client.xPubString); err != nil { +// return nil, err +// } +// } else if client.accessKeyString != "" { +// client.xPriv = nil +// client.xPub = nil + +// var privateKey *bec.PrivateKey +// var decodedWIF *wif.WIF +// if decodedWIF, err = wif.DecodeWIF(client.accessKeyString); err != nil { +// // try as a hex string +// var errHex error +// if privateKey, errHex = bitcoin.PrivateKeyFromString(client.accessKeyString); errHex != nil { +// return nil, errors.Wrap(err, errHex.Error()) +// } +// } else { +// privateKey = decodedWIF.PrivKey +// } +// client.accessKey = privateKey +// } else { +// return nil, errors.New("no keys available") +// } + +// transportOptions := make([]transports.ClientOps, 0) +// if client.xPriv != nil { +// transportOptions = append(transportOptions, transports.WithXPriv(client.xPriv)) +// transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) +// } else if client.xPub != nil { +// transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) +// } else if client.accessKey != nil { +// transportOptions = append(transportOptions, transports.WithAccessKey(client.accessKey)) +// } +// if len(client.transportOptions) > 0 { +// transportOptions = append(transportOptions, client.transportOptions...) +// } + +// if client.transport, err = transports.NewTransport(transportOptions...); err != nil { +// return nil, err +// } + +// client.TransportService = client.transport + +// return client, nil // } -// New create a new wallet client -func NewOld(opts ...ClientOps) (*WalletClient, error) { - client := &WalletClient{} - - for _, opt := range opts { - opt(client) - } - - var err error - if client.xPrivString != "" { - if client.xPriv, err = bitcoin.GenerateHDKeyFromString(client.xPrivString); err != nil { - return nil, err - } - if client.xPub, err = client.xPriv.Neuter(); err != nil { - return nil, err - } - } else if client.xPubString != "" { - client.xPriv = nil - if client.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(client.xPubString); err != nil { - return nil, err - } - } else if client.accessKeyString != "" { - client.xPriv = nil - client.xPub = nil - - var privateKey *bec.PrivateKey - var decodedWIF *wif.WIF - if decodedWIF, err = wif.DecodeWIF(client.accessKeyString); err != nil { - // try as a hex string - var errHex error - if privateKey, errHex = bitcoin.PrivateKeyFromString(client.accessKeyString); errHex != nil { - return nil, errors.Wrap(err, errHex.Error()) - } - } else { - privateKey = decodedWIF.PrivKey - } - client.accessKey = privateKey - } else { - return nil, errors.New("no keys available") - } - - transportOptions := make([]transports.ClientOps, 0) - if client.xPriv != nil { - transportOptions = append(transportOptions, transports.WithXPriv(client.xPriv)) - transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) - } else if client.xPub != nil { - transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) - } else if client.accessKey != nil { - transportOptions = append(transportOptions, transports.WithAccessKey(client.accessKey)) - } - if len(client.transportOptions) > 0 { - transportOptions = append(transportOptions, client.transportOptions...) - } - - if client.transport, err = transports.NewTransport(transportOptions...); err != nil { - return nil, err - } - - client.TransportService = client.transport - - return client, nil -} - -// SetAdminKey set the admin key to use to create new xpubs -func (b *WalletClient) SetAdminKey(adminKeyString string) error { - adminKey, err := bip32.NewKeyFromString(adminKeyString) - if err != nil { - return err - } - - b.transport.SetAdminKey(adminKey) - - return nil -} - -// SetSignRequest turn the signing of the http request on or off -func (b *WalletClient) SetSignRequest(signRequest bool) { - b.transport.SetSignRequest(signRequest) -} - -// IsSignRequest return whether to sign all requests -func (b *WalletClient) IsSignRequest() bool { - return b.transport.IsSignRequest() -} - -// GetTransport returns the current transport service -func (b *WalletClient) GetTransport() *transports.TransportService { - return &b.transport -} +// // SetAdminKey set the admin key to use to create new xpubs +// func (b *WalletClient) SetAdminKey(adminKeyString string) error { +// adminKey, err := bip32.NewKeyFromString(adminKeyString) +// if err != nil { +// return err +// } + +// b.transport.SetAdminKey(adminKey) + +// return nil +// } + +// // SetSignRequest turn the signing of the http request on or off +// func (b *WalletClient) SetSignRequest(signRequest bool) { +// b.transport.SetSignRequest(signRequest) +// } + +// // IsSignRequest return whether to sign all requests +// func (b *WalletClient) IsSignRequest() bool { +// return b.transport.IsSignRequest() +// } + +// // GetTransport returns the current transport service +// func (b *WalletClient) GetTransport() *transports.TransportService { +// return &b.transport +// } diff --git a/walletclient_test.go b/walletclient_test.go index 1e3514b..bb3a076 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -1,362 +1,392 @@ package walletclient -import ( - "context" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" - "github.com/bitcoin-sv/spv-wallet-go-client/transports" -) - -// localRoundTripper is an http.RoundTripper that executes HTTP transactions -// by using handler directly, instead of going over an HTTP connection. -type localRoundTripper struct { - handler http.Handler -} - -func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - w := httptest.NewRecorder() - l.handler.ServeHTTP(w, req) - return w.Result(), nil -} - -func mustWrite(w io.Writer, s string) { - _, err := io.WriteString(w, s) - if err != nil { - panic(err) - } -} - -type testTransportHandler struct { - ClientURL string - Client func(serverURL string, httpClient *http.Client) ClientOps - Path string - Queries []*testTransportHandlerRequest - Result string - Type string -} - -type testTransportHandlerRequest struct { - Path string - Result func(w http.ResponseWriter, req *http.Request) -} - -// TestNewWalletClient will test the TestNewWalletClient method -func TestNewWalletClient(t *testing.T) { - t.Run("no keys", func(t *testing.T) { - client, err := New() - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("empty xpriv", func(t *testing.T) { - client, err := New( - WithXPriv(""), - ) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("invalid xpriv", func(t *testing.T) { - client, err := New( - WithXPriv("invalid-xpriv"), - ) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("valid client", func(t *testing.T) { - client, err := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) - - t.Run("valid xPub client", func(t *testing.T) { - client, err := New( - WithXPub(fixtures.XPubString), - WithHTTP(fixtures.ServerURL), - ) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) - - t.Run("invalid xPub client", func(t *testing.T) { - client, err := New( - WithXPub("invalid-xpub"), - WithHTTP(fixtures.ServerURL), - ) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("valid access keys", func(t *testing.T) { - client, err := New( - WithAccessKey(fixtures.AccessKeyString), - WithHTTP(fixtures.ServerURL), - ) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) - - t.Run("invalid access keys", func(t *testing.T) { - client, err := New( - WithAccessKey("invalid-access-key"), - WithHTTP(fixtures.ServerURL), - ) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("valid access key WIF", func(t *testing.T) { - wifKey, _ := bitcoin.PrivateKeyToWif(fixtures.AccessKeyString) - client, err := New( - WithAccessKey(wifKey.String()), - WithHTTP(fixtures.ServerURL), - ) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) -} - -// TestSetAdminKey will test the admin key setter -func TestSetAdminKey(t *testing.T) { - t.Run("invalid", func(t *testing.T) { - client, _ := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - err := client.SetAdminKey("") - assert.Error(t, err) - }) - - t.Run("valid", func(t *testing.T) { - client, _ := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - err := client.SetAdminKey(fixtures.XPrivString) - assert.NoError(t, err) - }) - - t.Run("invalid with", func(t *testing.T) { - _, err := New( - WithXPriv(fixtures.XPrivString), - WithAdminKey("rest"), - WithHTTP(fixtures.ServerURL), - ) - assert.Error(t, err) - }) - - t.Run("valid with", func(t *testing.T) { - _, err := New( - WithXPriv(fixtures.XPrivString), - WithAdminKey(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - assert.NoError(t, err) - }) -} - -// TestSetSignRequest will test the sign request setter -func TestSetSignRequest(t *testing.T) { - t.Run("true", func(t *testing.T) { - client, _ := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - client.SetSignRequest(true) - assert.True(t, client.IsSignRequest()) - }) - - t.Run("false", func(t *testing.T) { - client, _ := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - client.SetSignRequest(false) - assert.False(t, client.IsSignRequest()) - }) - - t.Run("false by default", func(t *testing.T) { - client, err := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - require.NoError(t, err) - assert.False(t, client.IsSignRequest()) - }) -} - -// TestGetTransport will test the GetTransport method -func TestGetTransport(t *testing.T) { - t.Run("GetTransport", func(t *testing.T) { - client, _ := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - ) - transport := client.GetTransport() - assert.IsType(t, &transports.TransportHTTP{}, *transport) - }) - - t.Run("client GetTransport", func(t *testing.T) { - client, _ := New( - WithXPriv(fixtures.XPrivString), - WithHTTP(fixtures.ServerURL), - WithAdminKey(fixtures.XPrivString), - WithSignRequest(false), - ) - transport := client.GetTransport() - assert.IsType(t, &transports.TransportHTTP{}, *transport) - }) -} - -func TestAuthenticationWithOnlyAccessKey(t *testing.T) { - anyConditions := make(map[string]interface{}, 0) - var anyMetadataConditions *models.Metadata - anyParam := "sth" - - testCases := []struct { - caseTitle string - path string - clientMethod func(*WalletClient) (any, error) - }{ - { - caseTitle: "GetXPub", - path: "/xpub", - clientMethod: func(c *WalletClient) (any, error) { return c.GetXPub(context.Background()) }, - }, - { - caseTitle: "GetAccessKey", - path: "/access-key", - clientMethod: func(c *WalletClient) (any, error) { return c.GetAccessKey(context.Background(), anyParam) }, - }, - { - caseTitle: "GetAccessKeys", - path: "/access-key", - clientMethod: func(c *WalletClient) (any, error) { - return c.GetAccessKeys(context.Background(), anyMetadataConditions) - }, - }, - { - caseTitle: "GetDestinationByID", - path: "/destination", - clientMethod: func(c *WalletClient) (any, error) { return c.GetDestinationByID(context.Background(), anyParam) }, - }, - { - caseTitle: "GetDestinationByAddress", - path: "/destination", - clientMethod: func(c *WalletClient) (any, error) { - return c.GetDestinationByAddress(context.Background(), anyParam) - }, - }, - { - caseTitle: "GetDestinationByLockingScript", - path: "/destination", - clientMethod: func(c *WalletClient) (any, error) { - return c.GetDestinationByLockingScript(context.Background(), anyParam) - }, - }, - { - caseTitle: "GetDestinations", - path: "/destination/search", - clientMethod: func(c *WalletClient) (any, error) { - return c.GetDestinations(context.Background(), nil) - }, - }, - { - caseTitle: "GetTransaction", - path: "/transaction", - clientMethod: func(c *WalletClient) (any, error) { - return c.GetTransaction(context.Background(), fixtures.Transaction.ID) - }, - }, - { - caseTitle: "GetTransactions", - path: "/transaction/search", - clientMethod: func(c *WalletClient) (any, error) { - return c.GetTransactions(context.Background(), anyConditions, anyMetadataConditions, &transports.QueryParams{}) - }, - }, - } - - for _, test := range testCases { - t.Run(test.caseTitle, func(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Queries: []*testTransportHandlerRequest{{ - Path: test.path, - Result: func(w http.ResponseWriter, req *http.Request) { - assertAuthHeaders(t, req) - w.Header().Set("Content-Type", "application/json") - mustWrite(w, "{}") - }, - }}, - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - client := getTestWalletClientWithOpts(transportHandler, WithAccessKey(fixtures.AccessKeyString)) - - _, err := test.clientMethod(client) - if err != nil { - t.Log(err) - } - }) - } -} - -func assertAuthHeaders(t *testing.T, req *http.Request) { - assert.Empty(t, req.Header.Get("x-auth-xpub"), "Header value x-auth-xpub should be empty") - assert.NotEmpty(t, req.Header.Get("x-auth-key"), "Header value x-auth-key should not be empty") - assert.NotEmpty(t, req.Header.Get("x-auth-time"), "Header value x-auth-time should not be empty") - assert.NotEmpty(t, req.Header.Get("x-auth-hash"), "Header value x-auth-hash should not be empty") - assert.NotEmpty(t, req.Header.Get("x-auth-nonce"), "Header value x-auth-nonce should not be empty") - assert.NotEmpty(t, req.Header.Get("x-auth-signature"), "Header value x-auth-signature should not be empty") -} - -func getTestWalletClient(transportHandler testTransportHandler, adminKey bool) *WalletClient { - opts := []ClientOps{ - WithXPriv(fixtures.XPrivString), - } - if adminKey { - opts = append(opts, WithAdminKey(fixtures.XPrivString)) - } - - return getTestWalletClientWithOpts(transportHandler, opts...) -} - -func getTestWalletClientWithOpts(transportHandler testTransportHandler, options ...ClientOps) *WalletClient { - mux := http.NewServeMux() - if transportHandler.Queries != nil { - for _, query := range transportHandler.Queries { - mux.HandleFunc(query.Path, query.Result) - } - } else { - mux.HandleFunc(transportHandler.Path, func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, transportHandler.Result) - }) - } - httpclient := &http.Client{Transport: localRoundTripper{handler: mux}} - - opts := []ClientOps{ - transportHandler.Client(transportHandler.ClientURL, httpclient), - } - - opts = append(opts, options...) - - client, _ := New(opts...) - - return client -} +// import ( +// "context" +// "io" +// "net/http" +// "net/http/httptest" +// "testing" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// "github.com/bitcoin-sv/spv-wallet/models" +// "github.com/bitcoinschema/go-bitcoin/v2" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// "github.com/bitcoin-sv/spv-wallet-go-client/transports" +// ) + +// // localRoundTripper is an http.RoundTripper that executes HTTP transactions +// // by using handler directly, instead of going over an HTTP connection. +// type localRoundTripper struct { +// handler http.Handler +// } + +// func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +// w := httptest.NewRecorder() +// l.handler.ServeHTTP(w, req) +// return w.Result(), nil +// } + +// func mustWrite(w io.Writer, s string) { +// _, err := io.WriteString(w, s) +// if err != nil { +// panic(err) +// } +// } + +// type testTransportHandler struct { +// ClientURL string +// Client func(serverURL string, httpClient *http.Client) ClientOps +// Path string +// Queries []*testTransportHandlerRequest +// Result string +// Type string +// } + +// type testTransportHandlerRequest struct { +// Path string +// Result func(w http.ResponseWriter, req *http.Request) +// } + +// // TestNewWalletClient will test the TestNewWalletClient method +// func TestNewWalletClient(t *testing.T) { +// t.Run("no keys", func(t *testing.T) { +// client, err := New() +// assert.Error(t, err) +// assert.Nil(t, client) +// }) + +// t.Run("empty xpriv", func(t *testing.T) { +// client, err := New( +// WithXPriv(""), +// ) +// assert.Error(t, err) +// assert.Nil(t, client) +// }) + +// t.Run("invalid xpriv", func(t *testing.T) { +// client, err := New( +// WithXPriv("invalid-xpriv"), +// ) +// assert.Error(t, err) +// assert.Nil(t, client) +// }) + +// t.Run("valid client", func(t *testing.T) { +// client, err := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// require.NoError(t, err) +// assert.IsType(t, WalletClient{}, *client) +// }) + +// t.Run("valid xPub client", func(t *testing.T) { +// client, err := New( +// WithXPub(fixtures.XPubString), +// WithHTTP(fixtures.ServerURL), +// ) +// require.NoError(t, err) +// assert.IsType(t, WalletClient{}, *client) +// }) + +// t.Run("invalid xPub client", func(t *testing.T) { +// client, err := New( +// WithXPub("invalid-xpub"), +// WithHTTP(fixtures.ServerURL), +// ) +// assert.Error(t, err) +// assert.Nil(t, client) +// }) + +// t.Run("valid access keys", func(t *testing.T) { +// client, err := New( +// WithAccessKey(fixtures.AccessKeyString), +// WithHTTP(fixtures.ServerURL), +// ) +// require.NoError(t, err) +// assert.IsType(t, WalletClient{}, *client) +// }) + +// t.Run("invalid access keys", func(t *testing.T) { +// client, err := New( +// WithAccessKey("invalid-access-key"), +// WithHTTP(fixtures.ServerURL), +// ) +// assert.Error(t, err) +// assert.Nil(t, client) +// }) + +// t.Run("valid access key WIF", func(t *testing.T) { +// wifKey, _ := bitcoin.PrivateKeyToWif(fixtures.AccessKeyString) +// client, err := New( +// WithAccessKey(wifKey.String()), +// WithHTTP(fixtures.ServerURL), +// ) +// require.NoError(t, err) +// assert.IsType(t, WalletClient{}, *client) +// }) +// } + +// // TestSetAdminKey will test the admin key setter +// func TestSetAdminKey(t *testing.T) { +// t.Run("invalid", func(t *testing.T) { +// client, _ := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// err := client.SetAdminKey("") +// assert.Error(t, err) +// }) + +// t.Run("valid", func(t *testing.T) { +// client, _ := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// err := client.SetAdminKey(fixtures.XPrivString) +// assert.NoError(t, err) +// }) + +// t.Run("invalid with", func(t *testing.T) { +// _, err := New( +// WithXPriv(fixtures.XPrivString), +// WithAdminKey("rest"), +// WithHTTP(fixtures.ServerURL), +// ) +// assert.Error(t, err) +// }) + +// t.Run("valid with", func(t *testing.T) { +// _, err := New( +// WithXPriv(fixtures.XPrivString), +// WithAdminKey(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// assert.NoError(t, err) +// }) +// } + +// // TestSetSignRequest will test the sign request setter +// func TestSetSignRequest(t *testing.T) { +// t.Run("true", func(t *testing.T) { +// client, _ := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// client.SetSignRequest(true) +// assert.True(t, client.IsSignRequest()) +// }) + +// t.Run("false", func(t *testing.T) { +// client, _ := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// client.SetSignRequest(false) +// assert.False(t, client.IsSignRequest()) +// }) + +// t.Run("false by default", func(t *testing.T) { +// client, err := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// require.NoError(t, err) +// assert.False(t, client.IsSignRequest()) +// }) +// } + +// // TestGetTransport will test the GetTransport method +// func TestGetTransport(t *testing.T) { +// t.Run("GetTransport", func(t *testing.T) { +// client, _ := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// ) +// transport := client.GetTransport() +// assert.IsType(t, &transports.TransportHTTP{}, *transport) +// }) + +// t.Run("client GetTransport", func(t *testing.T) { +// client, _ := New( +// WithXPriv(fixtures.XPrivString), +// WithHTTP(fixtures.ServerURL), +// WithAdminKey(fixtures.XPrivString), +// WithSignRequest(false), +// ) +// transport := client.GetTransport() +// assert.IsType(t, &transports.TransportHTTP{}, *transport) +// }) +// } + +// func TestAuthenticationWithOnlyAccessKey(t *testing.T) { +// anyConditions := make(map[string]interface{}, 0) +// var anyMetadataConditions *models.Metadata +// anyParam := "sth" + +// testCases := []struct { +// caseTitle string +// path string +// clientMethod func(*WalletClient) (any, error) +// }{ +// { +// caseTitle: "GetXPub", +// path: "/xpub", +// clientMethod: func(c *WalletClient) (any, error) { return c.GetXPub(context.Background()) }, +// }, +// { +// caseTitle: "GetAccessKey", +// path: "/access-key", +// clientMethod: func(c *WalletClient) (any, error) { return c.GetAccessKey(context.Background(), anyParam) }, +// }, +// { +// caseTitle: "GetAccessKeys", +// path: "/access-key", +// clientMethod: func(c *WalletClient) (any, error) { +// return c.GetAccessKeys(context.Background(), anyMetadataConditions) +// }, +// }, +// { +// caseTitle: "GetDestinationByID", +// path: "/destination", +// clientMethod: func(c *WalletClient) (any, error) { return c.GetDestinationByID(context.Background(), anyParam) }, +// }, +// { +// caseTitle: "GetDestinationByAddress", +// path: "/destination", +// clientMethod: func(c *WalletClient) (any, error) { +// return c.GetDestinationByAddress(context.Background(), anyParam) +// }, +// }, +// { +// caseTitle: "GetDestinationByLockingScript", +// path: "/destination", +// clientMethod: func(c *WalletClient) (any, error) { +// return c.GetDestinationByLockingScript(context.Background(), anyParam) +// }, +// }, +// { +// caseTitle: "GetDestinations", +// path: "/destination/search", +// clientMethod: func(c *WalletClient) (any, error) { +// return c.GetDestinations(context.Background(), nil) +// }, +// }, +// { +// caseTitle: "GetTransaction", +// path: "/transaction", +// clientMethod: func(c *WalletClient) (any, error) { +// return c.GetTransaction(context.Background(), fixtures.Transaction.ID) +// }, +// }, +// { +// caseTitle: "GetTransactions", +// path: "/transaction/search", +// clientMethod: func(c *WalletClient) (any, error) { +// return c.GetTransactions(context.Background(), anyConditions, anyMetadataConditions, &transports.QueryParams{}) +// }, +// }, +// } + +// for _, test := range testCases { +// t.Run(test.caseTitle, func(t *testing.T) { +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Queries: []*testTransportHandlerRequest{{ +// Path: test.path, +// Result: func(w http.ResponseWriter, req *http.Request) { +// assertAuthHeaders(t, req) +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, "{}") +// }, +// }}, +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } + +// client := getTestWalletClientWithOpts(transportHandler, WithAccessKey(fixtures.AccessKeyString)) + +// _, err := test.clientMethod(client) +// if err != nil { +// t.Log(err) +// } +// }) +// } +// } + +// func assertAuthHeaders(t *testing.T, req *http.Request) { +// assert.Empty(t, req.Header.Get("x-auth-xpub"), "Header value x-auth-xpub should be empty") +// assert.NotEmpty(t, req.Header.Get("x-auth-key"), "Header value x-auth-key should not be empty") +// assert.NotEmpty(t, req.Header.Get("x-auth-time"), "Header value x-auth-time should not be empty") +// assert.NotEmpty(t, req.Header.Get("x-auth-hash"), "Header value x-auth-hash should not be empty") +// assert.NotEmpty(t, req.Header.Get("x-auth-nonce"), "Header value x-auth-nonce should not be empty") +// assert.NotEmpty(t, req.Header.Get("x-auth-signature"), "Header value x-auth-signature should not be empty") +// } + +// func getTestWalletClient(transportHandler testTransportHandler, adminKey bool) *WalletClient { +// opts := []ClientOps{ +// WithXPriv(fixtures.XPrivString), +// } +// if adminKey { +// opts = append(opts, WithAdminKey(fixtures.XPrivString)) +// } + +// return getTestWalletClientWithOpts(transportHandler, opts...) +// } + +// func getTestWalletClientWithOpts(transportHandler testTransportHandler, options ...ClientOps) *WalletClient { +// mux := http.NewServeMux() +// if transportHandler.Queries != nil { +// for _, query := range transportHandler.Queries { +// mux.HandleFunc(query.Path, query.Result) +// } +// } else { +// mux.HandleFunc(transportHandler.Path, func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, transportHandler.Result) +// }) +// } +// httpclient := &http.Client{Transport: localRoundTripper{handler: mux}} + +// opts := []ClientOps{ +// transportHandler.Client(transportHandler.ClientURL, httpclient), +// } + +// opts = append(opts, options...) + +// client, _ := New(opts...) + +// return client +// } diff --git a/xpubs.go b/xpubs.go index 65beaa3..64e30a3 100644 --- a/xpubs.go +++ b/xpubs.go @@ -1,18 +1,54 @@ package walletclient -import ( - "context" +// import ( +// "context" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// - "github.com/bitcoin-sv/spv-wallet-go-client/transports" - "github.com/bitcoin-sv/spv-wallet/models" -) +// "github.com/bitcoin-sv/spv-wallet/models" -// GetXPub gets the current xpub -func (b *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, transports.ResponseError) { - return b.transport.GetXPub(ctx) -} +// "github.com/bitcoin-sv/spv-wallet-go-client/transports" +// ) -// UpdateXPubMetadata update the metadata of the logged in xpub -func (b *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, transports.ResponseError) { - return b.transport.UpdateXPubMetadata(ctx, metadata) -} +// // GetXPub gets the current xpub +// func (b *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, transports.ResponseError) { +// return b.transport.GetXPub(ctx) +// } + +// // UpdateXPubMetadata update the metadata of the logged in xpub +// func (b *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, transports.ResponseError) { +// return b.transport.UpdateXPubMetadata(ctx, metadata) +// } From 1a7d420594f46c7e68038926127e62ce337b6dbd Mon Sep 17 00:00:00 2001 From: ac4ch Date: Sun, 12 May 2024 14:23:30 +0200 Subject: [PATCH 03/27] latest working --- examples/tt/admin.go | 168 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 examples/tt/admin.go diff --git a/examples/tt/admin.go b/examples/tt/admin.go new file mode 100644 index 0000000..f40bb54 --- /dev/null +++ b/examples/tt/admin.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/bitcoin-sv/spv-wallet/models" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/transports" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" +) + +const serverURL = "http://localhost:3003/v1" +const adminKey = "xprv9s21ZrQH143K3CbJXirfrtpLvhT3Vgusdo8coBritQ3rcS7Jy7sxWhatuxG5h2y1Cqj8FKmPp69536gmjYRpfga2MJdsGyBsnB12E19CESK" + +func main() { + clientAdmin, err := setupAdminClient() + if err != nil { + log.Fatalf("Failed to setup admin client: %v", err) + } + log.Println("is sign req: ", clientAdmin.IsSignRequest()) + ctx := context.Background() + + // Example of creating users and interacting with wallet functions + handleUsers(ctx, clientAdmin) +} + +func setupAdminClient() (*walletclient.WalletClient, error) { + + // Set up a client with administrative privileges + return walletclient.New( + &walletclient.WithXPriv{XPrivString: adminKey}, + &walletclient.WithAdminKey{AdminKeyString: adminKey}, + &walletclient.WithHTTP{ServerURL: serverURL}, + &walletclient.WithSignRequest{Sign: true}, + ) +} + +func handleUsers(ctx context.Context, clientAdmin *walletclient.WalletClient) { + aliceName, bobName := "alice", "bob" + aliceClient, _, err := createUser(ctx, aliceName, clientAdmin) + if err != nil { + log.Fatalf("Failed to create user %s: %v", aliceName, err) + } + bobClient, bobPaymail, err := createUser(ctx, bobName, clientAdmin) + if err != nil { + log.Fatalf("Failed to create user %s: %v", bobName, err) + } + + fmt.Println() + log.Println(" **** Admin Get Contacts ****") + c, err := clientAdmin.AdminGetContacts(ctx, nil, nil, &transports.QueryParams{}) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(c) == 0 { + log.Printf("empty list of contacts") + return + } + log.Printf("admin got contacts: %#v\n", c) + + fmt.Println() + log.Println(" **** Admin Update Contact ****") + bobToUpdate := c[0] + log.Printf("Bob before update [%v]", c[0].FullName) + updatedContact, err := clientAdmin.AdminUpdateContact(ctx, bobToUpdate.ID, fmt.Sprintf("%s %s", bobName, time.Now().Local().String()), nil) + if err != nil { + log.Panicf("error updating contact id: [%s] name: [%s] - %s", bobToUpdate.ID, bobToUpdate.FullName, err) + } + log.Printf("updated contact full name [%v]", updatedContact.FullName) + + fmt.Println() + log.Println(" **** Admin Reject Contact ****") + aliceContacts, err := bobClient.GetContacts(ctx, nil, nil, nil) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(aliceContacts) == 0 { + log.Printf("empty list of contacts") + return + } + log.Printf("status should be awaiting == [%v]", aliceContacts[0].Status) + + rejectAliceContact, err := clientAdmin.AdminRejectContact(ctx, aliceContacts[0].ID) + if err != nil { + log.Panicf("contact not accepted id: %v - %s", aliceContacts[0].ID, err) + } + log.Printf("status should change to rejected == [%v]", rejectAliceContact.Status) + + fmt.Println() + log.Println(" **** Admin Accept Contact ****") + if _, err := aliceClient.UpsertContact(ctx, bobPaymail, bobName, nil); err != nil { + panic(err) + } + + aliceContacts, err = bobClient.GetContacts(ctx, nil, nil, nil) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(aliceContacts) == 0 { + log.Printf("empty list of contacts") + return + } + + log.Printf("status should be awaiting == [%v]", aliceContacts[0].Status) + acceptAliceContact, err := clientAdmin.AdminAcceptContact(ctx, aliceContacts[0].ID) + if err != nil { + log.Panicf("contact not accepted id: %v - %s", aliceContacts[0].ID, err) + } + log.Printf("status should change to unconfirmed == [%v]", acceptAliceContact.Status) + + fmt.Println() + log.Println(" **** Admin Delete Contact ****") + + err = clientAdmin.AdminDeleteContact(ctx, aliceContacts[0].ID) + if err != nil { + log.Panicf("contact not accepted id: %v - %s", aliceContacts[0].ID, err) + } + if len(aliceContacts) > 0 { + log.Println("removed id:", aliceContacts[0].ID) + } else { + log.Println("alice contacts are empty") + } + aliceContacts, err = bobClient.GetContacts(ctx, nil, nil, nil) + if err != nil { + log.Printf("admin error for get contacts - %v", err) + } + if len(aliceContacts) == 0 { + log.Printf("empty list of contacts") + } + for _, c := range aliceContacts { + log.Println("alice contacts id:", c.ID, c.FullName, c.DeletedAt) + } +} + +func createUser(ctx context.Context, name string, adminClient *walletclient.WalletClient) (*walletclient.WalletClient, string, error) { + keys, err := xpriv.Generate() + if err != nil { + return nil, "", fmt.Errorf("failed to generate keys: %v", err) + } + + timestamp := time.Now().UnixMicro() + examplePaymail := fmt.Sprintf("contacttest_%d_%s@auggie.4chain.space", timestamp, name) + + metadata := make(models.Metadata) + metadata["name"] = name + + if err := adminClient.AdminNewXpub(ctx, keys.XPub().String(), &metadata); err != nil { + return nil, "", fmt.Errorf("failed to create new xpub: %v", err) + } + if _, err := adminClient.AdminCreatePaymail(ctx, keys.XPub().String(), examplePaymail, name, ""); err != nil { + return nil, "", fmt.Errorf("failed to create paymail: %v", err) + } + + userClient, err := walletclient.New( + &walletclient.WithXPriv{XPrivString: keys.XPriv()}, + &walletclient.WithHTTP{ServerURL: serverURL}, + &walletclient.WithSignRequest{Sign: true}, + ) + if err != nil { + return nil, "", fmt.Errorf("failed to create user client: %v", err) + } + + return userClient, examplePaymail, nil +} From 2f586bfa2c83c2d96261b532daf1ea0b32c6968d Mon Sep 17 00:00:00 2001 From: ac4ch Date: Sun, 12 May 2024 16:40:54 +0200 Subject: [PATCH 04/27] latest working --- client_options.go | 29 +- contacts.go | 2 +- examples/tt/admin.go | 13 +- fixtures/fixtures.go | 2 +- http.go | 47 ++- http_admin.go | 4 +- transactions.go | 117 ------ transactions_test.go | 656 ++++++++++++++++--------------- transports/authentication.go | 217 ---------- transports/client_options.go | 90 ----- transports/config.go | 97 ----- transports/errors.go | 72 ---- transports/http.go | 721 ---------------------------------- transports/http_admin.go | 369 ----------------- transports/interface.go | 104 ----- transports/transports.go | 72 ---- transports/transports_test.go | 41 -- walletclient.go | 59 ++- walletclient2.go | 144 ------- walletclient_test.go | 502 ++++++----------------- 20 files changed, 545 insertions(+), 2813 deletions(-) delete mode 100644 transactions.go delete mode 100644 transports/authentication.go delete mode 100644 transports/client_options.go delete mode 100644 transports/config.go delete mode 100644 transports/errors.go delete mode 100644 transports/http.go delete mode 100644 transports/http_admin.go delete mode 100644 transports/interface.go delete mode 100644 transports/transports.go delete mode 100644 transports/transports_test.go delete mode 100644 walletclient2.go diff --git a/client_options.go b/client_options.go index 9f34b2f..07b84b9 100644 --- a/client_options.go +++ b/client_options.go @@ -1,8 +1,9 @@ package walletclient import ( - "fmt" "net/http" + + "github.com/bitcoinschema/go-bitcoin/v2" ) // WalletClientConfigurator is the interface for configuring WalletClient @@ -12,17 +13,16 @@ type WalletClientConfigurator interface { // WithXPriv sets the xPrivString field of a WalletClient type WithXPriv struct { - XPrivString string + XPrivString *string } func (w *WithXPriv) Configure(c *WalletClient) { - fmt.Printf("withXpriv configure: %#v\n", w) c.xPrivString = w.XPrivString } // WithXPub sets the xPubString on the client type WithXPub struct { - XPubString string + XPubString *string } func (w *WithXPub) Configure(c *WalletClient) { @@ -31,7 +31,7 @@ func (w *WithXPub) Configure(c *WalletClient) { // WithAccessKey sets the accessKeyString on the client type WithAccessKey struct { - AccessKeyString string + AccessKeyString *string } func (w *WithAccessKey) Configure(c *WalletClient) { @@ -40,29 +40,36 @@ func (w *WithAccessKey) Configure(c *WalletClient) { // WithAdminKey sets the admin key for creating new xpubs type WithAdminKey struct { - AdminKeyString string + AdminKeyString *string } func (w *WithAdminKey) Configure(c *WalletClient) { - fmt.Printf("withAdminKey configure: %#v\n", w) - fmt.Printf("withAdminKey configure adminxpriv: %v \n", w.AdminKeyString) - c.adminXPriv = w.AdminKeyString + var err error + c.adminXPriv, err = bitcoin.GenerateHDKeyFromString(*w.AdminKeyString) + if err != nil { + c.adminXPriv = nil + } } // WithHTTP sets the URL and HTTP client of a WalletClient type WithHTTP struct { - ServerURL string + ServerURL *string HTTPClient *http.Client } func (w *WithHTTP) Configure(c *WalletClient) { c.server = w.ServerURL c.httpClient = w.HTTPClient + if w.HTTPClient != nil { + c.httpClient = w.HTTPClient + } else { + c.httpClient = http.DefaultClient + } } // WithSignRequest configures whether to sign HTTP requests type WithSignRequest struct { - Sign bool + Sign *bool } func (w *WithSignRequest) Configure(c *WalletClient) { diff --git a/contacts.go b/contacts.go index 8121a7d..9e30116 100644 --- a/contacts.go +++ b/contacts.go @@ -5,7 +5,7 @@ package walletclient // 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. +// 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) // } diff --git a/examples/tt/admin.go b/examples/tt/admin.go index 1bc8b22..86be836 100644 --- a/examples/tt/admin.go +++ b/examples/tt/admin.go @@ -31,12 +31,7 @@ func main() { func setupAdminClient() (*walletclient.WalletClient, error) { // Set up a client with administrative privileges - return walletclient.New( - &walletclient.WithXPriv{XPrivString: adminKey}, - &walletclient.WithAdminKey{AdminKeyString: adminKey}, - &walletclient.WithHTTP{ServerURL: serverURL}, - &walletclient.WithSignRequest{Sign: true}, - ) + return walletclient.NewWalletClientWithAdminKey(adminKey, serverURL, true) } func handleUsers(ctx context.Context, clientAdmin *walletclient.WalletClient) { @@ -155,11 +150,7 @@ func createUser(ctx context.Context, name string, adminClient *walletclient.Wall return nil, "", fmt.Errorf("failed to create paymail: %v", err) } - userClient, err := walletclient.New( - &walletclient.WithXPriv{XPrivString: keys.XPriv()}, - &walletclient.WithHTTP{ServerURL: serverURL}, - &walletclient.WithSignRequest{Sign: true}, - ) + userClient, err := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), serverURL, true) if err != nil { return nil, "", fmt.Errorf("failed to create user client: %v", err) } diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index 356517b..937e830 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -7,7 +7,7 @@ import ( "github.com/bitcoin-sv/spv-wallet/models/common" ) -const ( +var ( RequestType = "http" ServerURL = "https://example.com/" XPubString = "xpub661MyMwAqRbcFrBJbKwBGCB7d3fr2SaAuXGM95BA62X41m6eW2ehRQGW4xLi9wkEXUGnQZYxVVj4PxXnyrLk7jdqvBAs1Qq9gf6ykMvjR7J" diff --git a/http.go b/http.go index 23bc180..0c457d3 100644 --- a/http.go +++ b/http.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" "net/http" "strconv" @@ -15,6 +16,7 @@ import ( "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" + "github.com/bitcoin-sv/spv-wallet-go-client/transports" "github.com/bitcoin-sv/spv-wallet-go-client/utils" ) @@ -30,12 +32,12 @@ type transportHTTP struct { // SetSignRequest turn the signing of the http request on or off func (wc *WalletClient) SetSignRequest(signRequest bool) { - wc.signRequest = signRequest + wc.signRequest = &signRequest } // IsSignRequest return whether to sign all requests func (wc *WalletClient) IsSignRequest() bool { - return wc.signRequest + return *wc.signRequest } // SetAdminKey set the admin key @@ -327,7 +329,7 @@ func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[stri var transactions []*models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/search", jsonStr, wc.xPriv, wc.signRequest, &transactions, + ctx, http.MethodPost, "/transaction/search", jsonStr, wc.xPriv, *wc.signRequest, &transactions, ); err != nil { return nil, err } @@ -349,7 +351,7 @@ func (wc *WalletClient) GetTransactionsCount(ctx context.Context, conditions map var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/count", jsonStr, wc.xPriv, wc.signRequest, &count, + ctx, http.MethodPost, "/transaction/count", jsonStr, wc.xPriv, *wc.signRequest, &count, ); err != nil { return 0, err } @@ -425,7 +427,7 @@ func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/record", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ctx, http.MethodPost, "/transaction/record", jsonStr, wc.xPriv, *wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -447,7 +449,7 @@ func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID stri var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/transaction", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ctx, http.MethodPatch, "/transaction", jsonStr, wc.xPriv, *wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -498,7 +500,7 @@ func (wc *WalletClient) GetUtxos(ctx context.Context, conditions map[string]inte var utxos []*models.Utxo if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/search", jsonStr, wc.xPriv, wc.signRequest, &utxos, + ctx, http.MethodPost, "/utxo/search", jsonStr, wc.xPriv, *wc.signRequest, &utxos, ); err != nil { return nil, err } @@ -518,7 +520,7 @@ func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions map[string var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/count", jsonStr, wc.xPriv, wc.signRequest, &count, + ctx, http.MethodPost, "/utxo/count", jsonStr, wc.xPriv, *wc.signRequest, &count, ); err != nil { return 0, err } @@ -560,7 +562,7 @@ func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *models func (wc *WalletClient) doHTTPRequest(ctx context.Context, method string, path string, rawJSON []byte, xPriv *bip32.ExtendedKey, sign bool, responseJSON interface{}, ) ResponseError { - req, err := http.NewRequestWithContext(ctx, method, wc.server+path, bytes.NewBuffer(rawJSON)) + req, err := http.NewRequestWithContext(ctx, method, *wc.server+path, bytes.NewBuffer(rawJSON)) if err != nil { return WrapError(err) } @@ -626,7 +628,7 @@ func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []b // AcceptContact will accept the contact associated with the paymail func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.xPriv, *wc.signRequest, nil, ); err != nil { return err } @@ -637,7 +639,7 @@ func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) Respo // RejectContact will reject the contact associated with the paymail func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.xPriv, *wc.signRequest, nil, ); err != nil { return err } @@ -646,9 +648,18 @@ func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) Respo } // ConfirmContact will confirm the contact associated with the paymail -func (wc *WalletClient) ConfirmContact(ctx context.Context, paymail string) ResponseError { +func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) ResponseError { + isTotpValid, err := wc.ValidateTotpForContact(contact, passcode, requesterPaymail, period, digits) + if err != nil { + return WrapError(fmt.Errorf("totp validation failed: %w", err)) + } + + if !isTotpValid { + return WrapError(errors.New("totp is invalid")) + } + if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/confirmed/"+paymail, nil, wc.xPriv, wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.xPriv, *wc.signRequest, nil, ); err != nil { return err } @@ -669,7 +680,7 @@ func (wc *WalletClient) GetContacts(ctx context.Context, conditions map[string]i var result []*models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/contact/search", jsonStr, wc.xPriv, wc.signRequest, &result, + ctx, http.MethodPost, "/contact/search", jsonStr, wc.xPriv, *wc.signRequest, &result, ); err != nil { return nil, err } @@ -678,7 +689,11 @@ func (wc *WalletClient) GetContacts(ctx context.Context, conditions map[string]i } // 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 (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, ResponseError) { +func (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { + return wc.UpsertContactForPaymail(ctx, paymail, fullName, metadata, "") +} + +func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, transports.ResponseError) { payload := map[string]interface{}{ "fullName": fullName, FieldMetadata: processMetadata(metadata), @@ -695,7 +710,7 @@ func (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName str var result models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.xPriv, wc.signRequest, &result, + ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.xPriv, *wc.signRequest, &result, ); err != nil { return nil, err } diff --git a/http_admin.go b/http_admin.go index 7840a72..c3b918f 100644 --- a/http_admin.go +++ b/http_admin.go @@ -12,11 +12,9 @@ import ( // AdminNewXpub will register an xPub func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError { // Adding a xpub needs to be signed by an admin key - fmt.Printf("here1 %+v\n\n", wc) if wc.adminXPriv == nil { return WrapError(ErrAdminKey) } - fmt.Println("here") jsonStr, err := json.Marshal(map[string]interface{}{ FieldMetadata: processMetadata(metadata), @@ -300,7 +298,7 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.xPriv, wc.signRequest, &transaction, + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.xPriv, *wc.signRequest, &transaction, ); err != nil { return nil, err } diff --git a/transactions.go b/transactions.go deleted file mode 100644 index e5bffb4..0000000 --- a/transactions.go +++ /dev/null @@ -1,117 +0,0 @@ -package walletclient - -// import ( -// "context" -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" - -// "github.com/bitcoin-sv/spv-wallet-go-client/transports" -// ) - -// // GetTransaction get a transaction by id -// func (b *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, transports.ResponseError) { -// return b.transport.GetTransaction(ctx, txID) -// } - -// // GetTransactions get all transactions matching search criteria -// func (b *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, -// metadata *models.Metadata, queryParams *transports.QueryParams, -// ) ([]*models.Transaction, transports.ResponseError) { -// return b.transport.GetTransactions(ctx, conditions, metadata, queryParams) -// } - -// // GetTransactionsCount get number of user transactions -// func (b *WalletClient) GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, -// metadata *models.Metadata, -// ) (int64, transports.ResponseError) { -// return b.transport.GetTransactionsCount(ctx, conditions, metadata) -// } - -// // DraftToRecipients initialize a new P2PKH draft transaction to a list of recipients -// func (b *WalletClient) DraftToRecipients(ctx context.Context, recipients []*transports.Recipients, -// metadata *models.Metadata, -// ) (*models.DraftTransaction, transports.ResponseError) { -// return b.transport.DraftToRecipients(ctx, recipients, metadata) -// } - -// // DraftTransaction initialize a new draft transaction -// func (b *WalletClient) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, -// metadata *models.Metadata, -// ) (*models.DraftTransaction, transports.ResponseError) { -// return b.transport.DraftTransaction(ctx, transactionConfig, metadata) -// } - -// // RecordTransaction record a new transaction -// func (b *WalletClient) RecordTransaction(ctx context.Context, hex, draftID string, -// metadata *models.Metadata, -// ) (*models.Transaction, transports.ResponseError) { -// return b.transport.RecordTransaction(ctx, hex, draftID, metadata) -// } - -// // UpdateTransactionMetadata update the metadata of a transaction -// func (b *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID string, -// metadata *models.Metadata, -// ) (*models.Transaction, transports.ResponseError) { -// return b.transport.UpdateTransactionMetadata(ctx, txID, metadata) -// } - -// // FinalizeTransaction will finalize the transaction -// func (b *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, transports.ResponseError) { -// res, err := transports.GetSignedHex(draft, b.xPriv) -// if err != nil { -// return "", transports.WrapError(err) -// } - -// return res, nil -// } - -// // SendToRecipients send to recipients -// func (b *WalletClient) SendToRecipients(ctx context.Context, recipients []*transports.Recipients, -// metadata *models.Metadata, -// ) (*models.Transaction, transports.ResponseError) { -// draft, err := b.DraftToRecipients(ctx, recipients, metadata) -// if err != nil { -// return nil, err -// } - -// var hex string -// if hex, err = b.FinalizeTransaction(draft); err != nil { -// return nil, err -// } - -// return b.RecordTransaction(ctx, hex, draft.ID, metadata) -// } diff --git a/transactions_test.go b/transactions_test.go index e919007..fd95740 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -1,325 +1,335 @@ package walletclient -import ( - "context" - "net/http" - "testing" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/libsv/go-bt/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" - "github.com/bitcoin-sv/spv-wallet-go-client/transports" -) - -// TestTransactions will test the Transaction methods -func TestTransactions(t *testing.T) { - t.Run("GetTransaction", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction", - Result: fixtures.MarshallForTestHandler(fixtures.Transaction), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - - // when - tx, err := client.GetTransaction(context.Background(), fixtures.Transaction.ID) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.Transaction, tx) - }) - - t.Run("GetTransactions", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction/search", - Result: fixtures.MarshallForTestHandler([]*models.Transaction{fixtures.Transaction}), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - conditions := map[string]interface{}{ - "fee": map[string]interface{}{ - "$lt": 100, - }, - "total_value": map[string]interface{}{ - "$lt": 740, - }, - } - - // when - txs, err := client.GetTransactions(context.Background(), conditions, fixtures.TestMetadata, nil) - - // then - assert.NoError(t, err) - assert.Equal(t, []*models.Transaction{fixtures.Transaction}, txs) - }) - - t.Run("GetTransactionsCount", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction/count", - Result: "1", - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - conditions := map[string]interface{}{ - "fee": map[string]interface{}{ - "$lt": 100, - }, - "total_value": map[string]interface{}{ - "$lt": 740, - }, - } - - // when - count, err := client.GetTransactionsCount(context.Background(), conditions, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, int64(1), count) - }) - - t.Run("RecordTransaction", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction/record", - Result: fixtures.MarshallForTestHandler(fixtures.Transaction), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - - // when - tx, err := client.RecordTransaction(context.Background(), fixtures.Transaction.Hex, "", fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.Transaction, tx) - }) - - t.Run("UpdateTransactionMetadata", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction", - Result: fixtures.MarshallForTestHandler(fixtures.Transaction), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - - // when - tx, err := client.UpdateTransactionMetadata(context.Background(), fixtures.Transaction.ID, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.Transaction, tx) - }) - - t.Run("SendToRecipients", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Queries: []*testTransportHandlerRequest{ - { - Path: "/transaction/record", - Result: func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) - }, - }, - { - Path: "/transaction", - Result: func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, fixtures.MarshallForTestHandler(fixtures.DraftTx)) - }, - }, - }, - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - recipients := []*transports.Recipients{{ - OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, - Satoshis: 1000, - To: fixtures.Destination.Address, - }} - - // when - tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.Transaction, tx) - }) - - t.Run("SendToRecipients - nil draft", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Queries: []*testTransportHandlerRequest{ - { - Path: "/transaction/record", - Result: func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) - }, - }, - { - Path: "/transaction", - Result: func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, "nil") - }, - }, - }, - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - recipients := []*transports.Recipients{{ - OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, - Satoshis: 1000, - To: fixtures.Destination.Address, - }} - - // when - tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) - - // then - assert.Error(t, err) - assert.Nil(t, tx) - }) - - t.Run("SendToRecipients - FinalizeTransaction error", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Queries: []*testTransportHandlerRequest{ - { - Path: "/transaction/record", - Result: func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) - }, - }, - { - Path: "/transaction", - Result: func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - mustWrite(w, fixtures.MarshallForTestHandler(models.DraftTransaction{})) - }, - }, - }, - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - recipients := []*transports.Recipients{{ - OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, - Satoshis: 1000, - To: fixtures.Destination.Address, - }} - - // when - tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) - - // then - assert.Error(t, err) - assert.Nil(t, tx) - }) - - t.Run("FinalizeTransaction", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction/record", - Result: fixtures.MarshallForTestHandler(fixtures.Transaction), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - - // when - signedHex, err := client.FinalizeTransaction(fixtures.DraftTx) - - txDraft, btErr := bt.NewTxFromString(signedHex) - require.NoError(t, btErr) - - // then - assert.NoError(t, err) - assert.Len(t, txDraft.Inputs, len(fixtures.DraftTx.Configuration.Inputs)) - assert.Len(t, txDraft.Outputs, len(fixtures.DraftTx.Configuration.Outputs)) - }) - - t.Run("CountUtxos", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/utxo/count", - ClientURL: fixtures.ServerURL, - Result: "0", - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - - // when - _, err := client.GetUtxosCount(context.Background(), map[string]interface{}{}, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - }) -} - -// TestDraftTransactions will test the DraftTransaction methods -func TestDraftTransactions(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/transaction", - Result: fixtures.MarshallForTestHandler(fixtures.DraftTx), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - t.Run("DraftToRecipients", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - recipients := []*transports.Recipients{{ - OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, - Satoshis: 1000, - To: fixtures.Destination.Address, - }} - - // when - draft, err := client.DraftToRecipients(context.Background(), recipients, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.DraftTx, draft) - }) - - t.Run("DraftTransaction", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - draft, err := client.DraftTransaction(context.Background(), &fixtures.DraftTx.Configuration, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.DraftTx, draft) - }) -} +// import ( +// "context" +// "net/http" +// "testing" +// +// +// +// +// +// +// +// +// +// + +// "github.com/bitcoin-sv/spv-wallet/models" +// "github.com/libsv/go-bt/v2" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// "github.com/bitcoin-sv/spv-wallet-go-client/transports" +// ) + +// // TestTransactions will test the Transaction methods +// func TestTransactions(t *testing.T) { +// t.Run("GetTransaction", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction", +// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) + +// // when +// tx, err := client.GetTransaction(context.Background(), fixtures.Transaction.ID) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.Transaction, tx) +// }) + +// t.Run("GetTransactions", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction/search", +// Result: fixtures.MarshallForTestHandler([]*models.Transaction{fixtures.Transaction}), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) +// conditions := map[string]interface{}{ +// "fee": map[string]interface{}{ +// "$lt": 100, +// }, +// "total_value": map[string]interface{}{ +// "$lt": 740, +// }, +// } + +// // when +// txs, err := client.GetTransactions(context.Background(), conditions, fixtures.TestMetadata, nil) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, []*models.Transaction{fixtures.Transaction}, txs) +// }) + +// t.Run("GetTransactionsCount", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction/count", +// Result: "1", +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) +// conditions := map[string]interface{}{ +// "fee": map[string]interface{}{ +// "$lt": 100, +// }, +// "total_value": map[string]interface{}{ +// "$lt": 740, +// }, +// } + +// // when +// count, err := client.GetTransactionsCount(context.Background(), conditions, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, int64(1), count) +// }) + +// t.Run("RecordTransaction", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction/record", +// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) + +// // when +// tx, err := client.RecordTransaction(context.Background(), fixtures.Transaction.Hex, "", fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.Transaction, tx) +// }) + +// t.Run("UpdateTransactionMetadata", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction", +// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) + +// // when +// tx, err := client.UpdateTransactionMetadata(context.Background(), fixtures.Transaction.ID, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.Transaction, tx) +// }) + +// t.Run("SendToRecipients", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Queries: []*testTransportHandlerRequest{ +// { +// Path: "/transaction/record", +// Result: func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) +// }, +// }, +// { +// Path: "/transaction", +// Result: func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.DraftTx)) +// }, +// }, +// }, +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) +// recipients := []*transports.Recipients{{ +// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, +// Satoshis: 1000, +// To: fixtures.Destination.Address, +// }} + +// // when +// tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.Transaction, tx) +// }) + +// t.Run("SendToRecipients - nil draft", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Queries: []*testTransportHandlerRequest{ +// { +// Path: "/transaction/record", +// Result: func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) +// }, +// }, +// { +// Path: "/transaction", +// Result: func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, "nil") +// }, +// }, +// }, +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) +// recipients := []*transports.Recipients{{ +// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, +// Satoshis: 1000, +// To: fixtures.Destination.Address, +// }} + +// // when +// tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) + +// // then +// assert.Error(t, err) +// assert.Nil(t, tx) +// }) + +// t.Run("SendToRecipients - FinalizeTransaction error", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Queries: []*testTransportHandlerRequest{ +// { +// Path: "/transaction/record", +// Result: func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) +// }, +// }, +// { +// Path: "/transaction", +// Result: func(w http.ResponseWriter, req *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// mustWrite(w, fixtures.MarshallForTestHandler(models.DraftTransaction{})) +// }, +// }, +// }, +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) +// recipients := []*transports.Recipients{{ +// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, +// Satoshis: 1000, +// To: fixtures.Destination.Address, +// }} + +// // when +// tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) + +// // then +// assert.Error(t, err) +// assert.Nil(t, tx) +// }) + +// t.Run("FinalizeTransaction", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction/record", +// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) + +// // when +// signedHex, err := client.FinalizeTransaction(fixtures.DraftTx) + +// txDraft, btErr := bt.NewTxFromString(signedHex) +// require.NoError(t, btErr) + +// // then +// assert.NoError(t, err) +// assert.Len(t, txDraft.Inputs, len(fixtures.DraftTx.Configuration.Inputs)) +// assert.Len(t, txDraft.Outputs, len(fixtures.DraftTx.Configuration.Outputs)) +// }) + +// t.Run("CountUtxos", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/utxo/count", +// ClientURL: fixtures.ServerURL, +// Result: "0", +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) + +// // when +// _, err := client.GetUtxosCount(context.Background(), map[string]interface{}{}, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// }) +// } + +// // TestDraftTransactions will test the DraftTransaction methods +// func TestDraftTransactions(t *testing.T) { +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/transaction", +// Result: fixtures.MarshallForTestHandler(fixtures.DraftTx), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } + +// t.Run("DraftToRecipients", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// recipients := []*transports.Recipients{{ +// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, +// Satoshis: 1000, +// To: fixtures.Destination.Address, +// }} + +// // when +// draft, err := client.DraftToRecipients(context.Background(), recipients, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.DraftTx, draft) +// }) + +// t.Run("DraftTransaction", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// draft, err := client.DraftTransaction(context.Background(), &fixtures.DraftTx.Configuration, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.DraftTx, draft) +// }) +// } diff --git a/transports/authentication.go b/transports/authentication.go deleted file mode 100644 index 3a51108..0000000 --- a/transports/authentication.go +++ /dev/null @@ -1,217 +0,0 @@ -package transports - -import ( - "encoding/hex" - "fmt" - "net/http" - "time" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/bitcoin-sv/spv-wallet/models/apierrors" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" - "github.com/libsv/go-bt/v2" - "github.com/libsv/go-bt/v2/bscript" - "github.com/libsv/go-bt/v2/sighash" - - "github.com/bitcoin-sv/spv-wallet-go-client/utils" -) - -// SetSignature will set the signature on the header for the request -func setSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) ResponseError { - // Create the signature - authData, err := createSignature(xPriv, bodyString) - if err != nil { - return WrapError(err) - } - - // Set the auth header - header.Set(models.AuthHeader, authData.XPub) - - return setSignatureHeaders(header, authData) -} - -// GetSignedHex will sign all the inputs using the given xPriv key -func GetSignedHex(dt *models.DraftTransaction, xPriv *bip32.ExtendedKey) (signedHex string, err error) { - var tx *bt.Tx - if tx, err = bt.NewTxFromString(dt.Hex); err != nil { - return - } - - // Enrich inputs - for index, draftInput := range dt.Configuration.Inputs { - tx.Inputs[index].PreviousTxSatoshis = draftInput.Satoshis - - dst := draftInput.Destination - if err = setPreviousTxScript(tx, uint32(index), &dst); err != nil { - return - } - - if err = setUnlockingScript(tx, uint32(index), xPriv, &dst); err != nil { - return - } - } - - // Return the signed hex - signedHex = tx.String() - return -} - -func setPreviousTxScript(tx *bt.Tx, inputIndex uint32, dst *models.Destination) (err error) { - var ls *bscript.Script - if ls, err = bscript.NewFromHexString(dst.LockingScript); err != nil { - return - } - - tx.Inputs[inputIndex].PreviousTxScript = ls - return -} - -func setUnlockingScript(tx *bt.Tx, inputIndex uint32, xPriv *bip32.ExtendedKey, dst *models.Destination) (err error) { - var key *bec.PrivateKey - if key, err = getDerivedKeyForDestination(xPriv, dst); err != nil { - return - } - - var s *bscript.Script - if s, err = getUnlockingScript(tx, inputIndex, key); err != nil { - return - } - - tx.Inputs[inputIndex].UnlockingScript = s - return -} - -func getDerivedKeyForDestination(xPriv *bip32.ExtendedKey, dst *models.Destination) (key *bec.PrivateKey, err error) { - // Derive the child key (m/chain/num) - var derivedKey *bip32.ExtendedKey - if derivedKey, err = bitcoin.GetHDKeyByPath(xPriv, dst.Chain, dst.Num); err != nil { - return - } - - // Derive key for paymail destination (m/chain/num/paymailNum) - if dst.PaymailExternalDerivationNum != nil { - if derivedKey, err = derivedKey.Child( - *dst.PaymailExternalDerivationNum, - ); err != nil { - return - } - } - - if key, err = bitcoin.GetPrivateKeyFromHDKey(derivedKey); err != nil { - return - } - - return -} - -// GetUnlockingScript will generate an unlocking script -func getUnlockingScript(tx *bt.Tx, inputIndex uint32, privateKey *bec.PrivateKey) (*bscript.Script, error) { - sigHashFlags := sighash.AllForkID - - sigHash, err := tx.CalcInputSignatureHash(inputIndex, sigHashFlags) - if err != nil { - return nil, err - } - - var sig *bec.Signature - if sig, err = privateKey.Sign(sigHash); err != nil { - return nil, err - } - - pubKey := privateKey.PubKey().SerialiseCompressed() - signature := sig.Serialise() - - var script *bscript.Script - if script, err = bscript.NewP2PKHUnlockingScript(pubKey, signature, sigHashFlags); err != nil { - return nil, err - } - - return script, nil -} - -// createSignature will create a signature for the given key & body contents -func createSignature(xPriv *bip32.ExtendedKey, bodyString string) (payload *models.AuthPayload, err error) { - // No key? - if xPriv == nil { - err = apierrors.ErrMissingXPriv - return - } - - // Get the xPub - payload = new(models.AuthPayload) - if payload.XPub, err = bitcoin.GetExtendedPublicKey( - xPriv, - ); err != nil { // Should never error if key is correct - return - } - - // auth_nonce is a random unique string to seed the signing message - // this can be checked server side to make sure the request is not being replayed - if payload.AuthNonce, err = utils.RandomHex(32); err != nil { // Should never error if key is correct - return - } - - // Derive the address for signing - var key *bip32.ExtendedKey - if key, err = utils.DeriveChildKeyFromHex( - xPriv, payload.AuthNonce, - ); err != nil { - return - } - - var privateKey *bec.PrivateKey - if privateKey, err = bitcoin.GetPrivateKeyFromHDKey(key); err != nil { - return // Should never error if key is correct - } - - return createSignatureCommon(payload, bodyString, privateKey) -} - -// createSignatureCommon will create a signature -func createSignatureCommon(payload *models.AuthPayload, bodyString string, privateKey *bec.PrivateKey) (*models.AuthPayload, error) { - // Create the auth header hash - payload.AuthHash = utils.Hash(bodyString) - - // auth_time is the current time and makes sure a request can not be sent after 30 secs - payload.AuthTime = time.Now().UnixMilli() - - key := payload.XPub - if key == "" && payload.AccessKey != "" { - key = payload.AccessKey - } - - // Signature, using bitcoin signMessage - var err error - if payload.Signature, err = bitcoin.SignMessage( - hex.EncodeToString(privateKey.Serialise()), - getSigningMessage(key, payload), - true, - ); err != nil { - return nil, err - } - - return payload, nil -} - -// getSigningMessage will build the signing message string -func getSigningMessage(xPub string, auth *models.AuthPayload) string { - return fmt.Sprintf("%s%s%s%d", xPub, auth.AuthHash, auth.AuthNonce, auth.AuthTime) -} - -func setSignatureHeaders(header *http.Header, authData *models.AuthPayload) ResponseError { - // Create the auth header hash - header.Set(models.AuthHeaderHash, authData.AuthHash) - - // Set the nonce - header.Set(models.AuthHeaderNonce, authData.AuthNonce) - - // Set the time - header.Set(models.AuthHeaderTime, fmt.Sprintf("%d", authData.AuthTime)) - - // Set the signature - header.Set(models.AuthSignature, authData.Signature) - - return nil -} diff --git a/transports/client_options.go b/transports/client_options.go deleted file mode 100644 index 73c6f27..0000000 --- a/transports/client_options.go +++ /dev/null @@ -1,90 +0,0 @@ -package transports - -import ( - "net/http" - - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" -) - -// WithXPriv will set the xPriv -func WithXPriv(xPriv *bip32.ExtendedKey) ClientOps { - return func(c *Client) { - if c != nil { - c.xPriv = xPriv - } - } -} - -// WithXPub will set the xPub -func WithXPub(xPub *bip32.ExtendedKey) ClientOps { - return func(c *Client) { - if c != nil { - c.xPub = xPub - } - } -} - -// WithAccessKey will set the access key -func WithAccessKey(accessKey *bec.PrivateKey) ClientOps { - return func(c *Client) { - if c != nil { - c.accessKey = accessKey - } - } -} - -// WithHTTP will overwrite the default client with a custom client -func WithHTTP(serverURL string) ClientOps { - return func(c *Client) { - if c != nil { - c.transport = NewTransportService(&TransportHTTP{ - server: serverURL, - signRequest: c.signRequest, - adminXPriv: c.adminXPriv, - httpClient: &http.Client{}, - xPriv: c.xPriv, - xPub: c.xPub, - accessKey: c.accessKey, - }) - } - } -} - -// WithHTTPClient will overwrite the default client with a custom client -func WithHTTPClient(serverURL string, httpClient *http.Client) ClientOps { - return func(c *Client) { - if c != nil { - c.transport = NewTransportService(&TransportHTTP{ - server: serverURL, - signRequest: c.signRequest, - adminXPriv: c.adminXPriv, - httpClient: httpClient, - xPriv: c.xPriv, - xPub: c.xPub, - accessKey: c.accessKey, - }) - } - } -} - -// WithAdminKey will set the admin key for admin requests -func WithAdminKey(adminKey string) ClientOps { - return func(c *Client) { - if c != nil { - c.adminKey = adminKey - } - } -} - -// WithSignRequest will set whether to sign all requests -func WithSignRequest(signRequest bool) ClientOps { - return func(c *Client) { - if c != nil { - c.signRequest = signRequest - if c.transport != nil { - c.transport.SetSignRequest(signRequest) - } - } - } -} diff --git a/transports/config.go b/transports/config.go deleted file mode 100644 index c98fc12..0000000 --- a/transports/config.go +++ /dev/null @@ -1,97 +0,0 @@ -package transports - -import "github.com/bitcoin-sv/spv-wallet/models" - -// TransportType the type of transport being used ('http' for usage or 'mock' for testing) -type TransportType string - -// SPVWalletUserAgent the spv wallet user agent sent to the spv wallet. -const SPVWalletUserAgent = "SPVWallet: go-client" - -const ( - // SPVWalletTransportHTTP uses the http transport for all spv-wallet actions - SPVWalletTransportHTTP TransportType = "http" - - // SPVWalletTransportMock uses the mock transport for all spv-wallet actions - SPVWalletTransportMock TransportType = "mock" -) - -// Recipients is a struct for recipients -type Recipients struct { - OpReturn *models.OpReturn `json:"op_return"` - Satoshis uint64 `json:"satoshis"` - To string `json:"to"` -} - -// QueryParams object to use when limiting and sorting database query results -type QueryParams struct { - Page int `json:"page,omitempty"` - PageSize int `json:"page_size,omitempty"` - OrderByField string `json:"order_by_field,omitempty"` - SortDirection string `json:"sort_direction,omitempty"` -} - -const ( - // FieldMetadata is the field name for metadata - FieldMetadata = "metadata" - - // FieldQueryParams is the field name for the query params - FieldQueryParams = "params" - - // FieldXpubKey is the field name for xpub key - FieldXpubKey = "key" - - // FieldXpubID is the field name for xpub id - FieldXpubID = "xpub_id" - - // FieldAddress is the field name for paymail address - FieldAddress = "address" - - // FieldPublicName is the field name for (paymail) public name - FieldPublicName = "public_name" - - // FieldAvatar is the field name for (paymail) avatar - FieldAvatar = "avatar" - - // FieldConditions is the field name for conditions - FieldConditions = "conditions" - - // FieldTo is the field name for "to" - FieldTo = "to" - - // FieldSatoshis is the field name for "satoshis" - FieldSatoshis = "satoshis" - - // FieldOpReturn is the field name for "op_return" - FieldOpReturn = "op_return" - - // FieldConfig is the field name for "config" - FieldConfig = "config" - - // FieldOutputs is the field name for "outputs" - FieldOutputs = "outputs" - - // FieldHex is the field name for "hex" - FieldHex = "hex" - - // FieldReferenceID is the field name for "reference_id" - FieldReferenceID = "reference_id" - - // FieldID is the id field for most models - FieldID = "id" - - // FieldLockingScript is the field for locking script - FieldLockingScript = "locking_script" - - // FieldUserAgent is the field for storing the user agent - FieldUserAgent = "user_agent" - - // FieldTransactionConfig is the field for the config of a new transaction - FieldTransactionConfig = "transaction_config" - - // FieldTransactionID is the field for transaction ID - FieldTransactionID = "tx_id" - - // FieldOutputIndex is the field for "output_index" - FieldOutputIndex = "output_index" -) diff --git a/transports/errors.go b/transports/errors.go deleted file mode 100644 index 2a60e77..0000000 --- a/transports/errors.go +++ /dev/null @@ -1,72 +0,0 @@ -package transports - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" -) - -// ErrAdminKey admin key not set -var ErrAdminKey = errors.New("an admin key must be set to be able to create an xpub") - -// ErrNoClientSet is when no client is set -var ErrNoClientSet = errors.New("no transport client set") - -// ResError is a struct which contain information about error -type ResError struct { - StatusCode int - Message string -} - -// ResponseError is an interface for error -type ResponseError interface { - Error() string - GetStatusCode() int -} - -// WrapError wraps an error into ResponseError -func WrapError(err error) ResponseError { - if err == nil { - return nil - } - - return &ResError{ - StatusCode: http.StatusInternalServerError, - Message: err.Error(), - } -} - -// WrapResponseError wraps a http response into ResponseError -func WrapResponseError(res *http.Response) ResponseError { - if res == nil { - return nil - } - - var errorMsg string - - err := json.NewDecoder(res.Body).Decode(&errorMsg) - if err != nil { - // if EOF, then body is empty and we return response status as error message - if !errors.Is(err, io.EOF) { - errorMsg = fmt.Sprintf("spv-wallet error message can't be decoded. Reason: %s", err.Error()) - } - errorMsg = res.Status - } - - return &ResError{ - StatusCode: res.StatusCode, - Message: errorMsg, - } -} - -// Error returns the error message -func (e *ResError) Error() string { - return e.Message -} - -// GetStatusCode returns the status code of error -func (e *ResError) GetStatusCode() int { - return e.StatusCode -} diff --git a/transports/http.go b/transports/http.go deleted file mode 100644 index b124a74..0000000 --- a/transports/http.go +++ /dev/null @@ -1,721 +0,0 @@ -package transports - -import ( - "bytes" - "context" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "strconv" - - "github.com/bitcoin-sv/spv-wallet-go-client/utils" - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/bitcoin-sv/spv-wallet/models/apierrors" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" -) - -// TransportHTTP is the struct for HTTP -type TransportHTTP struct { - accessKey *bec.PrivateKey - adminXPriv *bip32.ExtendedKey - httpClient *http.Client - server string - signRequest bool - xPriv *bip32.ExtendedKey - xPub *bip32.ExtendedKey -} - -// Init will initialize -func (h *TransportHTTP) Init() error { - return nil -} - -// SetSignRequest turn the signing of the http request on or off -func (h *TransportHTTP) SetSignRequest(signRequest bool) { - h.signRequest = signRequest -} - -// IsSignRequest return whether to sign all requests -func (h *TransportHTTP) IsSignRequest() bool { - return h.signRequest -} - -// SetAdminKey set the admin key -func (h *TransportHTTP) SetAdminKey(adminKey *bip32.ExtendedKey) { - h.adminXPriv = adminKey -} - -// GetXPub will get the xpub of the current xpub -func (h *TransportHTTP) GetXPub(ctx context.Context) (*models.Xpub, ResponseError) { - var xPub models.Xpub - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/xpub", nil, h.xPriv, true, &xPub, - ); err != nil { - return nil, err - } - - return &xPub, nil -} - -// UpdateXPubMetadata update the metadata of the logged in xpub -func (h *TransportHTTP) UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - - var xPub models.Xpub - if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/xpub", jsonStr, h.xPriv, true, &xPub, - ); err != nil { - return nil, err - } - - return &xPub, nil -} - -// GetAccessKey will get an access key by id -func (h *TransportHTTP) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { - var accessKey models.AccessKey - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, h.xPriv, true, &accessKey, - ); err != nil { - return nil, err - } - - return &accessKey, nil -} - -// GetAccessKeys will get all access keys matching the metadata filter -func (h *TransportHTTP) GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadataConditions), - }) - if err != nil { - return nil, WrapError(err) - } - var accessKey []*models.AccessKey - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/access-key/search", jsonStr, h.xPriv, true, &accessKey, - ); err != nil { - return nil, err - } - - return accessKey, nil -} - -// GetAccessKeysCount will get the count of access keys -func (h *TransportHTTP) GetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/access-key/count", jsonStr, h.xPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// RevokeAccessKey will revoke an access key by id -func (h *TransportHTTP) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { - var accessKey models.AccessKey - if err := h.doHTTPRequest( - ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, h.xPriv, true, &accessKey, - ); err != nil { - return nil, err - } - - return &accessKey, nil -} - -// CreateAccessKey will create new access key -func (h *TransportHTTP) CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - var accessKey models.AccessKey - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/access-key", jsonStr, h.xPriv, true, &accessKey, - ); err != nil { - return nil, err - } - - return &accessKey, nil -} - -// GetDestinationByID will get a destination by id -func (h *TransportHTTP) GetDestinationByID(ctx context.Context, id string) (*models.Destination, ResponseError) { - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetDestinationByAddress will get a destination by address -func (h *TransportHTTP) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, ResponseError) { - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetDestinationByLockingScript will get a destination by locking script -func (h *TransportHTTP) GetDestinationByLockingScript(ctx context.Context, lockingScript string) (*models.Destination, ResponseError) { - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetDestinations will get all destinations matching the metadata filter -func (h *TransportHTTP) GetDestinations(ctx context.Context, metadataConditions *models.Metadata) ([]*models.Destination, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadataConditions), - }) - if err != nil { - return nil, WrapError(err) - } - var destinations []*models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/destination/search", jsonStr, h.xPriv, true, &destinations, - ); err != nil { - return nil, err - } - - return destinations, nil -} - -// GetDestinationsCount will get the count of destinations matching the metadata filter -func (h *TransportHTTP) GetDestinationsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/destination/count", jsonStr, h.xPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// NewDestination will create a new destination and return it -func (h *TransportHTTP) NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/destination", jsonStr, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// UpdateDestinationMetadataByID updates the destination metadata by id -func (h *TransportHTTP) UpdateDestinationMetadataByID(ctx context.Context, id string, - metadata *models.Metadata, -) (*models.Destination, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldID: id, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// UpdateDestinationMetadataByAddress updates the destination metadata by address -func (h *TransportHTTP) UpdateDestinationMetadataByAddress(ctx context.Context, address string, - metadata *models.Metadata, -) (*models.Destination, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script -func (h *TransportHTTP) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, - metadata *models.Metadata, -) (*models.Destination, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldLockingScript: lockingScript, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - - var destination models.Destination - if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, h.xPriv, true, &destination, - ); err != nil { - return nil, err - } - - return &destination, nil -} - -// GetTransaction will get a transaction by ID -func (h *TransportHTTP) GetTransaction(ctx context.Context, txID string) (*models.Transaction, ResponseError) { - var transaction models.Transaction - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, h.xPriv, h.signRequest, &transaction, - ); err != nil { - return nil, err - } - - return &transaction, nil -} - -// 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) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadataConditions), - FieldQueryParams: queryParams, - }) - if err != nil { - return nil, WrapError(err) - } - - var transactions []*models.Transaction - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/transaction/search", jsonStr, h.xPriv, h.signRequest, &transactions, - ); err != nil { - return nil, err - } - - return transactions, nil -} - -// GetTransactionsCount get number of user transactions -func (h *TransportHTTP) GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/transaction/count", jsonStr, h.xPriv, h.signRequest, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// DraftToRecipients is a draft transaction to a slice of recipients -func (h *TransportHTTP) DraftToRecipients(ctx context.Context, recipients []*Recipients, - metadata *models.Metadata, -) (*models.DraftTransaction, ResponseError) { - outputs := make([]map[string]interface{}, 0) - for _, recipient := range recipients { - outputs = append(outputs, map[string]interface{}{ - FieldTo: recipient.To, - FieldSatoshis: recipient.Satoshis, - FieldOpReturn: recipient.OpReturn, - }) - } - - return h.createDraftTransaction(ctx, map[string]interface{}{ - FieldConfig: map[string]interface{}{ - FieldOutputs: outputs, - }, - FieldMetadata: processMetadata(metadata), - }) -} - -// DraftTransaction is a draft transaction -func (h *TransportHTTP) DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, - metadata *models.Metadata, -) (*models.DraftTransaction, ResponseError) { - return h.createDraftTransaction(ctx, map[string]interface{}{ - FieldConfig: transactionConfig, - FieldMetadata: processMetadata(metadata), - }) -} - -// createDraftTransaction will create a draft transaction -func (h *TransportHTTP) createDraftTransaction(ctx context.Context, - jsonData map[string]interface{}, -) (*models.DraftTransaction, ResponseError) { - jsonStr, err := json.Marshal(jsonData) - if err != nil { - return nil, WrapError(err) - } - - var draftTransaction *models.DraftTransaction - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/transaction", jsonStr, h.xPriv, true, &draftTransaction, - ); err != nil { - return nil, err - } - if draftTransaction == nil { - return nil, WrapError(apierrors.ErrDraftNotFound) - } - - return draftTransaction, nil -} - -// RecordTransaction will record a transaction -func (h *TransportHTTP) RecordTransaction(ctx context.Context, hex, referenceID string, - metadata *models.Metadata, -) (*models.Transaction, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldHex: hex, - FieldReferenceID: referenceID, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - - var transaction models.Transaction - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/transaction/record", jsonStr, h.xPriv, h.signRequest, &transaction, - ); err != nil { - return nil, err - } - - return &transaction, nil -} - -// UpdateTransactionMetadata update the metadata of a transaction -func (h *TransportHTTP) UpdateTransactionMetadata(ctx context.Context, txID string, - metadata *models.Metadata, -) (*models.Transaction, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldID: txID, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - - var transaction models.Transaction - if err := h.doHTTPRequest( - ctx, http.MethodPatch, "/transaction", jsonStr, h.xPriv, h.signRequest, &transaction, - ); err != nil { - return nil, err - } - - return &transaction, nil -} - -// SetSignatureFromAccessKey will set the signature on the header for the request from an access key -func SetSignatureFromAccessKey(header *http.Header, privateKeyHex, bodyString string) ResponseError { - // Create the signature - authData, err := createSignatureAccessKey(privateKeyHex, bodyString) - if err != nil { - return WrapError(err) - } - - // Set the auth header - header.Set(models.AuthAccessKey, authData.AccessKey) - - return setSignatureHeaders(header, authData) -} - -// GetUtxo will get a utxo by transaction ID -func (h *TransportHTTP) GetUtxo(ctx context.Context, txID string, outputIndex uint32) (*models.Utxo, ResponseError) { - outputIndexStr := strconv.FormatUint(uint64(outputIndex), 10) - - url := fmt.Sprintf("/utxo?%s=%s&%s=%s", FieldTransactionID, txID, FieldOutputIndex, outputIndexStr) - - var utxo models.Utxo - if err := h.doHTTPRequest( - ctx, http.MethodGet, url, nil, h.xPriv, true, &utxo, - ); err != nil { - return nil, err - } - - return &utxo, nil -} - -// GetUtxos will get a list of utxos filtered by conditions and metadata -func (h *TransportHTTP) GetUtxos(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Utxo, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - FieldQueryParams: queryParams, - }) - if err != nil { - return nil, WrapError(err) - } - - var utxos []*models.Utxo - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/utxo/search", jsonStr, h.xPriv, h.signRequest, &utxos, - ); err != nil { - return nil, err - } - - return utxos, nil -} - -// GetUtxosCount will get the count of utxos filtered by conditions and metadata -func (h *TransportHTTP) GetUtxosCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/utxo/count", jsonStr, h.xPriv, h.signRequest, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// createSignatureAccessKey will create a signature for the given access key & body contents -func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *models.AuthPayload, err error) { - // No key? - if privateKeyHex == "" { - err = apierrors.ErrMissingAccessKey - return - } - - var privateKey *bec.PrivateKey - if privateKey, err = bitcoin.PrivateKeyFromString( - privateKeyHex, - ); err != nil { - return - } - publicKey := privateKey.PubKey() - - // Get the xPub - payload = new(models.AuthPayload) - payload.AccessKey = hex.EncodeToString(publicKey.SerialiseCompressed()) - - // auth_nonce is a random unique string to seed the signing message - // this can be checked server side to make sure the request is not being replayed - payload.AuthNonce, err = utils.RandomHex(32) - if err != nil { - return nil, err - } - - return createSignatureCommon(payload, bodyString, privateKey) -} - -// doHTTPRequest will create and submit the HTTP request -func (h *TransportHTTP) doHTTPRequest(ctx context.Context, method string, path string, - rawJSON []byte, xPriv *bip32.ExtendedKey, sign bool, responseJSON interface{}, -) ResponseError { - req, err := http.NewRequestWithContext(ctx, method, h.server+path, bytes.NewBuffer(rawJSON)) - if err != nil { - return WrapError(err) - } - req.Header.Set("Content-Type", "application/json") - - if xPriv != nil { - err := h.authenticateWithXpriv(sign, req, xPriv, rawJSON) - if err != nil { - return err - } - } else { - err := h.authenticateWithAccessKey(req, rawJSON) - if err != nil { - return err - } - } - - var resp *http.Response - defer func() { - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - }() - if resp, err = h.httpClient.Do(req); err != nil { - return WrapError(err) - } - if resp.StatusCode >= http.StatusBadRequest { - return WrapResponseError(resp) - } - - if responseJSON == nil { - return nil - } - - err = json.NewDecoder(resp.Body).Decode(&responseJSON) - if err != nil { - return WrapError(err) - } - return nil -} - -func (h *TransportHTTP) authenticateWithXpriv(sign bool, req *http.Request, xPriv *bip32.ExtendedKey, rawJSON []byte) ResponseError { - if sign { - if err := addSignature(&req.Header, xPriv, string(rawJSON)); err != nil { - return err - } - } else { - var xPub string - xPub, err := bitcoin.GetExtendedPublicKey(xPriv) - if err != nil { - return WrapError(err) - } - req.Header.Set(models.AuthHeader, xPub) - req.Header.Set("", xPub) - } - return nil -} - -func (h *TransportHTTP) authenticateWithAccessKey(req *http.Request, rawJSON []byte) ResponseError { - return SetSignatureFromAccessKey(&req.Header, hex.EncodeToString(h.accessKey.Serialise()), string(rawJSON)) -} - -// 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/accepted/"+paymail, nil, h.xPriv, h.signRequest, nil, - ); err != nil { - return err - } - - return nil -} - -// 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/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 -} - -// 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/http_admin.go b/transports/http_admin.go deleted file mode 100644 index 02ab6b1..0000000 --- a/transports/http_admin.go +++ /dev/null @@ -1,369 +0,0 @@ -package transports - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/bitcoin-sv/spv-wallet/models" -) - -// AdminNewXpub will register an xPub -func (h *TransportHTTP) AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError { - // Adding a xpub needs to be signed by an admin key - if h.adminXPriv == nil { - return WrapError(ErrAdminKey) - } - - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), - FieldXpubKey: rawXPub, - }) - if err != nil { - return WrapError(err) - } - - var xPubData models.Xpub - - return h.doHTTPRequest( - ctx, http.MethodPost, "/admin/xpub", jsonStr, h.adminXPriv, true, &xPubData, - ) -} - -// AdminGetStatus get whether admin key is valid -func (h *TransportHTTP) AdminGetStatus(ctx context.Context) (bool, ResponseError) { - var status bool - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/admin/status", nil, h.xPriv, true, &status, - ); err != nil { - return false, err - } - - return status, nil -} - -// AdminGetStats get admin stats -func (h *TransportHTTP) AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) { - var stats *models.AdminStats - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/admin/stats", nil, h.xPriv, true, &stats, - ); err != nil { - return nil, err - } - - return stats, nil -} - -// AdminGetAccessKeys get all access keys filtered by conditions -func (h *TransportHTTP) AdminGetAccessKeys(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.AccessKey, ResponseError) { - var models []*models.AccessKey - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/access-keys/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetAccessKeysCount get a count of all the access keys filtered by conditions -func (h *TransportHTTP) AdminGetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/access-keys/count") -} - -// AdminGetBlockHeaders get all block headers filtered by conditions -func (h *TransportHTTP) AdminGetBlockHeaders(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.BlockHeader, ResponseError) { - var models []*models.BlockHeader - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions -func (h *TransportHTTP) AdminGetBlockHeadersCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/block-headers/count") -} - -// AdminGetDestinations get all block destinations filtered by conditions -func (h *TransportHTTP) AdminGetDestinations(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Destination, ResponseError) { - var models []*models.Destination - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/destinations/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetDestinationsCount get a count of all the destinations filtered by conditions -func (h *TransportHTTP) AdminGetDestinationsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/destinations/count") -} - -// AdminGetPaymail get a paymail by address -func (h *TransportHTTP) AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - }) - if err != nil { - return nil, WrapError(err) - } - - var model *models.PaymailAddress - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/get", jsonStr, h.xPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminGetPaymails get all block paymails filtered by conditions -func (h *TransportHTTP) AdminGetPaymails(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.PaymailAddress, ResponseError) { - var models []*models.PaymailAddress - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/paymails/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetPaymailsCount get a count of all the paymails filtered by conditions -func (h *TransportHTTP) AdminGetPaymailsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/paymails/count") -} - -// AdminCreatePaymail create a new paymail for a xpub -func (h *TransportHTTP) AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldXpubKey: rawXPub, - FieldAddress: address, - FieldPublicName: publicName, - FieldAvatar: avatar, - }) - if err != nil { - return nil, WrapError(err) - } - - var model *models.PaymailAddress - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/create", jsonStr, h.xPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminDeletePaymail delete a paymail address from the database -func (h *TransportHTTP) AdminDeletePaymail(ctx context.Context, address string) ResponseError { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - }) - if err != nil { - return WrapError(err) - } - - if err := h.doHTTPRequest( - ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, h.xPriv, true, nil, - ); err != nil { - return err - } - - return nil -} - -// AdminGetTransactions get all block transactions filtered by conditions -func (h *TransportHTTP) AdminGetTransactions(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Transaction, ResponseError) { - var models []*models.Transaction - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/transactions/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetTransactionsCount get a count of all the transactions filtered by conditions -func (h *TransportHTTP) AdminGetTransactionsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/transactions/count") -} - -// AdminGetUtxos get all block utxos filtered by conditions -func (h *TransportHTTP) AdminGetUtxos(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Utxo, ResponseError) { - var models []*models.Utxo - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/utxos/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetUtxosCount get a count of all the utxos filtered by conditions -func (h *TransportHTTP) AdminGetUtxosCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/utxos/count") -} - -// AdminGetXPubs get all block xpubs filtered by conditions -func (h *TransportHTTP) AdminGetXPubs(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Xpub, ResponseError) { - var models []*models.Xpub - if err := h.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/xpubs/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetXPubsCount get a count of all the xpubs filtered by conditions -func (h *TransportHTTP) AdminGetXPubsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return h.adminCount(ctx, conditions, metadata, "/admin/xpubs/count") -} - -func (h *TransportHTTP) adminGetModels(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, path string, models interface{}, -) ResponseError { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - FieldQueryParams: queryParams, - }) - if err != nil { - return WrapError(err) - } - - if err := h.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, h.xPriv, true, &models, - ); err != nil { - return err - } - - return nil -} - -func (h *TransportHTTP) adminCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, path string) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := h.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, h.xPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// AdminRecordTransaction will record a transaction as an admin -func (h *TransportHTTP) AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldHex: hex, - }) - if err != nil { - return nil, WrapError(err) - } - - var transaction models.Transaction - if err := h.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, h.xPriv, h.signRequest, &transaction, - ); err != nil { - return nil, err - } - - return &transaction, nil -} - -// AdminGetSharedConfig gets the shared config -func (h *TransportHTTP) AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) { - var model *models.SharedConfig - if err := h.doHTTPRequest( - ctx, http.MethodGet, "/admin/shared-config", nil, h.xPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminGetContacts executes an HTTP POST request to search for contacts based on specified conditions, metadata, and query parameters. -func (h *TransportHTTP) AdminGetContacts(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 contacts []*models.Contact - err = h.doHTTPRequest(ctx, http.MethodPost, "/admin/contact/search", jsonStr, h.adminXPriv, true, &contacts) - return contacts, WrapError(err) -} - -// AdminUpdateContact executes an HTTP PATCH request to update a specific contact's full name using their ID. -func (h *TransportHTTP) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - "fullName": fullName, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - var contact models.Contact - err = h.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/%s", id), jsonStr, h.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - -// AdminDeleteContact executes an HTTP DELETE request to remove a contact using their ID. -func (h *TransportHTTP) AdminDeleteContact(ctx context.Context, id string) ResponseError { - err := h.doHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/admin/contact/%s", id), nil, h.adminXPriv, true, nil) - return WrapError(err) -} - -// AdminAcceptContact executes an HTTP PATCH request to mark a contact as accepted using their ID. -func (h *TransportHTTP) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, ResponseError) { - var contact models.Contact - err := h.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/accepted/%s", id), nil, h.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - -// AdminRejectContact executes an HTTP PATCH request to mark a contact as rejected using their ID. -func (h *TransportHTTP) AdminRejectContact(ctx context.Context, id string) (*models.Contact, ResponseError) { - var contact models.Contact - err := h.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, h.adminXPriv, true, &contact) - return &contact, WrapError(err) -} diff --git a/transports/interface.go b/transports/interface.go deleted file mode 100644 index a7ceeea..0000000 --- a/transports/interface.go +++ /dev/null @@ -1,104 +0,0 @@ -package transports - -import ( - "context" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/libsv/go-bk/bip32" -) - -// XpubService is the xPub related requests -type XpubService interface { - GetXPub(ctx context.Context) (*models.Xpub, ResponseError) - UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, ResponseError) -} - -// AccessKeyService is the access key related requests -type AccessKeyService interface { - CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, ResponseError) - GetAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) - GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, ResponseError) - GetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) -} - -// DestinationService is the destination related requests -type DestinationService interface { - GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, ResponseError) - GetDestinationByID(ctx context.Context, id string) (*models.Destination, ResponseError) - GetDestinationByLockingScript(ctx context.Context, lockingScript string) (*models.Destination, ResponseError) - GetDestinations(ctx context.Context, metadataConditions *models.Metadata) ([]*models.Destination, ResponseError) - GetDestinationsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, ResponseError) - UpdateDestinationMetadataByAddress(ctx context.Context, lockingScript string, metadata *models.Metadata) (*models.Destination, ResponseError) - UpdateDestinationMetadataByID(ctx context.Context, id string, metadata *models.Metadata) (*models.Destination, ResponseError) - UpdateDestinationMetadataByLockingScript(ctx context.Context, address string, metadata *models.Metadata) (*models.Destination, ResponseError) -} - -// TransactionService is the transaction related requests -type TransactionService interface { - DraftToRecipients(ctx context.Context, recipients []*Recipients, metadata *models.Metadata) (*models.DraftTransaction, ResponseError) - DraftTransaction(ctx context.Context, transactionConfig *models.TransactionConfig, metadata *models.Metadata) (*models.DraftTransaction, ResponseError) - GetTransaction(ctx context.Context, txID string) (*models.Transaction, ResponseError) - GetTransactions(ctx context.Context, conditions map[string]interface{}, metadataConditions *models.Metadata, queryParams *QueryParams) ([]*models.Transaction, ResponseError) - GetTransactionsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - RecordTransaction(ctx context.Context, hex, referenceID string, metadata *models.Metadata) (*models.Transaction, ResponseError) - UpdateTransactionMetadata(ctx context.Context, txID string, metadata *models.Metadata) (*models.Transaction, ResponseError) - GetUtxo(ctx context.Context, txID string, outputIndex uint32) (*models.Utxo, ResponseError) - GetUtxos(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Utxo, ResponseError) - 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 - 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 -type AdminService interface { - AdminGetStatus(ctx context.Context) (bool, ResponseError) - AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) - AdminGetAccessKeys(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.AccessKey, ResponseError) - AdminGetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminGetBlockHeaders(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.BlockHeader, ResponseError) - AdminGetBlockHeadersCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminGetDestinations(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Destination, ResponseError) - AdminGetDestinationsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, ResponseError) - AdminGetPaymails(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.PaymailAddress, ResponseError) - AdminGetPaymailsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, ResponseError) - AdminDeletePaymail(ctx context.Context, address string) ResponseError - AdminGetTransactions(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Transaction, ResponseError) - AdminGetTransactionsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminGetUtxos(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Utxo, ResponseError) - AdminGetUtxosCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError - AdminGetXPubs(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Xpub, ResponseError) - AdminGetXPubsCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata) (int64, ResponseError) - AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, ResponseError) - AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) - AdminGetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *QueryParams) ([]*models.Contact, ResponseError) - AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) - AdminDeleteContact(ctx context.Context, id string) ResponseError - AdminAcceptContact(ctx context.Context, id string) (*models.Contact, ResponseError) - AdminRejectContact(ctx context.Context, id string) (*models.Contact, ResponseError) -} - -// TransportService the transport service interface -type TransportService interface { - AccessKeyService - AdminService - ContactService - DestinationService - TransactionService - XpubService - Init() error - IsSignRequest() bool - SetAdminKey(adminKey *bip32.ExtendedKey) - SetSignRequest(signRequest bool) -} diff --git a/transports/transports.go b/transports/transports.go deleted file mode 100644 index cd94475..0000000 --- a/transports/transports.go +++ /dev/null @@ -1,72 +0,0 @@ -// Package transports encapsulates the different ways to communicate with SPV Wallet -package transports - -import ( - "net/http" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bk/bip32" -) - -// Client is the transport client -type Client struct { - accessKey *bec.PrivateKey - adminKey string - adminXPriv *bip32.ExtendedKey - signRequest bool - transport TransportService - xPriv *bip32.ExtendedKey - xPub *bip32.ExtendedKey -} - -// ClientOps are the client options functions -type ClientOps func(c *Client) - -// addSignature will add the signature to the request -func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) ResponseError { - return setSignature(header, xPriv, bodyString) -} - -// NewTransport create a new transport service object -func NewTransport(opts ...ClientOps) (TransportService, error) { - client := Client{} - - for _, opt := range opts { - opt(&client) - } - - if client.transport == nil { - return nil, ErrNoClientSet - } - - if err := client.transport.Init(); err != nil { - return nil, err - } - - if client.adminKey != "" { - adminXPriv, err := bip32.NewKeyFromString(client.adminKey) - if err != nil { - return nil, err - } - client.adminXPriv = adminXPriv - client.transport.SetAdminKey(adminXPriv) - } - - return client.transport, nil -} - -// NewTransportService create a new transport service interface -func NewTransportService(transportService TransportService) TransportService { - return transportService -} - -// processMetadata will process the metadata -func processMetadata(metadata *models.Metadata) *models.Metadata { - if metadata == nil { - m := make(models.Metadata) - metadata = &m - } - - return metadata -} diff --git a/transports/transports_test.go b/transports/transports_test.go deleted file mode 100644 index 396fc14..0000000 --- a/transports/transports_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package transports - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestWithSignRequest will test the method WithSignRequest() -func TestWithSignRequest(t *testing.T) { - - t.Run("get opts", func(t *testing.T) { - opt := WithSignRequest(false) - assert.IsType(t, *new(ClientOps), opt) - }) - - t.Run("sign request false", func(t *testing.T) { - opts := []ClientOps{ - WithSignRequest(false), - WithHTTP(""), - } - c, err := NewTransport(opts...) - require.NoError(t, err) - require.NotNil(t, c) - - assert.Equal(t, false, c.IsSignRequest()) - }) - - t.Run("sign request true", func(t *testing.T) { - opts := []ClientOps{ - WithSignRequest(true), - WithHTTP(""), - } - c, err := NewTransport(opts...) - require.NoError(t, err) - require.NotNil(t, c) - - assert.Equal(t, true, c.IsSignRequest()) - }) -} diff --git a/walletclient.go b/walletclient.go index 10ec25b..e18fa80 100644 --- a/walletclient.go +++ b/walletclient.go @@ -13,20 +13,53 @@ import ( // WalletClient is the spv wallet Go client representation. type WalletClient struct { - accessKeyString string - xPrivString string - xPubString string + accessKeyString *string + xPrivString *string + xPubString *string accessKey *bec.PrivateKey adminXPriv *bip32.ExtendedKey httpClient *http.Client - server string - signRequest bool + server *string + signRequest *bool xPriv *bip32.ExtendedKey xPub *bip32.ExtendedKey } +func NewWalletClientWithXPrivate(xPriv, serverURL string, sign bool) (*WalletClient, error) { + return newWalletClient( + &WithXPriv{XPrivString: &xPriv}, + &WithHTTP{ServerURL: &serverURL}, + &WithSignRequest{Sign: &sign}, + ) +} + +func NewWalletClientWithXPublic(xPub, serverURL string, sign bool) (*WalletClient, error) { + return newWalletClient( + &WithXPub{XPubString: &xPub}, + &WithHTTP{ServerURL: &serverURL}, + &WithSignRequest{Sign: &sign}, + ) +} + +func NewWalletClientWithAdminKey(adminKey, serverURL string, sign bool) (*WalletClient, error) { + return newWalletClient( + &WithXPriv{XPrivString: &adminKey}, + &WithAdminKey{AdminKeyString: &adminKey}, + &WithHTTP{ServerURL: &serverURL}, + &WithSignRequest{Sign: &sign}, + ) +} + +func NewWalletClientWithAccessKey(accessKey, serverURL string, sign bool) (*WalletClient, error) { + return newWalletClient( + &WithAccessKey{AccessKeyString: &accessKey}, + &WithHTTP{ServerURL: &serverURL}, + &WithSignRequest{Sign: &sign}, + ) +} + // New creates a new WalletClient using the provided configuration options. -func New(configurators ...WalletClientConfigurator) (*WalletClient, error) { +func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, error) { client := &WalletClient{} for _, configurator := range configurators { @@ -45,18 +78,18 @@ func New(configurators ...WalletClientConfigurator) (*WalletClient, error) { func (c *WalletClient) initializeKeys() error { var err error switch { - case c.xPrivString != "": - if c.xPriv, err = bitcoin.GenerateHDKeyFromString(c.xPrivString); err != nil { + case c.xPrivString != nil: + if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*c.xPrivString); err != nil { return err } if c.xPub, err = c.xPriv.Neuter(); err != nil { return err } - case c.xPubString != "": - if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(c.xPubString); err != nil { + case c.xPubString != nil: + if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(*c.xPubString); err != nil { return err } - case c.accessKeyString != "": + case c.accessKeyString != nil: return c.initializeAccessKey() default: return errors.New("no keys provided for initialization") @@ -70,8 +103,8 @@ func (c *WalletClient) initializeAccessKey() error { var privateKey *bec.PrivateKey var decodedWIF *wif.WIF - if decodedWIF, err = wif.DecodeWIF(c.accessKeyString); err != nil { - if privateKey, err = bitcoin.PrivateKeyFromString(c.accessKeyString); err != nil { + if decodedWIF, err = wif.DecodeWIF(*c.accessKeyString); err != nil { + if privateKey, err = bitcoin.PrivateKeyFromString(*c.accessKeyString); err != nil { return errors.Wrap(err, "failed to decode access key") } } else { diff --git a/walletclient2.go b/walletclient2.go deleted file mode 100644 index b380996..0000000 --- a/walletclient2.go +++ /dev/null @@ -1,144 +0,0 @@ -// // Package walletclient is a Go client for interacting with Spv Wallet. -package walletclient - -// import ( -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoinschema/go-bitcoin/v2" -// "github.com/libsv/go-bk/bec" -// "github.com/libsv/go-bk/bip32" -// "github.com/libsv/go-bk/wif" -// "github.com/pkg/errors" - -// "github.com/bitcoin-sv/spv-wallet-go-client/transports" -// ) - -// // ClientOps are used for client options -// type ClientOps func(c *WalletClient) - -// // // WalletClient is the spv wallet go client representation. -// // type WalletClient struct { -// // transports.TransportService -// // accessKey *bec.PrivateKey -// // accessKeyString string -// // transport transports.TransportService -// // transportOptions []transports.ClientOps -// // xPriv *bip32.ExtendedKey -// // xPrivString string -// // xPub *bip32.ExtendedKey -// // xPubString string -// // } - -// // New create a new wallet client -// func NewOld(opts ...ClientOps) (*WalletClient, error) { -// client := &WalletClient{} - -// for _, opt := range opts { -// opt(client) -// } - -// var err error -// if client.xPrivString != "" { -// if client.xPriv, err = bitcoin.GenerateHDKeyFromString(client.xPrivString); err != nil { -// return nil, err -// } -// if client.xPub, err = client.xPriv.Neuter(); err != nil { -// return nil, err -// } -// } else if client.xPubString != "" { -// client.xPriv = nil -// if client.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(client.xPubString); err != nil { -// return nil, err -// } -// } else if client.accessKeyString != "" { -// client.xPriv = nil -// client.xPub = nil - -// var privateKey *bec.PrivateKey -// var decodedWIF *wif.WIF -// if decodedWIF, err = wif.DecodeWIF(client.accessKeyString); err != nil { -// // try as a hex string -// var errHex error -// if privateKey, errHex = bitcoin.PrivateKeyFromString(client.accessKeyString); errHex != nil { -// return nil, errors.Wrap(err, errHex.Error()) -// } -// } else { -// privateKey = decodedWIF.PrivKey -// } -// client.accessKey = privateKey -// } else { -// return nil, errors.New("no keys available") -// } - -// transportOptions := make([]transports.ClientOps, 0) -// if client.xPriv != nil { -// transportOptions = append(transportOptions, transports.WithXPriv(client.xPriv)) -// transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) -// } else if client.xPub != nil { -// transportOptions = append(transportOptions, transports.WithXPub(client.xPub)) -// } else if client.accessKey != nil { -// transportOptions = append(transportOptions, transports.WithAccessKey(client.accessKey)) -// } -// if len(client.transportOptions) > 0 { -// transportOptions = append(transportOptions, client.transportOptions...) -// } - -// if client.transport, err = transports.NewTransport(transportOptions...); err != nil { -// return nil, err -// } - -// client.TransportService = client.transport - -// return client, nil -// } - -// // SetAdminKey set the admin key to use to create new xpubs -// func (b *WalletClient) SetAdminKey(adminKeyString string) error { -// adminKey, err := bip32.NewKeyFromString(adminKeyString) -// if err != nil { -// return err -// } - -// b.transport.SetAdminKey(adminKey) - -// return nil -// } - -// // SetSignRequest turn the signing of the http request on or off -// func (b *WalletClient) SetSignRequest(signRequest bool) { -// b.transport.SetSignRequest(signRequest) -// } - -// // IsSignRequest return whether to sign all requests -// func (b *WalletClient) IsSignRequest() bool { -// return b.transport.IsSignRequest() -// } - -// // GetTransport returns the current transport service -// func (b *WalletClient) GetTransport() *transports.TransportService { -// return &b.transport -// } diff --git a/walletclient_test.go b/walletclient_test.go index bb3a076..fc6e9b9 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -1,392 +1,114 @@ package walletclient -// import ( -// "context" -// "io" -// "net/http" -// "net/http/httptest" -// "testing" -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" -// "github.com/bitcoinschema/go-bitcoin/v2" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/require" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// "github.com/bitcoin-sv/spv-wallet-go-client/transports" -// ) - -// // localRoundTripper is an http.RoundTripper that executes HTTP transactions -// // by using handler directly, instead of going over an HTTP connection. -// type localRoundTripper struct { -// handler http.Handler -// } - -// func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { -// w := httptest.NewRecorder() -// l.handler.ServeHTTP(w, req) -// return w.Result(), nil -// } - -// func mustWrite(w io.Writer, s string) { -// _, err := io.WriteString(w, s) -// if err != nil { -// panic(err) -// } -// } - -// type testTransportHandler struct { -// ClientURL string -// Client func(serverURL string, httpClient *http.Client) ClientOps -// Path string -// Queries []*testTransportHandlerRequest -// Result string -// Type string -// } - -// type testTransportHandlerRequest struct { -// Path string -// Result func(w http.ResponseWriter, req *http.Request) -// } - -// // TestNewWalletClient will test the TestNewWalletClient method -// func TestNewWalletClient(t *testing.T) { -// t.Run("no keys", func(t *testing.T) { -// client, err := New() -// assert.Error(t, err) -// assert.Nil(t, client) -// }) - -// t.Run("empty xpriv", func(t *testing.T) { -// client, err := New( -// WithXPriv(""), -// ) -// assert.Error(t, err) -// assert.Nil(t, client) -// }) - -// t.Run("invalid xpriv", func(t *testing.T) { -// client, err := New( -// WithXPriv("invalid-xpriv"), -// ) -// assert.Error(t, err) -// assert.Nil(t, client) -// }) - -// t.Run("valid client", func(t *testing.T) { -// client, err := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// require.NoError(t, err) -// assert.IsType(t, WalletClient{}, *client) -// }) - -// t.Run("valid xPub client", func(t *testing.T) { -// client, err := New( -// WithXPub(fixtures.XPubString), -// WithHTTP(fixtures.ServerURL), -// ) -// require.NoError(t, err) -// assert.IsType(t, WalletClient{}, *client) -// }) - -// t.Run("invalid xPub client", func(t *testing.T) { -// client, err := New( -// WithXPub("invalid-xpub"), -// WithHTTP(fixtures.ServerURL), -// ) -// assert.Error(t, err) -// assert.Nil(t, client) -// }) - -// t.Run("valid access keys", func(t *testing.T) { -// client, err := New( -// WithAccessKey(fixtures.AccessKeyString), -// WithHTTP(fixtures.ServerURL), -// ) -// require.NoError(t, err) -// assert.IsType(t, WalletClient{}, *client) -// }) - -// t.Run("invalid access keys", func(t *testing.T) { -// client, err := New( -// WithAccessKey("invalid-access-key"), -// WithHTTP(fixtures.ServerURL), -// ) -// assert.Error(t, err) -// assert.Nil(t, client) -// }) - -// t.Run("valid access key WIF", func(t *testing.T) { -// wifKey, _ := bitcoin.PrivateKeyToWif(fixtures.AccessKeyString) -// client, err := New( -// WithAccessKey(wifKey.String()), -// WithHTTP(fixtures.ServerURL), -// ) -// require.NoError(t, err) -// assert.IsType(t, WalletClient{}, *client) -// }) -// } - -// // TestSetAdminKey will test the admin key setter -// func TestSetAdminKey(t *testing.T) { -// t.Run("invalid", func(t *testing.T) { -// client, _ := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// err := client.SetAdminKey("") -// assert.Error(t, err) -// }) - -// t.Run("valid", func(t *testing.T) { -// client, _ := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// err := client.SetAdminKey(fixtures.XPrivString) -// assert.NoError(t, err) -// }) - -// t.Run("invalid with", func(t *testing.T) { -// _, err := New( -// WithXPriv(fixtures.XPrivString), -// WithAdminKey("rest"), -// WithHTTP(fixtures.ServerURL), -// ) -// assert.Error(t, err) -// }) - -// t.Run("valid with", func(t *testing.T) { -// _, err := New( -// WithXPriv(fixtures.XPrivString), -// WithAdminKey(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// assert.NoError(t, err) -// }) -// } - -// // TestSetSignRequest will test the sign request setter -// func TestSetSignRequest(t *testing.T) { -// t.Run("true", func(t *testing.T) { -// client, _ := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// client.SetSignRequest(true) -// assert.True(t, client.IsSignRequest()) -// }) - -// t.Run("false", func(t *testing.T) { -// client, _ := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// client.SetSignRequest(false) -// assert.False(t, client.IsSignRequest()) -// }) - -// t.Run("false by default", func(t *testing.T) { -// client, err := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// require.NoError(t, err) -// assert.False(t, client.IsSignRequest()) -// }) -// } - -// // TestGetTransport will test the GetTransport method -// func TestGetTransport(t *testing.T) { -// t.Run("GetTransport", func(t *testing.T) { -// client, _ := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// ) -// transport := client.GetTransport() -// assert.IsType(t, &transports.TransportHTTP{}, *transport) -// }) - -// t.Run("client GetTransport", func(t *testing.T) { -// client, _ := New( -// WithXPriv(fixtures.XPrivString), -// WithHTTP(fixtures.ServerURL), -// WithAdminKey(fixtures.XPrivString), -// WithSignRequest(false), -// ) -// transport := client.GetTransport() -// assert.IsType(t, &transports.TransportHTTP{}, *transport) -// }) -// } - -// func TestAuthenticationWithOnlyAccessKey(t *testing.T) { -// anyConditions := make(map[string]interface{}, 0) -// var anyMetadataConditions *models.Metadata -// anyParam := "sth" - -// testCases := []struct { -// caseTitle string -// path string -// clientMethod func(*WalletClient) (any, error) -// }{ -// { -// caseTitle: "GetXPub", -// path: "/xpub", -// clientMethod: func(c *WalletClient) (any, error) { return c.GetXPub(context.Background()) }, -// }, -// { -// caseTitle: "GetAccessKey", -// path: "/access-key", -// clientMethod: func(c *WalletClient) (any, error) { return c.GetAccessKey(context.Background(), anyParam) }, -// }, -// { -// caseTitle: "GetAccessKeys", -// path: "/access-key", -// clientMethod: func(c *WalletClient) (any, error) { -// return c.GetAccessKeys(context.Background(), anyMetadataConditions) -// }, -// }, -// { -// caseTitle: "GetDestinationByID", -// path: "/destination", -// clientMethod: func(c *WalletClient) (any, error) { return c.GetDestinationByID(context.Background(), anyParam) }, -// }, -// { -// caseTitle: "GetDestinationByAddress", -// path: "/destination", -// clientMethod: func(c *WalletClient) (any, error) { -// return c.GetDestinationByAddress(context.Background(), anyParam) -// }, -// }, -// { -// caseTitle: "GetDestinationByLockingScript", -// path: "/destination", -// clientMethod: func(c *WalletClient) (any, error) { -// return c.GetDestinationByLockingScript(context.Background(), anyParam) -// }, -// }, -// { -// caseTitle: "GetDestinations", -// path: "/destination/search", -// clientMethod: func(c *WalletClient) (any, error) { -// return c.GetDestinations(context.Background(), nil) -// }, -// }, -// { -// caseTitle: "GetTransaction", -// path: "/transaction", -// clientMethod: func(c *WalletClient) (any, error) { -// return c.GetTransaction(context.Background(), fixtures.Transaction.ID) -// }, -// }, -// { -// caseTitle: "GetTransactions", -// path: "/transaction/search", -// clientMethod: func(c *WalletClient) (any, error) { -// return c.GetTransactions(context.Background(), anyConditions, anyMetadataConditions, &transports.QueryParams{}) -// }, -// }, -// } - -// for _, test := range testCases { -// t.Run(test.caseTitle, func(t *testing.T) { -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Queries: []*testTransportHandlerRequest{{ -// Path: test.path, -// Result: func(w http.ResponseWriter, req *http.Request) { -// assertAuthHeaders(t, req) -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, "{}") -// }, -// }}, -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } - -// client := getTestWalletClientWithOpts(transportHandler, WithAccessKey(fixtures.AccessKeyString)) - -// _, err := test.clientMethod(client) -// if err != nil { -// t.Log(err) -// } -// }) -// } -// } - -// func assertAuthHeaders(t *testing.T, req *http.Request) { -// assert.Empty(t, req.Header.Get("x-auth-xpub"), "Header value x-auth-xpub should be empty") -// assert.NotEmpty(t, req.Header.Get("x-auth-key"), "Header value x-auth-key should not be empty") -// assert.NotEmpty(t, req.Header.Get("x-auth-time"), "Header value x-auth-time should not be empty") -// assert.NotEmpty(t, req.Header.Get("x-auth-hash"), "Header value x-auth-hash should not be empty") -// assert.NotEmpty(t, req.Header.Get("x-auth-nonce"), "Header value x-auth-nonce should not be empty") -// assert.NotEmpty(t, req.Header.Get("x-auth-signature"), "Header value x-auth-signature should not be empty") -// } - -// func getTestWalletClient(transportHandler testTransportHandler, adminKey bool) *WalletClient { -// opts := []ClientOps{ -// WithXPriv(fixtures.XPrivString), -// } -// if adminKey { -// opts = append(opts, WithAdminKey(fixtures.XPrivString)) -// } - -// return getTestWalletClientWithOpts(transportHandler, opts...) -// } - -// func getTestWalletClientWithOpts(transportHandler testTransportHandler, options ...ClientOps) *WalletClient { -// mux := http.NewServeMux() -// if transportHandler.Queries != nil { -// for _, query := range transportHandler.Queries { -// mux.HandleFunc(query.Path, query.Result) -// } -// } else { -// mux.HandleFunc(transportHandler.Path, func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, transportHandler.Result) -// }) -// } -// httpclient := &http.Client{Transport: localRoundTripper{handler: mux}} - -// opts := []ClientOps{ -// transportHandler.Client(transportHandler.ClientURL, httpclient), -// } - -// opts = append(opts, options...) - -// client, _ := New(opts...) - -// return client -// } +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +) + +// localRoundTripper is an http.RoundTripper that executes HTTP transactions +// by using handler directly, instead of going over an HTTP connection. +type localRoundTripper struct { + handler http.Handler +} + +func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + w := httptest.NewRecorder() + l.handler.ServeHTTP(w, req) + return w.Result(), nil +} + +func mustWrite(w io.Writer, s string) { + _, err := io.WriteString(w, s) + if err != nil { + panic(err) + } +} + +type testTransportHandler struct { + ClientURL string + Client func(serverURL string, httpClient *http.Client) ClientOps + Path string + Queries []*testTransportHandlerRequest + Result string + Type string +} + +type testTransportHandlerRequest struct { + Path string + Result func(w http.ResponseWriter, req *http.Request) +} + +// TestNewWalletClient will test the TestNewWalletClient method +func TestNewWalletClient(t *testing.T) { + + t.Run("empty xpriv", func(t *testing.T) { + client, err := NewWalletClientWithXPrivate("", fixtures.ServerURL, false) + assert.Error(t, err) + assert.Nil(t, client) + }) + + t.Run("invalid xpriv", func(t *testing.T) { + client, err := NewWalletClientWithXPrivate("invalid-xpriv", fixtures.ServerURL, false) + assert.Error(t, err) + assert.Nil(t, client) + }) + + t.Run("valid client", func(t *testing.T) { + client, err := NewWalletClientWithXPrivate(fixtures.XPrivString, fixtures.ServerURL, false) + + require.NoError(t, err) + assert.IsType(t, WalletClient{}, *client) + }) + + t.Run("valid xPub client", func(t *testing.T) { + client, err := NewWalletClientWithXPublic(fixtures.XPubString, fixtures.ServerURL, false) + require.NoError(t, err) + assert.IsType(t, WalletClient{}, *client) + }) + + t.Run("invalid xPub client", func(t *testing.T) { + client, err := NewWalletClientWithXPublic("invalid-xpub", fixtures.ServerURL, false) + assert.Error(t, err) + assert.Nil(t, client) + }) + + t.Run("valid access keys", func(t *testing.T) { + client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, fixtures.ServerURL, false) + require.NoError(t, err) + assert.IsType(t, WalletClient{}, *client) + }) + + t.Run("invalid access keys", func(t *testing.T) { + client, err := NewWalletClientWithAccessKey("invalid-access-key", fixtures.ServerURL, false) + assert.Error(t, err) + assert.Nil(t, client) + }) + + t.Run("valid access key WIF", func(t *testing.T) { + wifKey, _ := bitcoin.PrivateKeyToWif(fixtures.AccessKeyString) + client, err := NewWalletClientWithAccessKey(wifKey.String(), fixtures.ServerURL, false) + require.NoError(t, err) + assert.IsType(t, WalletClient{}, *client) + }) +} + +// TestSetSignRequest will test the sign request setter +func TestSetSignRequest(t *testing.T) { + t.Run("true", func(t *testing.T) { + client, _ := NewWalletClientWithXPrivate(fixtures.XPrivString, fixtures.ServerURL, true) + assert.True(t, client.IsSignRequest()) + }) + + t.Run("false", func(t *testing.T) { + client, _ := NewWalletClientWithXPrivate(fixtures.XPrivString, fixtures.ServerURL, false) + assert.False(t, client.IsSignRequest()) + }) +} From 85f978cb818153045538dc32c54dd97f548ebcaf Mon Sep 17 00:00:00 2001 From: ac4ch Date: Sun, 12 May 2024 16:58:30 +0200 Subject: [PATCH 05/27] latest working --- access_keys_test.go | 152 +++++++++---------- contacts.go | 40 ----- contacts_test.go | 347 +++++++++++++++++++++++-------------------- destinations_test.go | 270 ++++++++++++++++++--------------- http.go | 3 +- totp_test.go | 294 ++++++++++++++++++------------------ walletclient_test.go | 129 ++++------------ xpubs_test.go | 97 ++++++------ 8 files changed, 648 insertions(+), 684 deletions(-) delete mode 100644 contacts.go diff --git a/access_keys_test.go b/access_keys_test.go index fa9de93..af70c08 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -1,77 +1,79 @@ package walletclient -import ( - "context" - "testing" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/stretchr/testify/assert" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -) - -// TestAccessKeys will test the AccessKey methods -func TestAccessKeys(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/access-key", - Result: fixtures.MarshallForTestHandler(fixtures.AccessKey), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - t.Run("GetAccessKey", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - accessKey, err := client.GetAccessKey(context.Background(), fixtures.AccessKey.ID) - - // then - assert.NoError(t, err) - assert.Equal(t, accessKey, fixtures.AccessKey) - }) - - t.Run("GetAccessKeys", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/access-key/search", - Result: fixtures.MarshallForTestHandler([]*models.AccessKey{fixtures.AccessKey}), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, true) - - // when - accessKeys, err := client.GetAccessKeys(context.Background(), fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, accessKeys, []*models.AccessKey{fixtures.AccessKey}) - }) - - t.Run("CreateAccessKey", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - accessKey, err := client.CreateAccessKey(context.Background(), fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, accessKey, fixtures.AccessKey) - }) - - t.Run("RevokeAccessKey", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - accessKey, err := client.RevokeAccessKey(context.Background(), fixtures.AccessKey.ID) - - // then - assert.NoError(t, err) - assert.Equal(t, accessKey, fixtures.AccessKey) - }) -} +// import ( +// "context" +// "testing" +// +// + +// "github.com/bitcoin-sv/spv-wallet/models" +// "github.com/stretchr/testify/assert" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// ) + +// // TestAccessKeys will test the AccessKey methods +// func TestAccessKeys(t *testing.T) { +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/access-key", +// Result: fixtures.MarshallForTestHandler(fixtures.AccessKey), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } + +// t.Run("GetAccessKey", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, true) + +// // when +// accessKey, err := client.GetAccessKey(context.Background(), fixtures.AccessKey.ID) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, accessKey, fixtures.AccessKey) +// }) + +// t.Run("GetAccessKeys", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/access-key/search", +// Result: fixtures.MarshallForTestHandler([]*models.AccessKey{fixtures.AccessKey}), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, true) + +// // when +// accessKeys, err := client.GetAccessKeys(context.Background(), fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, accessKeys, []*models.AccessKey{fixtures.AccessKey}) +// }) + +// t.Run("CreateAccessKey", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, true) + +// // when +// accessKey, err := client.CreateAccessKey(context.Background(), fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, accessKey, fixtures.AccessKey) +// }) + +// t.Run("RevokeAccessKey", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, true) + +// // when +// accessKey, err := client.RevokeAccessKey(context.Background(), fixtures.AccessKey.ID) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, accessKey, fixtures.AccessKey) +// }) +// } diff --git a/contacts.go b/contacts.go deleted file mode 100644 index 9e30116..0000000 --- a/contacts.go +++ /dev/null @@ -1,40 +0,0 @@ -package walletclient - -// // 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) -// } - -// // 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 try to confirm the contact -// func (b *WalletClient) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) transports.ResponseError { -// isTotpValid, err := b.ValidateTotpForContact(contact, passcode, requesterPaymail, period, 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 -// 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 08d87a6..a014052 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -1,160 +1,191 @@ package walletclient -import ( - "context" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -) - -// TestContactActionsRouting will test routing -func TestContactActionsRouting(t *testing.T) { - tcs := []struct { - name string - route string - responsePayload string - f func(c *WalletClient) error - }{ - { - name: "RejectContact", - route: "/contact/rejected/", - responsePayload: "{}", - f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) }, - }, - { - name: "AcceptContact", - route: "/contact/accepted/", - responsePayload: "{}", - f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, - }, - { - 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 - }, - }, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - // given - tmq := testTransportHandler{ - Type: fixtures.RequestType, - Path: tc.route, - Result: tc.responsePayload, - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString)) - - // when - err := tc.f(client) - - // then - assert.NoError(t, err) - }) - } - -} - -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, - } - - clientMaker := func(opts ...ClientOps) (*WalletClient, error) { - return getTestWalletClientWithOpts(tmq, opts...), nil - } - - alice := makeMockUser("alice", clientMaker) - bob := makeMockUser("bob", clientMaker) - - totp, err := alice.client.GenerateTotpForContact(bob.contact, 30, 2) - require.NoError(t, err) - - // when - result := bob.client.ConfirmContact(context.Background(), alice.contact, totp, bob.paymail, 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, - } - - clientMaker := func(opts ...ClientOps) (*WalletClient, error) { - return getTestWalletClientWithOpts(tmq, opts...), nil - } - - alice := makeMockUser("alice", clientMaker) - bob := makeMockUser("bob", clientMaker) - - totp, err := alice.client.GenerateTotpForContact(bob.contact, 30, 2) - require.NoError(t, err) - - //make sure the wrongTotp is not the same as the generated one - wrongTotp := incrementDigits(totp) //the length should remain the same - - // when - result := bob.client.ConfirmContact(context.Background(), alice.contact, wrongTotp, bob.paymail, 30, 2) - - // then - require.NotNil(t, result) - require.Equal(t, result.Error(), "totp is invalid") - }) -} - -// incrementDigits takes a string of digits and increments each digit by 1. -// Digits wrap around such that '9' becomes '0'. -func incrementDigits(input string) string { - var result strings.Builder - - for _, c := range input { - if c == '9' { - result.WriteRune('0') - } else { - result.WriteRune(c + 1) - } - } - - return result.String() -} +// import ( +// "context" +// "strings" +// "testing" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// ) + +// // TestContactActionsRouting will test routing +// func TestContactActionsRouting(t *testing.T) { +// tcs := []struct { +// name string +// route string +// responsePayload string +// f func(c *WalletClient) error +// }{ +// { +// name: "RejectContact", +// route: "/contact/rejected/", +// responsePayload: "{}", +// f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) }, +// }, +// { +// name: "AcceptContact", +// route: "/contact/accepted/", +// responsePayload: "{}", +// f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, +// }, +// { +// 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 +// }, +// }, +// } + +// for _, tc := range tcs { +// t.Run(tc.name, func(t *testing.T) { +// // given +// tmq := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: tc.route, +// Result: tc.responsePayload, +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } + +// client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString)) + +// // when +// err := tc.f(client) + +// // then +// assert.NoError(t, err) +// }) +// } + +// } + +// 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, +// } + +// clientMaker := func(opts ...ClientOps) (*WalletClient, error) { +// return getTestWalletClientWithOpts(tmq, opts...), nil +// } + +// alice := makeMockUser("alice", clientMaker) +// bob := makeMockUser("bob", clientMaker) + +// totp, err := alice.client.GenerateTotpForContact(bob.contact, 30, 2) +// require.NoError(t, err) + +// // when +// result := bob.client.ConfirmContact(context.Background(), alice.contact, totp, bob.paymail, 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, +// } + +// clientMaker := func(opts ...ClientOps) (*WalletClient, error) { +// return getTestWalletClientWithOpts(tmq, opts...), nil +// } + +// alice := makeMockUser("alice", clientMaker) +// bob := makeMockUser("bob", clientMaker) + +// totp, err := alice.client.GenerateTotpForContact(bob.contact, 30, 2) +// require.NoError(t, err) + +// //make sure the wrongTotp is not the same as the generated one +// wrongTotp := incrementDigits(totp) //the length should remain the same + +// // when +// result := bob.client.ConfirmContact(context.Background(), alice.contact, wrongTotp, bob.paymail, 30, 2) + +// // then +// require.NotNil(t, result) +// require.Equal(t, result.Error(), "totp is invalid") +// }) +// } + +// // incrementDigits takes a string of digits and increments each digit by 1. +// // Digits wrap around such that '9' becomes '0'. +// func incrementDigits(input string) string { +// var result strings.Builder + +// for _, c := range input { +// if c == '9' { +// result.WriteRune('0') +// } else { +// result.WriteRune(c + 1) +// } +// } + +// return result.String() +// } diff --git a/destinations_test.go b/destinations_test.go index 176ffcc..684055f 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -1,125 +1,149 @@ package walletclient -import ( - "context" - "testing" - - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/stretchr/testify/assert" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -) - -// TestDestinations will test the Destinations methods -func TestDestinations(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/destination", - Result: fixtures.MarshallForTestHandler(fixtures.Destination), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - t.Run("GetDestinationByID", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.GetDestinationByID(context.Background(), fixtures.Destination.ID) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) - - t.Run("GetDestinationByAddress", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.GetDestinationByAddress(context.Background(), fixtures.Destination.Address) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) - - t.Run("GetDestinationByLockingScript", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.GetDestinationByLockingScript(context.Background(), fixtures.Destination.LockingScript) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) - - t.Run("GetDestinations", func(t *testing.T) { - // given - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/destination/search", - Result: fixtures.MarshallForTestHandler([]*models.Destination{fixtures.Destination}), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - client := getTestWalletClient(transportHandler, false) - - // when - destinations, err := client.GetDestinations(context.Background(), fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, destinations, []*models.Destination{fixtures.Destination}) - }) - - t.Run("NewDestination", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.NewDestination(context.Background(), fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) - - t.Run("UpdateDestinationMetadataByID", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.UpdateDestinationMetadataByID(context.Background(), fixtures.Destination.ID, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) - - t.Run("UpdateDestinationMetadataByAddress", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.UpdateDestinationMetadataByAddress(context.Background(), fixtures.Destination.Address, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) - - t.Run("UpdateDestinationMetadataByLockingScript", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, false) - - // when - destination, err := client.UpdateDestinationMetadataByLockingScript(context.Background(), fixtures.Destination.LockingScript, fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, destination, fixtures.Destination) - }) -} +// import ( +// "context" +// "testing" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// "github.com/bitcoin-sv/spv-wallet/models" +// "github.com/stretchr/testify/assert" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// ) + +// // TestDestinations will test the Destinations methods +// func TestDestinations(t *testing.T) { +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/destination", +// Result: fixtures.MarshallForTestHandler(fixtures.Destination), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } + +// t.Run("GetDestinationByID", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.GetDestinationByID(context.Background(), fixtures.Destination.ID) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) + +// t.Run("GetDestinationByAddress", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.GetDestinationByAddress(context.Background(), fixtures.Destination.Address) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) + +// t.Run("GetDestinationByLockingScript", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.GetDestinationByLockingScript(context.Background(), fixtures.Destination.LockingScript) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) + +// t.Run("GetDestinations", func(t *testing.T) { +// // given +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/destination/search", +// Result: fixtures.MarshallForTestHandler([]*models.Destination{fixtures.Destination}), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } +// client := getTestWalletClient(transportHandler, false) + +// // when +// destinations, err := client.GetDestinations(context.Background(), fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destinations, []*models.Destination{fixtures.Destination}) +// }) + +// t.Run("NewDestination", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.NewDestination(context.Background(), fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) + +// t.Run("UpdateDestinationMetadataByID", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.UpdateDestinationMetadataByID(context.Background(), fixtures.Destination.ID, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) + +// t.Run("UpdateDestinationMetadataByAddress", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.UpdateDestinationMetadataByAddress(context.Background(), fixtures.Destination.Address, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) + +// t.Run("UpdateDestinationMetadataByLockingScript", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, false) + +// // when +// destination, err := client.UpdateDestinationMetadataByLockingScript(context.Background(), fixtures.Destination.LockingScript, fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, destination, fixtures.Destination) +// }) +// } diff --git a/http.go b/http.go index 0c457d3..00147bb 100644 --- a/http.go +++ b/http.go @@ -16,7 +16,6 @@ import ( "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" - "github.com/bitcoin-sv/spv-wallet-go-client/transports" "github.com/bitcoin-sv/spv-wallet-go-client/utils" ) @@ -693,7 +692,7 @@ func (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName str return wc.UpsertContactForPaymail(ctx, paymail, fullName, metadata, "") } -func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, transports.ResponseError) { +func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, ResponseError) { payload := map[string]interface{}{ "fullName": fullName, FieldMetadata: processMetadata(metadata), diff --git a/totp_test.go b/totp_test.go index 0eccd6e..07a2140 100644 --- a/totp_test.go +++ b/totp_test.go @@ -1,143 +1,155 @@ package walletclient -import ( - "encoding/hex" - "testing" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" - "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/libsv/go-bk/bip32" - "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 - clientMaker := func(opts ...ClientOps) (*WalletClient, error) { - allOptions := append(opts, WithHTTP("localhost:3001")) - return New(allOptions...) - } - alice := makeMockUser("alice", clientMaker) - bob := makeMockUser("bob", clientMaker) - - pass, err := alice.client.GenerateTotpForContact(bob.contact, 3600, 2) - require.NoError(t, err) - - // when - result, err := bob.client.ValidateTotpForContact(alice.contact, pass, bob.paymail, 3600, 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, "", fixtures.PaymailAddress, 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, "", fixtures.PaymailAddress, 30, 2) - - // then - require.ErrorContains(t, err, "contact's PubKey is invalid:") - - }) -} - -type mockUser struct { - contact *models.Contact - client *WalletClient - paymail string -} - -func makeMockUser(name string, clientMaker func(opts ...ClientOps) (*WalletClient, error)) mockUser { - keys, _ := xpriv.Generate() - paymail := name + "@example.com" - client, _ := clientMaker(WithXPriv(keys.XPriv())) - pki := makeMockPKI(keys.XPub().String()) - contact := models.Contact{PubKey: pki, Paymail: paymail} - return mockUser{ - contact: &contact, - client: client, - paymail: paymail, - } -} - -func makeMockPKI(xpub string) string { - xPub, _ := bip32.NewKeyFromString(xpub) - magicNumberOfInheritance := 3 //2+1; 2: because of the way spv-wallet stores xpubs in db; 1: to make a PKI - var err error - for i := 0; i < magicNumberOfInheritance; i++ { - xPub, err = xPub.Child(0) - if err != nil { - panic(err) - } - } - - pubKey, err := xPub.ECPubKey() - if err != nil { - panic(err) - } - - return hex.EncodeToString(pubKey.SerialiseCompressed()) -} +// import ( +// "encoding/hex" +// "testing" +// +// +// +// +// +// +// +// +// +// +// + +// "github.com/bitcoin-sv/spv-wallet/models" +// "github.com/libsv/go-bk/bip32" +// "github.com/stretchr/testify/require" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" +// ) + +// 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 +// clientMaker := func(opts ...ClientOps) (*WalletClient, error) { +// allOptions := append(opts, WithHTTP("localhost:3001")) +// return New(allOptions...) +// } +// alice := makeMockUser("alice", clientMaker) +// bob := makeMockUser("bob", clientMaker) + +// pass, err := alice.client.GenerateTotpForContact(bob.contact, 3600, 2) +// require.NoError(t, err) + +// // when +// result, err := bob.client.ValidateTotpForContact(alice.contact, pass, bob.paymail, 3600, 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, "", fixtures.PaymailAddress, 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, "", fixtures.PaymailAddress, 30, 2) + +// // then +// require.ErrorContains(t, err, "contact's PubKey is invalid:") + +// }) +// } + +// type mockUser struct { +// contact *models.Contact +// client *WalletClient +// paymail string +// } + +// func makeMockUser(name string, clientMaker func(opts ...ClientOps) (*WalletClient, error)) mockUser { +// keys, _ := xpriv.Generate() +// paymail := name + "@example.com" +// client, _ := clientMaker(WithXPriv(keys.XPriv())) +// pki := makeMockPKI(keys.XPub().String()) +// contact := models.Contact{PubKey: pki, Paymail: paymail} +// return mockUser{ +// contact: &contact, +// client: client, +// paymail: paymail, +// } +// } + +// func makeMockPKI(xpub string) string { +// xPub, _ := bip32.NewKeyFromString(xpub) +// magicNumberOfInheritance := 3 //2+1; 2: because of the way spv-wallet stores xpubs in db; 1: to make a PKI +// var err error +// for i := 0; i < magicNumberOfInheritance; i++ { +// xPub, err = xPub.Child(0) +// if err != nil { +// panic(err) +// } +// } + +// pubKey, err := xPub.ECPubKey() +// if err != nil { +// panic(err) +// } + +// return hex.EncodeToString(pubKey.SerialiseCompressed()) +// } diff --git a/walletclient_test.go b/walletclient_test.go index fc6e9b9..f500f2e 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -1,114 +1,39 @@ package walletclient import ( - "io" "net/http" "net/http/httptest" "testing" - "github.com/bitcoinschema/go-bitcoin/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + assert "github.com/stretchr/testify/require" ) -// localRoundTripper is an http.RoundTripper that executes HTTP transactions -// by using handler directly, instead of going over an HTTP connection. -type localRoundTripper struct { - handler http.Handler -} - -func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - w := httptest.NewRecorder() - l.handler.ServeHTTP(w, req) - return w.Result(), nil +func TestNewWalletClientWithXPrivate(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"result": "success"}`)) + })) + defer server.Close() + + // Test creating a client with a valid xPriv + xPriv := "xprv9s21ZrQH143K3CbJXirfrtpLvhT3Vgusdo8coBritQ3rcS7Jy7sxWhatuxG5h2y1Cqj8FKmPp69536gmjYRpfga2MJdsGyBsnB12E19CESK" + client, err := NewWalletClientWithXPrivate(xPriv, server.URL, true) + assert.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, &xPriv, client.xPrivString) + assert.NotNil(t, client.httpClient) + assert.True(t, *client.signRequest) + + // Ensure HTTP calls can be made + resp, err := client.httpClient.Get(server.URL) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) } -func mustWrite(w io.Writer, s string) { - _, err := io.WriteString(w, s) - if err != nil { - panic(err) - } -} - -type testTransportHandler struct { - ClientURL string - Client func(serverURL string, httpClient *http.Client) ClientOps - Path string - Queries []*testTransportHandlerRequest - Result string - Type string -} - -type testTransportHandlerRequest struct { - Path string - Result func(w http.ResponseWriter, req *http.Request) -} - -// TestNewWalletClient will test the TestNewWalletClient method -func TestNewWalletClient(t *testing.T) { - - t.Run("empty xpriv", func(t *testing.T) { - client, err := NewWalletClientWithXPrivate("", fixtures.ServerURL, false) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("invalid xpriv", func(t *testing.T) { - client, err := NewWalletClientWithXPrivate("invalid-xpriv", fixtures.ServerURL, false) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("valid client", func(t *testing.T) { - client, err := NewWalletClientWithXPrivate(fixtures.XPrivString, fixtures.ServerURL, false) - - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) - - t.Run("valid xPub client", func(t *testing.T) { - client, err := NewWalletClientWithXPublic(fixtures.XPubString, fixtures.ServerURL, false) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) - - t.Run("invalid xPub client", func(t *testing.T) { - client, err := NewWalletClientWithXPublic("invalid-xpub", fixtures.ServerURL, false) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("valid access keys", func(t *testing.T) { - client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, fixtures.ServerURL, false) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) - - t.Run("invalid access keys", func(t *testing.T) { - client, err := NewWalletClientWithAccessKey("invalid-access-key", fixtures.ServerURL, false) - assert.Error(t, err) - assert.Nil(t, client) - }) - - t.Run("valid access key WIF", func(t *testing.T) { - wifKey, _ := bitcoin.PrivateKeyToWif(fixtures.AccessKeyString) - client, err := NewWalletClientWithAccessKey(wifKey.String(), fixtures.ServerURL, false) - require.NoError(t, err) - assert.IsType(t, WalletClient{}, *client) - }) -} - -// TestSetSignRequest will test the sign request setter -func TestSetSignRequest(t *testing.T) { - t.Run("true", func(t *testing.T) { - client, _ := NewWalletClientWithXPrivate(fixtures.XPrivString, fixtures.ServerURL, true) - assert.True(t, client.IsSignRequest()) - }) - - t.Run("false", func(t *testing.T) { - client, _ := NewWalletClientWithXPrivate(fixtures.XPrivString, fixtures.ServerURL, false) - assert.False(t, client.IsSignRequest()) - }) +func TestKeyInitialization(t *testing.T) { + xPriv := "invalid_key" + client, err := NewWalletClientWithXPrivate(xPriv, "http://example.com", true) + assert.Error(t, err) // Expect error due to invalid key + assert.Nil(t, client) } diff --git a/xpubs_test.go b/xpubs_test.go index 233d1c8..662bf85 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -1,45 +1,56 @@ package walletclient -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -) - -// TestXpub will test the Xpub methods -func TestXpub(t *testing.T) { - transportHandler := testTransportHandler{ - Type: fixtures.RequestType, - Path: "/xpub", - Result: fixtures.MarshallForTestHandler(fixtures.Xpub), - ClientURL: fixtures.ServerURL, - Client: WithHTTPClient, - } - - t.Run("GetXPub", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - xpub, err := client.GetXPub(context.Background()) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.Xpub, xpub) - }) - - t.Run("UpdateXPubMetadata", func(t *testing.T) { - // given - client := getTestWalletClient(transportHandler, true) - - // when - xpub, err := client.UpdateXPubMetadata(context.Background(), fixtures.TestMetadata) - - // then - assert.NoError(t, err) - assert.Equal(t, fixtures.Xpub, xpub) - }) -} +// import ( +// "context" +// "testing" +// +// +// +// +// +// +// +// +// +// +// + +// "github.com/stretchr/testify/assert" + +// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +// ) + +// // TestXpub will test the Xpub methods +// func TestXpub(t *testing.T) { +// transportHandler := testTransportHandler{ +// Type: fixtures.RequestType, +// Path: "/xpub", +// Result: fixtures.MarshallForTestHandler(fixtures.Xpub), +// ClientURL: fixtures.ServerURL, +// Client: WithHTTPClient, +// } + +// t.Run("GetXPub", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, true) + +// // when +// xpub, err := client.GetXPub(context.Background()) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.Xpub, xpub) +// }) + +// t.Run("UpdateXPubMetadata", func(t *testing.T) { +// // given +// client := getTestWalletClient(transportHandler, true) + +// // when +// xpub, err := client.UpdateXPubMetadata(context.Background(), fixtures.TestMetadata) + +// // then +// assert.NoError(t, err) +// assert.Equal(t, fixtures.Xpub, xpub) +// }) +// } From c2b70a9cddad1f37911d15351b134b76367a57f9 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Sun, 12 May 2024 17:51:05 +0200 Subject: [PATCH 06/27] latest working --- totp_test.go | 265 ++++++++++++++++++++++----------------------------- 1 file changed, 112 insertions(+), 153 deletions(-) diff --git a/totp_test.go b/totp_test.go index 07a2140..7261d5a 100644 --- a/totp_test.go +++ b/totp_test.go @@ -1,155 +1,114 @@ package walletclient -// import ( -// "encoding/hex" -// "testing" -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" -// "github.com/libsv/go-bk/bip32" -// "github.com/stretchr/testify/require" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" -// ) - -// 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 -// clientMaker := func(opts ...ClientOps) (*WalletClient, error) { -// allOptions := append(opts, WithHTTP("localhost:3001")) -// return New(allOptions...) -// } -// alice := makeMockUser("alice", clientMaker) -// bob := makeMockUser("bob", clientMaker) - -// pass, err := alice.client.GenerateTotpForContact(bob.contact, 3600, 2) -// require.NoError(t, err) - -// // when -// result, err := bob.client.ValidateTotpForContact(alice.contact, pass, bob.paymail, 3600, 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, "", fixtures.PaymailAddress, 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, "", fixtures.PaymailAddress, 30, 2) - -// // then -// require.ErrorContains(t, err, "contact's PubKey is invalid:") - -// }) -// } - -// type mockUser struct { -// contact *models.Contact -// client *WalletClient -// paymail string -// } - -// func makeMockUser(name string, clientMaker func(opts ...ClientOps) (*WalletClient, error)) mockUser { -// keys, _ := xpriv.Generate() -// paymail := name + "@example.com" -// client, _ := clientMaker(WithXPriv(keys.XPriv())) -// pki := makeMockPKI(keys.XPub().String()) -// contact := models.Contact{PubKey: pki, Paymail: paymail} -// return mockUser{ -// contact: &contact, -// client: client, -// paymail: paymail, -// } -// } - -// func makeMockPKI(xpub string) string { -// xPub, _ := bip32.NewKeyFromString(xpub) -// magicNumberOfInheritance := 3 //2+1; 2: because of the way spv-wallet stores xpubs in db; 1: to make a PKI -// var err error -// for i := 0; i < magicNumberOfInheritance; i++ { -// xPub, err = xPub.Child(0) -// if err != nil { -// panic(err) -// } -// } - -// pubKey, err := xPub.ECPubKey() -// if err != nil { -// panic(err) -// } - -// return hex.EncodeToString(pubKey.SerialiseCompressed()) -// } +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" +) + +func TestGenerateTotpForContact(t *testing.T) { + t.Run("success", func(t *testing.T) { + // given + sut, err := NewWalletClientWithXPrivate(fixtures.XPrivString, "localhost:3001", false) + 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 := NewWalletClientWithXPublic(fixtures.XPubString, "localhost:3001", false) + 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 := NewWalletClientWithXPrivate(fixtures.XPrivString, "localhost:3001", false) + 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) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // This handler could be adjusted depending on the expected API endpoints + w.WriteHeader(http.StatusOK) + w.Write([]byte("123456")) // Simulate a TOTP response for any requests + })) + defer server.Close() + + t.Run("success", func(t *testing.T) { + aliceKeys, err := xpriv.Generate() + require.NoError(t, err) + bobKeys, err := xpriv.Generate() + require.NoError(t, err) + + // Set up the WalletClient for Alice and Bob + clientAlice, err := NewWalletClientWithXPrivate(aliceKeys.XPriv(), server.URL, true) + require.NoError(t, err) + clientBob, err := NewWalletClientWithXPrivate(bobKeys.XPriv(), server.URL, true) + require.NoError(t, err) + + require.NoError(t, err) + bobContact := &models.Contact{ + PubKey: bobKeys.XPub().String(), + Paymail: "bob@example.com", + } + + // Generate and validate TOTP + passcode, err := clientAlice.GenerateTotpForContact(bobContact, 3600, 6) + require.NoError(t, err) + result, err := clientBob.ValidateTotpForContact(bobContact, passcode, "alice@example.com", 3600, 6) + require.NoError(t, err) + assert.True(t, result) + }) + + t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { + client, err := NewWalletClientWithXPublic("invalid_xpub", server.URL, true) + assert.Error(t, err) + assert.Nil(t, client) + }) + + t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { + xPrivString := "xprv9s21ZrQH143K3CbJXirfrtpLvhT3Vgusdo8coBritQ3rcS7Jy7sxWhatuxG5h2y1Cqj8FKmPp69536gmjYRpfga2MJdsGyBsnB12E19CESK" + sut, err := NewWalletClientWithXPrivate(xPrivString, server.URL, true) + require.NoError(t, err) + + invalidContact := &models.Contact{ + PubKey: "invalid_pub_key_format", + Paymail: "invalid@example.com", + } + + _, err = sut.ValidateTotpForContact(invalidContact, "123456", "someone@example.com", 3600, 6) + assert.Error(t, err) + assert.Contains(t, err.Error(), "contact's PubKey is invalid") + }) +} From 51a1637ec5f9aafd5b53b39eb121b528c0d50bf0 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Sun, 12 May 2024 19:12:35 +0200 Subject: [PATCH 07/27] latest working --- examples/tt/admin.go | 2 +- http.go | 369 +++++++++++++++++++++++++++++++++++++++++-- http_admin.go | 369 ------------------------------------------- totp_test.go | 30 +++- xpubs_test.go | 117 +++++++------- 5 files changed, 450 insertions(+), 437 deletions(-) delete mode 100644 http_admin.go diff --git a/examples/tt/admin.go b/examples/tt/admin.go index 86be836..91de307 100644 --- a/examples/tt/admin.go +++ b/examples/tt/admin.go @@ -87,7 +87,7 @@ func handleUsers(ctx context.Context, clientAdmin *walletclient.WalletClient) { fmt.Println() log.Println(" **** Admin Accept Contact ****") - if _, err := aliceClient.UpsertContact(ctx, bobPaymail, bobName, nil, ""); err != nil { + if _, err := aliceClient.UpsertContact(ctx, bobPaymail, bobName, nil); err != nil { panic(err) } diff --git a/http.go b/http.go index 00147bb..9d6a2e5 100644 --- a/http.go +++ b/http.go @@ -19,16 +19,6 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/utils" ) -type transportHTTP struct { - accessKey *bec.PrivateKey - adminXPriv *bip32.ExtendedKey - httpClient *http.Client - server string - signRequest bool - xPriv *bip32.ExtendedKey - xPub *bip32.ExtendedKey -} - // SetSignRequest turn the signing of the http request on or off func (wc *WalletClient) SetSignRequest(signRequest bool) { wc.signRequest = &signRequest @@ -716,3 +706,362 @@ func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fu return &result, nil } + +// AdminNewXpub will register an xPub +func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError { + // Adding a xpub needs to be signed by an admin key + if wc.adminXPriv == nil { + return WrapError(ErrAdminKey) + } + + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldMetadata: processMetadata(metadata), + FieldXpubKey: rawXPub, + }) + if err != nil { + return WrapError(err) + } + + var xPubData models.Xpub + + return wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/xpub", jsonStr, wc.adminXPriv, true, &xPubData, + ) +} + +// AdminGetStatus get whether admin key is valid +func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError) { + var status bool + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/status", nil, wc.xPriv, true, &status, + ); err != nil { + return false, err + } + + return status, nil +} + +// AdminGetStats get admin stats +func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) { + var stats *models.AdminStats + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/stats", nil, wc.xPriv, true, &stats, + ); err != nil { + return nil, err + } + + return stats, nil +} + +// AdminGetAccessKeys get all access keys filtered by conditions +func (wc *WalletClient) AdminGetAccessKeys(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.AccessKey, ResponseError) { + var models []*models.AccessKey + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/access-keys/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetAccessKeysCount get a count of all the access keys filtered by conditions +func (wc *WalletClient) AdminGetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/access-keys/count") +} + +// AdminGetBlockHeaders get all block headers filtered by conditions +func (wc *WalletClient) AdminGetBlockHeaders(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.BlockHeader, ResponseError) { + var models []*models.BlockHeader + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions +func (wc *WalletClient) AdminGetBlockHeadersCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/block-headers/count") +} + +// AdminGetDestinations get all block destinations filtered by conditions +func (wc *WalletClient) AdminGetDestinations(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Destination, ResponseError) { + var models []*models.Destination + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/destinations/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetDestinationsCount get a count of all the destinations filtered by conditions +func (wc *WalletClient) AdminGetDestinationsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/destinations/count") +} + +// AdminGetPaymail get a paymail by address +func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + }) + if err != nil { + return nil, WrapError(err) + } + + var model *models.PaymailAddress + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.xPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminGetPaymails get all block paymails filtered by conditions +func (wc *WalletClient) AdminGetPaymails(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.PaymailAddress, ResponseError) { + var models []*models.PaymailAddress + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/paymails/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetPaymailsCount get a count of all the paymails filtered by conditions +func (wc *WalletClient) AdminGetPaymailsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/paymails/count") +} + +// AdminCreatePaymail create a new paymail for a xpub +func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldXpubKey: rawXPub, + FieldAddress: address, + FieldPublicName: publicName, + FieldAvatar: avatar, + }) + if err != nil { + return nil, WrapError(err) + } + + var model *models.PaymailAddress + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.xPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminDeletePaymail delete a paymail address from the database +func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) ResponseError { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldAddress: address, + }) + if err != nil { + return WrapError(err) + } + + if err := wc.doHTTPRequest( + ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.xPriv, true, nil, + ); err != nil { + return err + } + + return nil +} + +// AdminGetTransactions get all block transactions filtered by conditions +func (wc *WalletClient) AdminGetTransactions(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Transaction, ResponseError) { + var models []*models.Transaction + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/transactions/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetTransactionsCount get a count of all the transactions filtered by conditions +func (wc *WalletClient) AdminGetTransactionsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/transactions/count") +} + +// AdminGetUtxos get all block utxos filtered by conditions +func (wc *WalletClient) AdminGetUtxos(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Utxo, ResponseError) { + var models []*models.Utxo + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/utxos/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetUtxosCount get a count of all the utxos filtered by conditions +func (wc *WalletClient) AdminGetUtxosCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/utxos/count") +} + +// AdminGetXPubs get all block xpubs filtered by conditions +func (wc *WalletClient) AdminGetXPubs(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, +) ([]*models.Xpub, ResponseError) { + var models []*models.Xpub + if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/xpubs/search", &models); err != nil { + return nil, err + } + + return models, nil +} + +// AdminGetXPubsCount get a count of all the xpubs filtered by conditions +func (wc *WalletClient) AdminGetXPubsCount(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, +) (int64, ResponseError) { + return wc.adminCount(ctx, conditions, metadata, "/admin/xpubs/count") +} + +func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[string]interface{}, + metadata *models.Metadata, queryParams *QueryParams, path string, models interface{}, +) ResponseError { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + FieldQueryParams: queryParams, + }) + if err != nil { + return WrapError(err) + } + + if err := wc.doHTTPRequest( + ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &models, + ); err != nil { + return err + } + + return nil +} + +func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, path string) (int64, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldConditions: conditions, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return 0, WrapError(err) + } + + var count int64 + if err := wc.doHTTPRequest( + ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &count, + ); err != nil { + return 0, err + } + + return count, nil +} + +// AdminRecordTransaction will record a transaction as an admin +func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + FieldHex: hex, + }) + if err != nil { + return nil, WrapError(err) + } + + var transaction models.Transaction + if err := wc.doHTTPRequest( + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.xPriv, *wc.signRequest, &transaction, + ); err != nil { + return nil, err + } + + return &transaction, nil +} + +// AdminGetSharedConfig gets the shared config +func (wc *WalletClient) AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) { + var model *models.SharedConfig + if err := wc.doHTTPRequest( + ctx, http.MethodGet, "/admin/shared-config", nil, wc.xPriv, true, &model, + ); err != nil { + return nil, err + } + + return model, nil +} + +// AdminGetContacts executes an HTTP POST request to search for contacts based on specified conditions, metadata, and query parameters. +func (wc *WalletClient) AdminGetContacts(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 contacts []*models.Contact + err = wc.doHTTPRequest(ctx, http.MethodPost, "/admin/contact/search", jsonStr, wc.adminXPriv, true, &contacts) + return contacts, WrapError(err) +} + +// AdminUpdateContact executes an HTTP PATCH request to update a specific contact's full name using their ID. +func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { + jsonStr, err := json.Marshal(map[string]interface{}{ + "fullName": fullName, + FieldMetadata: processMetadata(metadata), + }) + if err != nil { + return nil, WrapError(err) + } + var contact models.Contact + err = wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/%s", id), jsonStr, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminDeleteContact executes an HTTP DELETE request to remove a contact using their ID. +func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) ResponseError { + err := wc.doHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/admin/contact/%s", id), nil, wc.adminXPriv, true, nil) + return WrapError(err) +} + +// AdminAcceptContact executes an HTTP PATCH request to mark a contact as accepted using their ID. +func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, ResponseError) { + var contact models.Contact + err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/accepted/%s", id), nil, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} + +// AdminRejectContact executes an HTTP PATCH request to mark a contact as rejected using their ID. +func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, ResponseError) { + var contact models.Contact + err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, wc.adminXPriv, true, &contact) + return &contact, WrapError(err) +} diff --git a/http_admin.go b/http_admin.go deleted file mode 100644 index c3b918f..0000000 --- a/http_admin.go +++ /dev/null @@ -1,369 +0,0 @@ -package walletclient - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/bitcoin-sv/spv-wallet/models" -) - -// AdminNewXpub will register an xPub -func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metadata *models.Metadata) ResponseError { - // Adding a xpub needs to be signed by an admin key - if wc.adminXPriv == nil { - return WrapError(ErrAdminKey) - } - - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldMetadata: processMetadata(metadata), - FieldXpubKey: rawXPub, - }) - if err != nil { - return WrapError(err) - } - - var xPubData models.Xpub - - return wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/xpub", jsonStr, wc.adminXPriv, true, &xPubData, - ) -} - -// AdminGetStatus get whether admin key is valid -func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError) { - var status bool - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/status", nil, wc.xPriv, true, &status, - ); err != nil { - return false, err - } - - return status, nil -} - -// AdminGetStats get admin stats -func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) { - var stats *models.AdminStats - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/stats", nil, wc.xPriv, true, &stats, - ); err != nil { - return nil, err - } - - return stats, nil -} - -// AdminGetAccessKeys get all access keys filtered by conditions -func (wc *WalletClient) AdminGetAccessKeys(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.AccessKey, ResponseError) { - var models []*models.AccessKey - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/access-keys/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetAccessKeysCount get a count of all the access keys filtered by conditions -func (wc *WalletClient) AdminGetAccessKeysCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/access-keys/count") -} - -// AdminGetBlockHeaders get all block headers filtered by conditions -func (wc *WalletClient) AdminGetBlockHeaders(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.BlockHeader, ResponseError) { - var models []*models.BlockHeader - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/block-headers/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetBlockHeadersCount get a count of all the block headers filtered by conditions -func (wc *WalletClient) AdminGetBlockHeadersCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/block-headers/count") -} - -// AdminGetDestinations get all block destinations filtered by conditions -func (wc *WalletClient) AdminGetDestinations(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Destination, ResponseError) { - var models []*models.Destination - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/destinations/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetDestinationsCount get a count of all the destinations filtered by conditions -func (wc *WalletClient) AdminGetDestinationsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/destinations/count") -} - -// AdminGetPaymail get a paymail by address -func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*models.PaymailAddress, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - }) - if err != nil { - return nil, WrapError(err) - } - - var model *models.PaymailAddress - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.xPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminGetPaymails get all block paymails filtered by conditions -func (wc *WalletClient) AdminGetPaymails(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.PaymailAddress, ResponseError) { - var models []*models.PaymailAddress - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/paymails/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetPaymailsCount get a count of all the paymails filtered by conditions -func (wc *WalletClient) AdminGetPaymailsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/paymails/count") -} - -// AdminCreatePaymail create a new paymail for a xpub -func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, address string, publicName string, avatar string) (*models.PaymailAddress, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldXpubKey: rawXPub, - FieldAddress: address, - FieldPublicName: publicName, - FieldAvatar: avatar, - }) - if err != nil { - return nil, WrapError(err) - } - - var model *models.PaymailAddress - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.xPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminDeletePaymail delete a paymail address from the database -func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) ResponseError { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldAddress: address, - }) - if err != nil { - return WrapError(err) - } - - if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.xPriv, true, nil, - ); err != nil { - return err - } - - return nil -} - -// AdminGetTransactions get all block transactions filtered by conditions -func (wc *WalletClient) AdminGetTransactions(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Transaction, ResponseError) { - var models []*models.Transaction - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/transactions/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetTransactionsCount get a count of all the transactions filtered by conditions -func (wc *WalletClient) AdminGetTransactionsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/transactions/count") -} - -// AdminGetUtxos get all block utxos filtered by conditions -func (wc *WalletClient) AdminGetUtxos(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Utxo, ResponseError) { - var models []*models.Utxo - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/utxos/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetUtxosCount get a count of all the utxos filtered by conditions -func (wc *WalletClient) AdminGetUtxosCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/utxos/count") -} - -// AdminGetXPubs get all block xpubs filtered by conditions -func (wc *WalletClient) AdminGetXPubs(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, -) ([]*models.Xpub, ResponseError) { - var models []*models.Xpub - if err := wc.adminGetModels(ctx, conditions, metadata, queryParams, "/admin/xpubs/search", &models); err != nil { - return nil, err - } - - return models, nil -} - -// AdminGetXPubsCount get a count of all the xpubs filtered by conditions -func (wc *WalletClient) AdminGetXPubsCount(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, -) (int64, ResponseError) { - return wc.adminCount(ctx, conditions, metadata, "/admin/xpubs/count") -} - -func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[string]interface{}, - metadata *models.Metadata, queryParams *QueryParams, path string, models interface{}, -) ResponseError { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - FieldQueryParams: queryParams, - }) - if err != nil { - return WrapError(err) - } - - if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &models, - ); err != nil { - return err - } - - return nil -} - -func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, path string) (int64, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldConditions: conditions, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return 0, WrapError(err) - } - - var count int64 - if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &count, - ); err != nil { - return 0, err - } - - return count, nil -} - -// AdminRecordTransaction will record a transaction as an admin -func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) (*models.Transaction, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - FieldHex: hex, - }) - if err != nil { - return nil, WrapError(err) - } - - var transaction models.Transaction - if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.xPriv, *wc.signRequest, &transaction, - ); err != nil { - return nil, err - } - - return &transaction, nil -} - -// AdminGetSharedConfig gets the shared config -func (wc *WalletClient) AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) { - var model *models.SharedConfig - if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/shared-config", nil, wc.xPriv, true, &model, - ); err != nil { - return nil, err - } - - return model, nil -} - -// AdminGetContacts executes an HTTP POST request to search for contacts based on specified conditions, metadata, and query parameters. -func (wc *WalletClient) AdminGetContacts(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 contacts []*models.Contact - err = wc.doHTTPRequest(ctx, http.MethodPost, "/admin/contact/search", jsonStr, wc.adminXPriv, true, &contacts) - return contacts, WrapError(err) -} - -// AdminUpdateContact executes an HTTP PATCH request to update a specific contact's full name using their ID. -func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, ResponseError) { - jsonStr, err := json.Marshal(map[string]interface{}{ - "fullName": fullName, - FieldMetadata: processMetadata(metadata), - }) - if err != nil { - return nil, WrapError(err) - } - var contact models.Contact - err = wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/%s", id), jsonStr, wc.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - -// AdminDeleteContact executes an HTTP DELETE request to remove a contact using their ID. -func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) ResponseError { - err := wc.doHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/admin/contact/%s", id), nil, wc.adminXPriv, true, nil) - return WrapError(err) -} - -// AdminAcceptContact executes an HTTP PATCH request to mark a contact as accepted using their ID. -func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, ResponseError) { - var contact models.Contact - err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/accepted/%s", id), nil, wc.adminXPriv, true, &contact) - return &contact, WrapError(err) -} - -// AdminRejectContact executes an HTTP PATCH request to mark a contact as rejected using their ID. -func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, ResponseError) { - var contact models.Contact - err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, wc.adminXPriv, true, &contact) - return &contact, WrapError(err) -} diff --git a/totp_test.go b/totp_test.go index 7261d5a..f8c1db5 100644 --- a/totp_test.go +++ b/totp_test.go @@ -1,11 +1,13 @@ package walletclient import ( + "encoding/hex" "net/http" "net/http/httptest" "testing" "github.com/bitcoin-sv/spv-wallet/models" + "github.com/libsv/go-bk/bip32" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -77,16 +79,20 @@ func TestValidateTotpForContact(t *testing.T) { clientBob, err := NewWalletClientWithXPrivate(bobKeys.XPriv(), server.URL, true) require.NoError(t, err) - require.NoError(t, err) + aliceContact := &models.Contact{ + PubKey: makeMockPKI(aliceKeys.XPub().String()), + Paymail: "bob@example.com", + } + bobContact := &models.Contact{ - PubKey: bobKeys.XPub().String(), + PubKey: makeMockPKI(bobKeys.XPub().String()), Paymail: "bob@example.com", } // Generate and validate TOTP passcode, err := clientAlice.GenerateTotpForContact(bobContact, 3600, 6) require.NoError(t, err) - result, err := clientBob.ValidateTotpForContact(bobContact, passcode, "alice@example.com", 3600, 6) + result, err := clientBob.ValidateTotpForContact(aliceContact, passcode, bobContact.Paymail, 3600, 6) require.NoError(t, err) assert.True(t, result) }) @@ -112,3 +118,21 @@ func TestValidateTotpForContact(t *testing.T) { assert.Contains(t, err.Error(), "contact's PubKey is invalid") }) } + +func makeMockPKI(xpub string) string { + xPub, _ := bip32.NewKeyFromString(xpub) + var err error + for i := 0; i < 3; i++ { //magicNumberOfInheritance is 3 -> 2+1; 2: because of the way spv-wallet stores xpubs in db; 1: to make a PKI + xPub, err = xPub.Child(0) + if err != nil { + panic(err) + } + } + + pubKey, err := xPub.ECPubKey() + if err != nil { + panic(err) + } + + return hex.EncodeToString(pubKey.SerialiseCompressed()) +} diff --git a/xpubs_test.go b/xpubs_test.go index 662bf85..52b3bb9 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -1,56 +1,65 @@ package walletclient -// import ( -// "context" -// "testing" -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/stretchr/testify/assert" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// ) - -// // TestXpub will test the Xpub methods -// func TestXpub(t *testing.T) { -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/xpub", -// Result: fixtures.MarshallForTestHandler(fixtures.Xpub), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } - -// t.Run("GetXPub", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, true) - -// // when -// xpub, err := client.GetXPub(context.Background()) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.Xpub, xpub) -// }) - -// t.Run("UpdateXPubMetadata", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, true) - -// // when -// xpub, err := client.UpdateXPubMetadata(context.Background(), fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.Xpub, xpub) -// }) -// } +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" +) + +type xpub struct { + CurrentBalance uint64 `json:"current_balance"` + Metadata *models.Metadata `json:"metadata"` +} + +func TestXpub(t *testing.T) { + var update bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var response xpub + // Check path and method to customize the response + switch { + case r.URL.Path == "/xpub": + metadata := &models.Metadata{"key": "value"} + if update { + metadata = &models.Metadata{"updated": "info"} + } + response = xpub{ + CurrentBalance: 1234, + Metadata: metadata, + } + } + respBytes, _ := json.Marshal(response) + w.Write(respBytes) + })) + defer server.Close() + + keys, err := xpriv.Generate() + require.NoError(t, err) + + client, err := NewWalletClientWithXPrivate(keys.XPriv(), server.URL, true) + require.NoError(t, err) + + t.Run("GetXPub", func(t *testing.T) { + xpub, err := client.GetXPub(context.Background()) + require.NoError(t, err) + require.NotNil(t, xpub) + require.Equal(t, uint64(1234), xpub.CurrentBalance) + require.Equal(t, "value", xpub.Metadata["key"]) + }) + + t.Run("UpdateXPubMetadata", func(t *testing.T) { + update = true + metadata := &models.Metadata{"updated": "info"} + xpub, err := client.UpdateXPubMetadata(context.Background(), metadata) + require.NoError(t, err) + require.NotNil(t, xpub) + require.Equal(t, "info", xpub.Metadata["updated"]) + }) +} From e0188ca3823ca13ae57d036c7a4129cf909ad7f9 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Mon, 13 May 2024 09:36:55 +0200 Subject: [PATCH 08/27] fixed my issue with merfing local debug branch --- access_keys.go | 21 --- access_keys_test.go | 137 ++++++-------- admin_contacts.go | 26 --- client.go | 1 - contacts_test.go | 263 ++++++++------------------ destinations.go | 51 ----- destinations_test.go | 239 +++++++++--------------- fixtures/fixtures.go | 8 + http.go | 39 +++- totp_test.go | 14 +- transactions_test.go | 434 ++++++++++--------------------------------- walletclient.go | 28 ++- walletclient_test.go | 119 ++++++++++-- xpubs.go | 54 ------ 14 files changed, 500 insertions(+), 934 deletions(-) delete mode 100644 access_keys.go delete mode 100644 admin_contacts.go delete mode 100644 client.go delete mode 100644 destinations.go delete mode 100644 xpubs.go diff --git a/access_keys.go b/access_keys.go deleted file mode 100644 index 30e1544..0000000 --- a/access_keys.go +++ /dev/null @@ -1,21 +0,0 @@ -package walletclient - -// // GetAccessKey gets the access key given by id -// func (b *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, transports.ResponseError) { -// return b.transport.GetAccessKey(ctx, id) -// } - -// // GetAccessKeys gets all the access keys filtered by the metadata -// func (b *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *models.Metadata) ([]*models.AccessKey, transports.ResponseError) { -// return b.transport.GetAccessKeys(ctx, metadataConditions) -// } - -// // CreateAccessKey creates new access key -// func (b *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Metadata) (*models.AccessKey, transports.ResponseError) { -// return b.transport.CreateAccessKey(ctx, metadata) -// } - -// // RevokeAccessKey revoked the access key given by id -// func (b *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, transports.ResponseError) { -// return b.transport.RevokeAccessKey(ctx, id) -// } diff --git a/access_keys_test.go b/access_keys_test.go index af70c08..e86552d 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -1,79 +1,62 @@ package walletclient -// import ( -// "context" -// "testing" -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" -// "github.com/stretchr/testify/assert" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// ) - -// // TestAccessKeys will test the AccessKey methods -// func TestAccessKeys(t *testing.T) { -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/access-key", -// Result: fixtures.MarshallForTestHandler(fixtures.AccessKey), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } - -// t.Run("GetAccessKey", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, true) - -// // when -// accessKey, err := client.GetAccessKey(context.Background(), fixtures.AccessKey.ID) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, accessKey, fixtures.AccessKey) -// }) - -// t.Run("GetAccessKeys", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/access-key/search", -// Result: fixtures.MarshallForTestHandler([]*models.AccessKey{fixtures.AccessKey}), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, true) - -// // when -// accessKeys, err := client.GetAccessKeys(context.Background(), fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, accessKeys, []*models.AccessKey{fixtures.AccessKey}) -// }) - -// t.Run("CreateAccessKey", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, true) - -// // when -// accessKey, err := client.CreateAccessKey(context.Background(), fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, accessKey, fixtures.AccessKey) -// }) - -// t.Run("RevokeAccessKey", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, true) - -// // when -// accessKey, err := client.RevokeAccessKey(context.Background(), fixtures.AccessKey.ID) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, accessKey, fixtures.AccessKey) -// }) -// } +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +) + +// TestAccessKeys will test the AccessKey methods +func TestAccessKeys(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/access-key": + if r.Method == http.MethodGet { + json.NewEncoder(w).Encode(fixtures.AccessKey) + } else if r.Method == http.MethodPost { + json.NewEncoder(w).Encode(fixtures.AccessKey) + } else if r.Method == http.MethodDelete { + json.NewEncoder(w).Encode(fixtures.AccessKey) + } + case "/access-key/search": + json.NewEncoder(w).Encode([]*models.AccessKey{fixtures.AccessKey}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, server.URL, true) + require.NoError(t, err) + + t.Run("GetAccessKey", func(t *testing.T) { + accessKey, err := client.GetAccessKey(context.Background(), fixtures.AccessKey.ID) + require.NoError(t, err) + require.Equal(t, fixtures.AccessKey, accessKey) + }) + + t.Run("GetAccessKeys", func(t *testing.T) { + accessKeys, err := client.GetAccessKeys(context.Background(), nil) + require.NoError(t, err) + require.Equal(t, []*models.AccessKey{fixtures.AccessKey}, accessKeys) + }) + + t.Run("CreateAccessKey", func(t *testing.T) { + accessKey, err := client.CreateAccessKey(context.Background(), nil) + require.NoError(t, err) + require.Equal(t, fixtures.AccessKey, accessKey) + }) + + t.Run("RevokeAccessKey", func(t *testing.T) { + accessKey, err := client.RevokeAccessKey(context.Background(), fixtures.AccessKey.ID) + require.NoError(t, err) + require.Equal(t, fixtures.AccessKey, accessKey) + }) +} diff --git a/admin_contacts.go b/admin_contacts.go deleted file mode 100644 index 9c01e02..0000000 --- a/admin_contacts.go +++ /dev/null @@ -1,26 +0,0 @@ -package walletclient - -// // AdminGetContacts retrieves a list of contacts based on the provided conditions, metadata, and query parameters. -// func (wc *WalletClient) AdminGetContacts(ctx context.Context, conditions map[string]interface{}, metadata *models.Metadata, queryParams *transports.QueryParams) ([]*models.Contact, transports.ResponseError) { -// return wc.transport.AdminGetContacts(ctx, conditions, metadata, queryParams) -// } - -// // AdminUpdateContact updates a contact's details such as their full name using the specified contact ID and metadata. -// func (wc *WalletClient) AdminUpdateContact(ctx context.Context, id, fullName string, metadata *models.Metadata) (*models.Contact, transports.ResponseError) { -// return wc.transport.AdminUpdateContact(ctx, id, fullName, metadata) -// } - -// // AdminDeleteContact removes a contact from the system using the specified contact ID. -// func (wc *WalletClient) AdminDeleteContact(ctx context.Context, id string) transports.ResponseError { -// return wc.transport.AdminDeleteContact(ctx, id) -// } - -// // AdminAcceptContact marks a contact as accepted using the specified contact ID. -// func (wc *WalletClient) AdminAcceptContact(ctx context.Context, id string) (*models.Contact, transports.ResponseError) { -// return wc.transport.AdminAcceptContact(ctx, id) -// } - -// // AdminRejectContact marks a contact as rejected using the specified contact ID. -// func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*models.Contact, transports.ResponseError) { -// return wc.transport.AdminRejectContact(ctx, id) -// } diff --git a/client.go b/client.go deleted file mode 100644 index a5b925f..0000000 --- a/client.go +++ /dev/null @@ -1 +0,0 @@ -package walletclient diff --git a/contacts_test.go b/contacts_test.go index a014052..cfbaf37 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -1,191 +1,76 @@ package walletclient -// import ( -// "context" -// "strings" -// "testing" -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/require" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// ) - -// // TestContactActionsRouting will test routing -// func TestContactActionsRouting(t *testing.T) { -// tcs := []struct { -// name string -// route string -// responsePayload string -// f func(c *WalletClient) error -// }{ -// { -// name: "RejectContact", -// route: "/contact/rejected/", -// responsePayload: "{}", -// f: func(c *WalletClient) error { return c.RejectContact(context.Background(), fixtures.PaymailAddress) }, -// }, -// { -// name: "AcceptContact", -// route: "/contact/accepted/", -// responsePayload: "{}", -// f: func(c *WalletClient) error { return c.AcceptContact(context.Background(), fixtures.PaymailAddress) }, -// }, -// { -// 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 -// }, -// }, -// } - -// for _, tc := range tcs { -// t.Run(tc.name, func(t *testing.T) { -// // given -// tmq := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: tc.route, -// Result: tc.responsePayload, -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } - -// client := getTestWalletClientWithOpts(tmq, WithXPriv(fixtures.XPrivString)) - -// // when -// err := tc.f(client) - -// // then -// assert.NoError(t, err) -// }) -// } - -// } - -// 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, -// } - -// clientMaker := func(opts ...ClientOps) (*WalletClient, error) { -// return getTestWalletClientWithOpts(tmq, opts...), nil -// } - -// alice := makeMockUser("alice", clientMaker) -// bob := makeMockUser("bob", clientMaker) - -// totp, err := alice.client.GenerateTotpForContact(bob.contact, 30, 2) -// require.NoError(t, err) - -// // when -// result := bob.client.ConfirmContact(context.Background(), alice.contact, totp, bob.paymail, 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, -// } - -// clientMaker := func(opts ...ClientOps) (*WalletClient, error) { -// return getTestWalletClientWithOpts(tmq, opts...), nil -// } - -// alice := makeMockUser("alice", clientMaker) -// bob := makeMockUser("bob", clientMaker) - -// totp, err := alice.client.GenerateTotpForContact(bob.contact, 30, 2) -// require.NoError(t, err) - -// //make sure the wrongTotp is not the same as the generated one -// wrongTotp := incrementDigits(totp) //the length should remain the same - -// // when -// result := bob.client.ConfirmContact(context.Background(), alice.contact, wrongTotp, bob.paymail, 30, 2) - -// // then -// require.NotNil(t, result) -// require.Equal(t, result.Error(), "totp is invalid") -// }) -// } - -// // incrementDigits takes a string of digits and increments each digit by 1. -// // Digits wrap around such that '9' becomes '0'. -// func incrementDigits(input string) string { -// var result strings.Builder - -// for _, c := range input { -// if c == '9' { -// result.WriteRune('0') -// } else { -// result.WriteRune(c + 1) -// } -// } - -// return result.String() -// } +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +) + +// TestContactActionsRouting will test routing +func TestContactActionsRouting(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Println("=== test", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + switch { + case strings.HasPrefix(r.URL.Path, "/contact/rejected/"): + if r.Method == http.MethodPatch { + json.NewEncoder(w).Encode(map[string]string{"result": "rejected"}) + } + case r.URL.Path == "/contact/accepted/": + if r.Method == http.MethodPost { + json.NewEncoder(w).Encode(map[string]string{"result": "accepted"}) + } + case r.URL.Path == "/contact/search": + if r.Method == http.MethodPost { + json.NewEncoder(w).Encode([]*models.Contact{fixtures.Contact}) + } + case strings.HasPrefix(r.URL.Path, "/contact/"): + if r.Method == http.MethodPost || r.Method == http.MethodPut { + json.NewEncoder(w).Encode(map[string]string{"result": "upserted"}) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, server.URL, true) + require.NoError(t, err) + + t.Run("RejectContact", func(t *testing.T) { + err := client.RejectContact(context.Background(), fixtures.PaymailAddress) + require.NoError(t, err) + }) + + t.Run("AcceptContact", func(t *testing.T) { + err := client.AcceptContact(context.Background(), fixtures.PaymailAddress) + require.NoError(t, err) + }) + + t.Run("GetContacts", func(t *testing.T) { + contacts, err := client.GetContacts(context.Background(), nil, nil, nil) + require.NoError(t, err) + require.NotNil(t, contacts) + }) + + t.Run("UpsertContact", func(t *testing.T) { + contact, err := client.UpsertContact(context.Background(), "test-id", "test@paymail.com", nil) + require.NoError(t, err) + require.NotNil(t, contact) + }) + + t.Run("UpsertContactForPaymail", func(t *testing.T) { + contact, err := client.UpsertContactForPaymail(context.Background(), "test-id", "test@paymail.com", nil, "test@paymail.com") + require.NoError(t, err) + require.NotNil(t, contact) + }) +} diff --git a/destinations.go b/destinations.go deleted file mode 100644 index 7fc22b0..0000000 --- a/destinations.go +++ /dev/null @@ -1,51 +0,0 @@ -package walletclient - -// // GetDestinationByID gets the destination by id -// func (b *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, transports.ResponseError) { -// return b.transport.GetDestinationByID(ctx, id) -// } - -// // GetDestinationByAddress gets the destination by address -// func (b *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, transports.ResponseError) { -// return b.transport.GetDestinationByAddress(ctx, address) -// } - -// // GetDestinationByLockingScript gets the destination by locking script -// func (b *WalletClient) GetDestinationByLockingScript(ctx context.Context, -// lockingScript string, -// ) (*models.Destination, transports.ResponseError) { -// return b.transport.GetDestinationByLockingScript(ctx, lockingScript) -// } - -// // GetDestinations gets all destinations that match the metadata filter -// func (b *WalletClient) GetDestinations(ctx context.Context, -// metadataConditions *models.Metadata, -// ) ([]*models.Destination, transports.ResponseError) { -// return b.transport.GetDestinations(ctx, metadataConditions) -// } - -// // NewDestination create a new destination and return it -// func (b *WalletClient) NewDestination(ctx context.Context, metadata *models.Metadata) (*models.Destination, transports.ResponseError) { -// return b.transport.NewDestination(ctx, metadata) -// } - -// // UpdateDestinationMetadataByID updates the destination metadata by id -// func (b *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id string, -// metadata *models.Metadata, -// ) (*models.Destination, transports.ResponseError) { -// return b.transport.UpdateDestinationMetadataByID(ctx, id, metadata) -// } - -// // UpdateDestinationMetadataByAddress updates the destination metadata by address -// func (b *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, address string, -// metadata *models.Metadata, -// ) (*models.Destination, transports.ResponseError) { -// return b.transport.UpdateDestinationMetadataByAddress(ctx, address, metadata) -// } - -// // UpdateDestinationMetadataByLockingScript updates the destination metadata by locking script -// func (b *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Context, lockingScript string, -// metadata *models.Metadata, -// ) (*models.Destination, transports.ResponseError) { -// return b.transport.UpdateDestinationMetadataByLockingScript(ctx, lockingScript, metadata) -// } diff --git a/destinations_test.go b/destinations_test.go index 684055f..b6cf7c6 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -1,149 +1,94 @@ package walletclient -// import ( -// "context" -// "testing" -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" -// "github.com/stretchr/testify/assert" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// ) - -// // TestDestinations will test the Destinations methods -// func TestDestinations(t *testing.T) { -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/destination", -// Result: fixtures.MarshallForTestHandler(fixtures.Destination), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } - -// t.Run("GetDestinationByID", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.GetDestinationByID(context.Background(), fixtures.Destination.ID) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) - -// t.Run("GetDestinationByAddress", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.GetDestinationByAddress(context.Background(), fixtures.Destination.Address) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) - -// t.Run("GetDestinationByLockingScript", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.GetDestinationByLockingScript(context.Background(), fixtures.Destination.LockingScript) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) - -// t.Run("GetDestinations", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/destination/search", -// Result: fixtures.MarshallForTestHandler([]*models.Destination{fixtures.Destination}), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) - -// // when -// destinations, err := client.GetDestinations(context.Background(), fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destinations, []*models.Destination{fixtures.Destination}) -// }) - -// t.Run("NewDestination", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.NewDestination(context.Background(), fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) - -// t.Run("UpdateDestinationMetadataByID", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.UpdateDestinationMetadataByID(context.Background(), fixtures.Destination.ID, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) - -// t.Run("UpdateDestinationMetadataByAddress", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.UpdateDestinationMetadataByAddress(context.Background(), fixtures.Destination.Address, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) - -// t.Run("UpdateDestinationMetadataByLockingScript", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// destination, err := client.UpdateDestinationMetadataByLockingScript(context.Background(), fixtures.Destination.LockingScript, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, destination, fixtures.Destination) -// }) -// } +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +) + +func TestDestinations(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sendJSONResponse := func(data interface{}) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + } + + switch { + case r.URL.Path == "/destination/address/"+fixtures.Destination.Address && r.Method == http.MethodGet: + sendJSONResponse(fixtures.Destination) + case r.URL.Path == "/destination/lockingScript/"+fixtures.Destination.LockingScript && r.Method == http.MethodGet: + sendJSONResponse(fixtures.Destination) + case r.URL.Path == "/destination/search" && r.Method == http.MethodPost: + sendJSONResponse([]*models.Destination{fixtures.Destination}) + case r.URL.Path == "/destination" && r.Method == http.MethodGet: + sendJSONResponse(fixtures.Destination) + case r.URL.Path == "/destination" && r.Method == http.MethodPatch: + sendJSONResponse(fixtures.Destination) + case r.URL.Path == "/destination" && r.Method == http.MethodPost: + sendJSONResponse(fixtures.Destination) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, server.URL, true) + require.NoError(t, err) + + t.Run("GetDestinationByID", func(t *testing.T) { + destination, err := client.GetDestinationByID(context.Background(), fixtures.Destination.ID) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) + + t.Run("GetDestinationByAddress", func(t *testing.T) { + destination, err := client.GetDestinationByAddress(context.Background(), fixtures.Destination.Address) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) + + t.Run("GetDestinationByLockingScript", func(t *testing.T) { + destination, err := client.GetDestinationByLockingScript(context.Background(), fixtures.Destination.LockingScript) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) + + t.Run("GetDestinations", func(t *testing.T) { + destinations, err := client.GetDestinations(context.Background(), fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, []*models.Destination{fixtures.Destination}, destinations) + }) + + t.Run("NewDestination", func(t *testing.T) { + destination, err := client.NewDestination(context.Background(), fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) + + t.Run("UpdateDestinationMetadataByID", func(t *testing.T) { + destination, err := client.UpdateDestinationMetadataByID(context.Background(), fixtures.Destination.ID, fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) + + t.Run("UpdateDestinationMetadataByAddress", func(t *testing.T) { + destination, err := client.UpdateDestinationMetadataByAddress(context.Background(), fixtures.Destination.Address, fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) + + t.Run("UpdateDestinationMetadataByLockingScript", func(t *testing.T) { + destination, err := client.UpdateDestinationMetadataByLockingScript(context.Background(), fixtures.Destination.LockingScript, fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Destination, destination) + }) +} diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index 937e830..f0fb28f 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -190,3 +190,11 @@ var DraftTx = &models.DraftTransaction{ Status: "draft", FinalTxID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", } + +var Contact = &models.Contact{ + ID: "68af358bde7d8641621c7dd3de1a276c9a62cfa9e2d0740494519f1ba61e2f4a", + FullName: "Test User", + Paymail: "test@spv-wallet.com", + PubKey: "xpub661MyMwAqRbcGpZVrSHU...", + Status: models.ContactStatus("unconfirmed"), +} diff --git a/http.go b/http.go index 9d6a2e5..3bfeedd 100644 --- a/http.go +++ b/http.go @@ -303,10 +303,18 @@ func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Con return &destination, nil } +// GetTransaction will get a transaction by ID +func (wc *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, ResponseError) { + var transaction models.Transaction + if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.xPriv, *wc.signRequest, &transaction); err != nil { + return nil, err + } + + return &transaction, nil +} + // GetTransactions will get transactions by conditions -func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, - metadataConditions *models.Metadata, queryParams *QueryParams, -) ([]*models.Transaction, ResponseError) { +func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[string]interface{}, metadataConditions *models.Metadata, queryParams *QueryParams) ([]*models.Transaction, ResponseError) { jsonStr, err := json.Marshal(map[string]interface{}{ FieldConditions: conditions, FieldMetadata: processMetadata(metadataConditions), @@ -1065,3 +1073,28 @@ func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*mod err := wc.doHTTPRequest(ctx, http.MethodPatch, fmt.Sprintf("/admin/contact/rejected/%s", id), nil, wc.adminXPriv, true, &contact) return &contact, WrapError(err) } + +// FinalizeTransaction will finalize the transaction +func (wc *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, ResponseError) { + res, err := GetSignedHex(draft, wc.xPriv) + if err != nil { + return "", WrapError(err) + } + + return res, nil +} + +// SendToRecipients send to recipients +func (wc *WalletClient) SendToRecipients(ctx context.Context, recipients []*Recipients, metadata *models.Metadata) (*models.Transaction, ResponseError) { + draft, err := wc.DraftToRecipients(ctx, recipients, metadata) + if err != nil { + return nil, err + } + + var hex string + if hex, err = wc.FinalizeTransaction(draft); err != nil { + return nil, err + } + + return wc.RecordTransaction(ctx, hex, draft.ID, metadata) +} diff --git a/totp_test.go b/totp_test.go index f8c1db5..fbec16d 100644 --- a/totp_test.go +++ b/totp_test.go @@ -8,7 +8,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/models" "github.com/libsv/go-bk/bip32" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" @@ -94,18 +93,17 @@ func TestValidateTotpForContact(t *testing.T) { require.NoError(t, err) result, err := clientBob.ValidateTotpForContact(aliceContact, passcode, bobContact.Paymail, 3600, 6) require.NoError(t, err) - assert.True(t, result) + require.True(t, result) }) t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { client, err := NewWalletClientWithXPublic("invalid_xpub", server.URL, true) - assert.Error(t, err) - assert.Nil(t, client) + require.Error(t, err) + require.Nil(t, client) }) t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { - xPrivString := "xprv9s21ZrQH143K3CbJXirfrtpLvhT3Vgusdo8coBritQ3rcS7Jy7sxWhatuxG5h2y1Cqj8FKmPp69536gmjYRpfga2MJdsGyBsnB12E19CESK" - sut, err := NewWalletClientWithXPrivate(xPrivString, server.URL, true) + sut, err := NewWalletClientWithXPrivate(fixtures.XPrivString, server.URL, true) require.NoError(t, err) invalidContact := &models.Contact{ @@ -114,8 +112,8 @@ func TestValidateTotpForContact(t *testing.T) { } _, err = sut.ValidateTotpForContact(invalidContact, "123456", "someone@example.com", 3600, 6) - assert.Error(t, err) - assert.Contains(t, err.Error(), "contact's PubKey is invalid") + require.Error(t, err) + require.Contains(t, err.Error(), "contact's PubKey is invalid") }) } diff --git a/transactions_test.go b/transactions_test.go index fd95740..0dc2819 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -1,335 +1,103 @@ package walletclient -// import ( -// "context" -// "net/http" -// "testing" -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" -// "github.com/libsv/go-bt/v2" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/require" - -// "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" -// "github.com/bitcoin-sv/spv-wallet-go-client/transports" -// ) - -// // TestTransactions will test the Transaction methods -// func TestTransactions(t *testing.T) { -// t.Run("GetTransaction", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction", -// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) - -// // when -// tx, err := client.GetTransaction(context.Background(), fixtures.Transaction.ID) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.Transaction, tx) -// }) - -// t.Run("GetTransactions", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction/search", -// Result: fixtures.MarshallForTestHandler([]*models.Transaction{fixtures.Transaction}), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) -// conditions := map[string]interface{}{ -// "fee": map[string]interface{}{ -// "$lt": 100, -// }, -// "total_value": map[string]interface{}{ -// "$lt": 740, -// }, -// } - -// // when -// txs, err := client.GetTransactions(context.Background(), conditions, fixtures.TestMetadata, nil) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, []*models.Transaction{fixtures.Transaction}, txs) -// }) - -// t.Run("GetTransactionsCount", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction/count", -// Result: "1", -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) -// conditions := map[string]interface{}{ -// "fee": map[string]interface{}{ -// "$lt": 100, -// }, -// "total_value": map[string]interface{}{ -// "$lt": 740, -// }, -// } - -// // when -// count, err := client.GetTransactionsCount(context.Background(), conditions, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, int64(1), count) -// }) - -// t.Run("RecordTransaction", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction/record", -// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) - -// // when -// tx, err := client.RecordTransaction(context.Background(), fixtures.Transaction.Hex, "", fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.Transaction, tx) -// }) - -// t.Run("UpdateTransactionMetadata", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction", -// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) - -// // when -// tx, err := client.UpdateTransactionMetadata(context.Background(), fixtures.Transaction.ID, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.Transaction, tx) -// }) - -// t.Run("SendToRecipients", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Queries: []*testTransportHandlerRequest{ -// { -// Path: "/transaction/record", -// Result: func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) -// }, -// }, -// { -// Path: "/transaction", -// Result: func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.DraftTx)) -// }, -// }, -// }, -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) -// recipients := []*transports.Recipients{{ -// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, -// Satoshis: 1000, -// To: fixtures.Destination.Address, -// }} - -// // when -// tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.Transaction, tx) -// }) - -// t.Run("SendToRecipients - nil draft", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Queries: []*testTransportHandlerRequest{ -// { -// Path: "/transaction/record", -// Result: func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) -// }, -// }, -// { -// Path: "/transaction", -// Result: func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, "nil") -// }, -// }, -// }, -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) -// recipients := []*transports.Recipients{{ -// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, -// Satoshis: 1000, -// To: fixtures.Destination.Address, -// }} - -// // when -// tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) - -// // then -// assert.Error(t, err) -// assert.Nil(t, tx) -// }) - -// t.Run("SendToRecipients - FinalizeTransaction error", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Queries: []*testTransportHandlerRequest{ -// { -// Path: "/transaction/record", -// Result: func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, fixtures.MarshallForTestHandler(fixtures.Transaction)) -// }, -// }, -// { -// Path: "/transaction", -// Result: func(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "application/json") -// mustWrite(w, fixtures.MarshallForTestHandler(models.DraftTransaction{})) -// }, -// }, -// }, -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) -// recipients := []*transports.Recipients{{ -// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, -// Satoshis: 1000, -// To: fixtures.Destination.Address, -// }} - -// // when -// tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) - -// // then -// assert.Error(t, err) -// assert.Nil(t, tx) -// }) - -// t.Run("FinalizeTransaction", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction/record", -// Result: fixtures.MarshallForTestHandler(fixtures.Transaction), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) - -// // when -// signedHex, err := client.FinalizeTransaction(fixtures.DraftTx) - -// txDraft, btErr := bt.NewTxFromString(signedHex) -// require.NoError(t, btErr) - -// // then -// assert.NoError(t, err) -// assert.Len(t, txDraft.Inputs, len(fixtures.DraftTx.Configuration.Inputs)) -// assert.Len(t, txDraft.Outputs, len(fixtures.DraftTx.Configuration.Outputs)) -// }) - -// t.Run("CountUtxos", func(t *testing.T) { -// // given -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/utxo/count", -// ClientURL: fixtures.ServerURL, -// Result: "0", -// Client: WithHTTPClient, -// } -// client := getTestWalletClient(transportHandler, false) - -// // when -// _, err := client.GetUtxosCount(context.Background(), map[string]interface{}{}, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// }) -// } - -// // TestDraftTransactions will test the DraftTransaction methods -// func TestDraftTransactions(t *testing.T) { -// transportHandler := testTransportHandler{ -// Type: fixtures.RequestType, -// Path: "/transaction", -// Result: fixtures.MarshallForTestHandler(fixtures.DraftTx), -// ClientURL: fixtures.ServerURL, -// Client: WithHTTPClient, -// } - -// t.Run("DraftToRecipients", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// recipients := []*transports.Recipients{{ -// OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, -// Satoshis: 1000, -// To: fixtures.Destination.Address, -// }} - -// // when -// draft, err := client.DraftToRecipients(context.Background(), recipients, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.DraftTx, draft) -// }) - -// t.Run("DraftTransaction", func(t *testing.T) { -// // given -// client := getTestWalletClient(transportHandler, false) - -// // when -// draft, err := client.DraftTransaction(context.Background(), &fixtures.DraftTx.Configuration, fixtures.TestMetadata) - -// // then -// assert.NoError(t, err) -// assert.Equal(t, fixtures.DraftTx, draft) -// }) -// } +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +) + +func TestTransactions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/transaction": + switch r.Method { + case http.MethodGet: + json.NewEncoder(w).Encode(fixtures.Transaction) + case http.MethodPost: + json.NewEncoder(w).Encode(fixtures.Transaction) + case http.MethodPatch: + var input map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "bad request"}) + return + } + response := fixtures.Transaction + response.Metadata = input["metadata"].(map[string]interface{}) + response.ID = input["id"].(string) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + } + case "/transaction/search": + json.NewEncoder(w).Encode([]*models.Transaction{fixtures.Transaction}) + case "/transaction/count": + json.NewEncoder(w).Encode(1) + case "/transaction/record": + if r.Method == http.MethodPost { + w.Header().Set("Content-Type", "application/json") + response := fixtures.Transaction + json.NewEncoder(w).Encode(response) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client, err := NewWalletClientWithXPrivate(fixtures.XPrivString, server.URL, true) + require.NoError(t, err) + + t.Run("GetTransaction", func(t *testing.T) { + tx, err := client.GetTransaction(context.Background(), fixtures.Transaction.ID) + require.NoError(t, err) + require.Equal(t, fixtures.Transaction, tx) + }) + + t.Run("GetTransactions", func(t *testing.T) { + conditions := map[string]interface{}{ + "fee": map[string]interface{}{"$lt": 100}, + "total_value": map[string]interface{}{"$lt": 740}, + } + txs, err := client.GetTransactions(context.Background(), conditions, fixtures.TestMetadata, nil) + require.NoError(t, err) + require.Equal(t, []*models.Transaction{fixtures.Transaction}, txs) + }) + + t.Run("GetTransactionsCount", func(t *testing.T) { + count, err := client.GetTransactionsCount(context.Background(), nil, fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, int64(1), count) + }) + + t.Run("RecordTransaction", func(t *testing.T) { + tx, err := client.RecordTransaction(context.Background(), fixtures.Transaction.Hex, "", fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Transaction, tx) + }) + + t.Run("UpdateTransactionMetadata", func(t *testing.T) { + tx, err := client.UpdateTransactionMetadata(context.Background(), fixtures.Transaction.ID, fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Transaction, tx) + }) + + t.Run("SendToRecipients", func(t *testing.T) { + recipients := []*Recipients{{ + OpReturn: fixtures.DraftTx.Configuration.Outputs[0].OpReturn, + Satoshis: 1000, + To: fixtures.Destination.Address, + }} + tx, err := client.SendToRecipients(context.Background(), recipients, fixtures.TestMetadata) + require.NoError(t, err) + require.Equal(t, fixtures.Transaction, tx) + }) +} diff --git a/walletclient.go b/walletclient.go index e18fa80..60babd5 100644 --- a/walletclient.go +++ b/walletclient.go @@ -13,18 +13,23 @@ import ( // WalletClient is the spv wallet Go client representation. type WalletClient struct { + signRequest *bool + server *string accessKeyString *string xPrivString *string xPubString *string + httpClient *http.Client accessKey *bec.PrivateKey adminXPriv *bip32.ExtendedKey - httpClient *http.Client - server *string - signRequest *bool xPriv *bip32.ExtendedKey xPub *bip32.ExtendedKey } +// NewWalletClientWithXPrivate creates a new WalletClient instance using a private key (xPriv). +// It configures the client with a specific server URL and a flag indicating whether requests should be signed. +// - `xPriv`: The extended private key used for cryptographic operations. +// - `serverURL`: The URL of the server the client will interact with. +// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWalletClientWithXPrivate(xPriv, serverURL string, sign bool) (*WalletClient, error) { return newWalletClient( &WithXPriv{XPrivString: &xPriv}, @@ -33,6 +38,11 @@ func NewWalletClientWithXPrivate(xPriv, serverURL string, sign bool) (*WalletCli ) } +// NewWalletClientWithXPublic creates a new WalletClient instance using a public key (xPub). +// This client is configured for operations that require a public key, such as verifying signatures or receiving transactions. +// - `xPub`: The extended public key used for cryptographic verification and other public operations. +// - `serverURL`: The URL of the server the client will interact with. +// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWalletClientWithXPublic(xPub, serverURL string, sign bool) (*WalletClient, error) { return newWalletClient( &WithXPub{XPubString: &xPub}, @@ -41,6 +51,11 @@ func NewWalletClientWithXPublic(xPub, serverURL string, sign bool) (*WalletClien ) } +// NewWalletClientWithAdminKey creates a new WalletClient using an administrative key for advanced operations. +// This configuration is typically used for administrative tasks such as managing sub-wallets or configuring system-wide settings. +// - `adminKey`: The extended private key used for administrative operations. +// - `serverURL`: The URL of the server the client will interact with. +// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWalletClientWithAdminKey(adminKey, serverURL string, sign bool) (*WalletClient, error) { return newWalletClient( &WithXPriv{XPrivString: &adminKey}, @@ -50,6 +65,11 @@ func NewWalletClientWithAdminKey(adminKey, serverURL string, sign bool) (*Wallet ) } +// NewWalletClientWithAccessKey creates a new WalletClient configured with an access key for API authentication. +// This method is useful for scenarios where the client needs to authenticate using a less sensitive key than an xPriv. +// - `accessKey`: The access key used for API authentication. +// - `serverURL`: The URL of the server the client will interact with. +// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWalletClientWithAccessKey(accessKey, serverURL string, sign bool) (*WalletClient, error) { return newWalletClient( &WithAccessKey{AccessKeyString: &accessKey}, @@ -58,7 +78,7 @@ func NewWalletClientWithAccessKey(accessKey, serverURL string, sign bool) (*Wall ) } -// New creates a new WalletClient using the provided configuration options. +// newWalletClient creates a new WalletClient using the provided configuration options. func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, error) { client := &WalletClient{} diff --git a/walletclient_test.go b/walletclient_test.go index f500f2e..d407245 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -5,10 +5,13 @@ import ( "net/http/httptest" "testing" - assert "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" ) -func TestNewWalletClientWithXPrivate(t *testing.T) { +func TestNewWalletClient(t *testing.T) { // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -17,23 +20,99 @@ func TestNewWalletClientWithXPrivate(t *testing.T) { defer server.Close() // Test creating a client with a valid xPriv - xPriv := "xprv9s21ZrQH143K3CbJXirfrtpLvhT3Vgusdo8coBritQ3rcS7Jy7sxWhatuxG5h2y1Cqj8FKmPp69536gmjYRpfga2MJdsGyBsnB12E19CESK" - client, err := NewWalletClientWithXPrivate(xPriv, server.URL, true) - assert.NoError(t, err) - assert.NotNil(t, client) - assert.Equal(t, &xPriv, client.xPrivString) - assert.NotNil(t, client.httpClient) - assert.True(t, *client.signRequest) - - // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(server.URL) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} + t.Run("NewWalletClientWithXPrivate success", func(t *testing.T) { + keys, err := xpriv.Generate() + require.NoError(t, err) + client, err := NewWalletClientWithXPrivate(keys.XPriv(), server.URL, true) + require.NoError(t, err) + require.NotNil(t, client) + require.Equal(t, keys.XPriv(), *client.xPrivString) + require.NotNil(t, client.httpClient) + require.True(t, *client.signRequest) + + // Ensure HTTP calls can be made + resp, err := client.httpClient.Get(server.URL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("NewWalletClientWithXPrivate fail", func(t *testing.T) { + xPriv := "invalid_key" + client, err := NewWalletClientWithXPrivate(xPriv, "http://example.com", true) + require.Error(t, err) // Expect error due to invalid key + require.Nil(t, client) + }) + + t.Run("NewWalletClientWithXPublic success", func(t *testing.T) { + keys, err := xpriv.Generate() + require.NoError(t, err) + client, err := NewWalletClientWithXPublic(keys.XPub().String(), server.URL, false) + require.NoError(t, err) + require.NotNil(t, client) + require.Equal(t, keys.XPub().String(), *client.xPubString) + require.NotNil(t, client.httpClient) + require.False(t, *client.signRequest) + + // Ensure HTTP calls can be made + resp, err := client.httpClient.Get(server.URL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("NewWalletClientWithXPublic fail", func(t *testing.T) { + xpub := "invalid_key" + client, err := NewWalletClientWithXPublic(xpub, server.URL, false) + require.Error(t, err) // Expect error due to invalid key + require.Nil(t, client) + }) + + t.Run("NewWalletClientWithAdminKey success", func(t *testing.T) { + client, err := NewWalletClientWithAdminKey(fixtures.XPrivString, server.URL, true) + require.NoError(t, err) + require.NotNil(t, client) + require.Equal(t, fixtures.XPrivString, *client.xPrivString) + require.Equal(t, fixtures.XPrivString, client.adminXPriv.String()) + require.Equal(t, server.URL, *client.server) + require.NotNil(t, client.httpClient) + require.True(t, *client.signRequest) + + // Ensure HTTP calls can be made + resp, err := client.httpClient.Get(server.URL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("NewWalletClientWithAdminKey fail", func(t *testing.T) { + adminKey := "invalid_key" + client, err := NewWalletClientWithAdminKey(adminKey, server.URL, true) + require.Error(t, err) + require.Nil(t, client) + }) + + t.Run("NewWalletClientWithAccessKey success", func(t *testing.T) { + // Attempt to create a new WalletClient with an access key + accessKey := fixtures.AccessKeyString + client, err := NewWalletClientWithAccessKey(accessKey, server.URL, true) + + require.NoError(t, err) + require.NotNil(t, client) + + require.Equal(t, &accessKey, client.accessKeyString) + require.Equal(t, &server.URL, client.server) + require.True(t, *client.signRequest) + require.NotNil(t, client.httpClient) + + // Ensure HTTP calls can be made + resp, err := client.httpClient.Get(server.URL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("NewWalletClientWithAccessKey fail", func(t *testing.T) { + accessKey := "invalid_key" + client, err := NewWalletClientWithAccessKey(accessKey, server.URL, true) -func TestKeyInitialization(t *testing.T) { - xPriv := "invalid_key" - client, err := NewWalletClientWithXPrivate(xPriv, "http://example.com", true) - assert.Error(t, err) // Expect error due to invalid key - assert.Nil(t, client) + require.Error(t, err) + require.Nil(t, client) + }) } diff --git a/xpubs.go b/xpubs.go deleted file mode 100644 index 64e30a3..0000000 --- a/xpubs.go +++ /dev/null @@ -1,54 +0,0 @@ -package walletclient - -// import ( -// "context" -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// "github.com/bitcoin-sv/spv-wallet/models" - -// "github.com/bitcoin-sv/spv-wallet-go-client/transports" -// ) - -// // GetXPub gets the current xpub -// func (b *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, transports.ResponseError) { -// return b.transport.GetXPub(ctx) -// } - -// // UpdateXPubMetadata update the metadata of the logged in xpub -// func (b *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models.Metadata) (*models.Xpub, transports.ResponseError) { -// return b.transport.UpdateXPubMetadata(ctx, metadata) -// } From cc5db48e8025f780c5457778104fe072b9f47f11 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Tue, 14 May 2024 08:44:12 +0200 Subject: [PATCH 09/27] adding unit to admin --- admin_contacts_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 admin_contacts_test.go diff --git a/admin_contacts_test.go b/admin_contacts_test.go new file mode 100644 index 0000000..a202629 --- /dev/null +++ b/admin_contacts_test.go @@ -0,0 +1,76 @@ +package walletclient + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" +) + +// TestAdminContactActions testing Admin contacts methods +func TestAdminContactActions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.URL.Path == "/admin/contact/search" && r.Method == http.MethodPost: + c := fixtures.Contact + c.ID = "1" + contacts := []*models.Contact{c} + json.NewEncoder(w).Encode(contacts) + case r.URL.Path == "/admin/contact/1" && r.Method == http.MethodPatch: + contact := fixtures.Contact + json.NewEncoder(w).Encode(contact) + case r.URL.Path == "/admin/contact/1" && r.Method == http.MethodDelete: + w.WriteHeader(http.StatusOK) + case r.URL.Path == "/admin/contact/accepted/1" && r.Method == http.MethodPatch: + contact := fixtures.Contact + contact.Status = "accepted" + json.NewEncoder(w).Encode(contact) + case r.URL.Path == "/admin/contact/rejected/1" && r.Method == http.MethodPatch: + contact := fixtures.Contact + contact.Status = "rejected" + json.NewEncoder(w).Encode(contact) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client, err := NewWalletClientWithAdminKey(fixtures.XPrivString, server.URL, true) + require.NoError(t, err) + + t.Run("AdminGetContacts", func(t *testing.T) { + contacts, err := client.AdminGetContacts(context.Background(), nil, nil, nil) + assert.NoError(t, err) + assert.Equal(t, "1", contacts[0].ID) + }) + + t.Run("AdminUpdateContact", func(t *testing.T) { + contact, err := client.AdminUpdateContact(context.Background(), "1", "Jane Doe", nil) + assert.NoError(t, err) + assert.Equal(t, "Test User", contact.FullName) + }) + + t.Run("AdminDeleteContact", func(t *testing.T) { + err := client.AdminDeleteContact(context.Background(), "1") + assert.NoError(t, err) + }) + + t.Run("AdminAcceptContact", func(t *testing.T) { + contact, err := client.AdminAcceptContact(context.Background(), "1") + assert.NoError(t, err) + assert.Equal(t, models.ContactStatus("accepted"), contact.Status) + }) + + t.Run("AdminRejectContact", func(t *testing.T) { + contact, err := client.AdminRejectContact(context.Background(), "1") + assert.NoError(t, err) + assert.Equal(t, models.ContactStatus("rejected"), contact.Status) + }) +} From c13ec128bca7fe39d6b1234d82ada27d185f6b47 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Tue, 14 May 2024 16:37:39 +0200 Subject: [PATCH 10/27] correcting per comments --- admin_contacts_test.go | 19 +++++++++---------- http.go | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/admin_contacts_test.go b/admin_contacts_test.go index a202629..71ea798 100644 --- a/admin_contacts_test.go +++ b/admin_contacts_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/bitcoin-sv/spv-wallet/models" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" @@ -47,30 +46,30 @@ func TestAdminContactActions(t *testing.T) { t.Run("AdminGetContacts", func(t *testing.T) { contacts, err := client.AdminGetContacts(context.Background(), nil, nil, nil) - assert.NoError(t, err) - assert.Equal(t, "1", contacts[0].ID) + require.NoError(t, err) + require.Equal(t, "1", contacts[0].ID) }) t.Run("AdminUpdateContact", func(t *testing.T) { contact, err := client.AdminUpdateContact(context.Background(), "1", "Jane Doe", nil) - assert.NoError(t, err) - assert.Equal(t, "Test User", contact.FullName) + require.NoError(t, err) + require.Equal(t, "Test User", contact.FullName) }) t.Run("AdminDeleteContact", func(t *testing.T) { err := client.AdminDeleteContact(context.Background(), "1") - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("AdminAcceptContact", func(t *testing.T) { contact, err := client.AdminAcceptContact(context.Background(), "1") - assert.NoError(t, err) - assert.Equal(t, models.ContactStatus("accepted"), contact.Status) + require.NoError(t, err) + require.Equal(t, models.ContactStatus("accepted"), contact.Status) }) t.Run("AdminRejectContact", func(t *testing.T) { contact, err := client.AdminRejectContact(context.Background(), "1") - assert.NoError(t, err) - assert.Equal(t, models.ContactStatus("rejected"), contact.Status) + require.NoError(t, err) + require.Equal(t, models.ContactStatus("rejected"), contact.Status) }) } diff --git a/http.go b/http.go index 3bfeedd..a9fd69f 100644 --- a/http.go +++ b/http.go @@ -541,7 +541,7 @@ func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *models } publicKey := privateKey.PubKey() - // Get the xPub + // Get the AccessKey payload = new(models.AuthPayload) payload.AccessKey = hex.EncodeToString(publicKey.SerialiseCompressed()) From c9591a5a0777fb83a13ebf7170d9ecab4c8dcb76 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Tue, 14 May 2024 16:44:53 +0200 Subject: [PATCH 11/27] correcting examples code --- examples/http_with_access_key/http_with_access_key.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/http_with_access_key/http_with_access_key.go b/examples/http_with_access_key/http_with_access_key.go index 85ac44f..684651a 100644 --- a/examples/http_with_access_key/http_with_access_key.go +++ b/examples/http_with_access_key/http_with_access_key.go @@ -1,15 +1,14 @@ package main -import walletclient "github.com/bitcoin-sv/spv-wallet-go-client" +import ( + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" +) func main() { // Replace with created access key exampleAccessKey := "some_generated_access_key" // Create a client - _, _ = walletclient.New( - walletclient.WithAccessKey(exampleAccessKey), - walletclient.WithHTTP("http://localhost:3003/v1"), - walletclient.WithSignRequest(true), - ) + _, _ = walletclient.NewWalletClientWithAccessKey(exampleAccessKey, "http://localhost:3003/v1", true) + } From cb92eb8c66deaf198e938723459536e804e3c26d Mon Sep 17 00:00:00 2001 From: ac4ch Date: Tue, 14 May 2024 16:48:51 +0200 Subject: [PATCH 12/27] correcting examples code --- examples/new_paymail/new_paymail.go | 9 ++------- examples/register_xpub/register_xpub.go | 17 ++++++----------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/examples/new_paymail/new_paymail.go b/examples/new_paymail/new_paymail.go index bfbc836..c2691e0 100644 --- a/examples/new_paymail/new_paymail.go +++ b/examples/new_paymail/new_paymail.go @@ -12,11 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - walletclient, _ := walletclient.New( - walletclient.WithXPriv(keys.XPriv()), - walletclient.WithHTTP("localhost:3001"), - walletclient.WithSignRequest(true), - ) - - walletclient.AdminCreatePaymail(context.Background(), keys.XPub().String(), "foo@domain.com", "", "Foo") + wc, _ := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), "localhost:3001", true) + wc.AdminCreatePaymail(context.Background(), keys.XPub().String(), "foo@domain.com", "", "Foo") } diff --git a/examples/register_xpub/register_xpub.go b/examples/register_xpub/register_xpub.go index 73471cd..5b4b095 100644 --- a/examples/register_xpub/register_xpub.go +++ b/examples/register_xpub/register_xpub.go @@ -3,10 +3,11 @@ package main import ( "context" "fmt" - "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" "github.com/bitcoin-sv/spv-wallet/models" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" ) func main() { @@ -14,17 +15,11 @@ func main() { keys, _ := xpriv.Generate() // Create a client - walletClient, _ := walletclient.New( - walletclient.WithXPriv(keys.XPriv()), - walletclient.WithHTTP("localhost:3003/v1"), - walletclient.WithSignRequest(true), - ) - + wc, _ := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), "localhost:3003/v1", true) ctx := context.Background() + _ = wc.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) - _ = walletClient.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) - - xpubKey, err := walletClient.GetXPub(ctx) + xpubKey, err := wc.GetXPub(ctx) if err != nil { fmt.Println(err) } From afe07aa9d1baa298a75709298cbb6cb0a1670df4 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Tue, 14 May 2024 16:53:57 +0200 Subject: [PATCH 13/27] correcting examples code --- examples/http/http.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/http/http.go b/examples/http/http.go index c1c201d..9e146d5 100644 --- a/examples/http/http.go +++ b/examples/http/http.go @@ -12,10 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - client, _ := walletclient.New( - walletclient.WithXPriv(keys.XPriv()), - walletclient.WithHTTP("localhost:3001"), - walletclient.WithSignRequest(true), - ) - fmt.Println(client.IsSignRequest()) + wc, _ := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), "localhost:3001", true) + fmt.Println(wc.IsSignRequest()) } From 5d589095b62f1948d17d0e3f14bc16c6449c1c28 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 13:12:20 +0200 Subject: [PATCH 14/27] addressing comments in review per naming --- access_keys_test.go | 2 +- admin_contacts_test.go | 2 +- contacts_test.go | 2 +- destinations_test.go | 2 +- examples/http/http.go | 2 +- .../http_with_access_key.go | 2 +- examples/new_paymail/new_paymail.go | 2 +- examples/register_xpub/register_xpub.go | 2 +- totp_test.go | 14 ++++++------ transactions_test.go | 2 +- walletclient.go | 22 +++++++++++-------- walletclient_test.go | 16 +++++++------- xpubs_test.go | 2 +- 13 files changed, 38 insertions(+), 34 deletions(-) diff --git a/access_keys_test.go b/access_keys_test.go index e86552d..c19e6f9 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -33,7 +33,7 @@ func TestAccessKeys(t *testing.T) { })) defer server.Close() - client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, server.URL, true) + client, err := NewWithAccessKey(fixtures.AccessKeyString, server.URL) require.NoError(t, err) t.Run("GetAccessKey", func(t *testing.T) { diff --git a/admin_contacts_test.go b/admin_contacts_test.go index 71ea798..f5221cf 100644 --- a/admin_contacts_test.go +++ b/admin_contacts_test.go @@ -41,7 +41,7 @@ func TestAdminContactActions(t *testing.T) { })) defer server.Close() - client, err := NewWalletClientWithAdminKey(fixtures.XPrivString, server.URL, true) + client, err := NewWithAdminKey(fixtures.XPrivString, server.URL) require.NoError(t, err) t.Run("AdminGetContacts", func(t *testing.T) { diff --git a/contacts_test.go b/contacts_test.go index cfbaf37..fe0e126 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -43,7 +43,7 @@ func TestContactActionsRouting(t *testing.T) { })) defer server.Close() - client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, server.URL, true) + client, err := NewWithAccessKey(fixtures.AccessKeyString, server.URL) require.NoError(t, err) t.Run("RejectContact", func(t *testing.T) { diff --git a/destinations_test.go b/destinations_test.go index b6cf7c6..c1345ee 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -41,7 +41,7 @@ func TestDestinations(t *testing.T) { })) defer server.Close() - client, err := NewWalletClientWithAccessKey(fixtures.AccessKeyString, server.URL, true) + client, err := NewWithAccessKey(fixtures.AccessKeyString, server.URL) require.NoError(t, err) t.Run("GetDestinationByID", func(t *testing.T) { diff --git a/examples/http/http.go b/examples/http/http.go index 9e146d5..e5a8ea1 100644 --- a/examples/http/http.go +++ b/examples/http/http.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc, _ := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), "localhost:3001", true) + wc, _ := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") fmt.Println(wc.IsSignRequest()) } diff --git a/examples/http_with_access_key/http_with_access_key.go b/examples/http_with_access_key/http_with_access_key.go index 684651a..26b04f2 100644 --- a/examples/http_with_access_key/http_with_access_key.go +++ b/examples/http_with_access_key/http_with_access_key.go @@ -9,6 +9,6 @@ func main() { exampleAccessKey := "some_generated_access_key" // Create a client - _, _ = walletclient.NewWalletClientWithAccessKey(exampleAccessKey, "http://localhost:3003/v1", true) + _, _ = walletclient.NewWithAccessKey(exampleAccessKey, "http://localhost:3003/v1") } diff --git a/examples/new_paymail/new_paymail.go b/examples/new_paymail/new_paymail.go index c2691e0..3ed39c1 100644 --- a/examples/new_paymail/new_paymail.go +++ b/examples/new_paymail/new_paymail.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc, _ := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), "localhost:3001", true) + wc, _ := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") wc.AdminCreatePaymail(context.Background(), keys.XPub().String(), "foo@domain.com", "", "Foo") } diff --git a/examples/register_xpub/register_xpub.go b/examples/register_xpub/register_xpub.go index 5b4b095..78ac0ae 100644 --- a/examples/register_xpub/register_xpub.go +++ b/examples/register_xpub/register_xpub.go @@ -15,7 +15,7 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc, _ := walletclient.NewWalletClientWithXPrivate(keys.XPriv(), "localhost:3003/v1", true) + wc, _ := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3003/v1") ctx := context.Background() _ = wc.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) diff --git a/totp_test.go b/totp_test.go index fbec16d..7b058d7 100644 --- a/totp_test.go +++ b/totp_test.go @@ -17,7 +17,7 @@ import ( func TestGenerateTotpForContact(t *testing.T) { t.Run("success", func(t *testing.T) { // given - sut, err := NewWalletClientWithXPrivate(fixtures.XPrivString, "localhost:3001", false) + sut, err := NewWithXPriv(fixtures.XPrivString, "localhost:3001") require.NoError(t, err) contact := models.Contact{PubKey: fixtures.PubKey} @@ -32,7 +32,7 @@ func TestGenerateTotpForContact(t *testing.T) { t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { // given - sut, err := NewWalletClientWithXPublic(fixtures.XPubString, "localhost:3001", false) + sut, err := NewWithXPub(fixtures.XPubString, "localhost:3001") require.NoError(t, err) // when @@ -44,7 +44,7 @@ func TestGenerateTotpForContact(t *testing.T) { t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { // given - sut, err := NewWalletClientWithXPrivate(fixtures.XPrivString, "localhost:3001", false) + sut, err := NewWithXPriv(fixtures.XPrivString, "localhost:3001") require.NoError(t, err) contact := models.Contact{PubKey: "invalid-pk-format"} @@ -73,9 +73,9 @@ func TestValidateTotpForContact(t *testing.T) { require.NoError(t, err) // Set up the WalletClient for Alice and Bob - clientAlice, err := NewWalletClientWithXPrivate(aliceKeys.XPriv(), server.URL, true) + clientAlice, err := NewWithXPriv(aliceKeys.XPriv(), server.URL) require.NoError(t, err) - clientBob, err := NewWalletClientWithXPrivate(bobKeys.XPriv(), server.URL, true) + clientBob, err := NewWithXPriv(bobKeys.XPriv(), server.URL) require.NoError(t, err) aliceContact := &models.Contact{ @@ -97,13 +97,13 @@ func TestValidateTotpForContact(t *testing.T) { }) t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { - client, err := NewWalletClientWithXPublic("invalid_xpub", server.URL, true) + client, err := NewWithXPub("invalid_xpub", server.URL) require.Error(t, err) require.Nil(t, client) }) t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { - sut, err := NewWalletClientWithXPrivate(fixtures.XPrivString, server.URL, true) + sut, err := NewWithXPriv(fixtures.XPrivString, server.URL) require.NoError(t, err) invalidContact := &models.Contact{ diff --git a/transactions_test.go b/transactions_test.go index 0dc2819..c6ea732 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -53,7 +53,7 @@ func TestTransactions(t *testing.T) { })) defer server.Close() - client, err := NewWalletClientWithXPrivate(fixtures.XPrivString, server.URL, true) + client, err := NewWithXPriv(fixtures.XPrivString, server.URL) require.NoError(t, err) t.Run("GetTransaction", func(t *testing.T) { diff --git a/walletclient.go b/walletclient.go index 60babd5..f350025 100644 --- a/walletclient.go +++ b/walletclient.go @@ -30,11 +30,11 @@ type WalletClient struct { // - `xPriv`: The extended private key used for cryptographic operations. // - `serverURL`: The URL of the server the client will interact with. // - `sign`: A boolean flag to determine if the outgoing requests should be signed. -func NewWalletClientWithXPrivate(xPriv, serverURL string, sign bool) (*WalletClient, error) { +func NewWithXPriv(xPriv, serverURL string) (*WalletClient, error) { return newWalletClient( &WithXPriv{XPrivString: &xPriv}, &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: &sign}, + &WithSignRequest{Sign: Ptr(true)}, ) } @@ -43,11 +43,11 @@ func NewWalletClientWithXPrivate(xPriv, serverURL string, sign bool) (*WalletCli // - `xPub`: The extended public key used for cryptographic verification and other public operations. // - `serverURL`: The URL of the server the client will interact with. // - `sign`: A boolean flag to determine if the outgoing requests should be signed. -func NewWalletClientWithXPublic(xPub, serverURL string, sign bool) (*WalletClient, error) { +func NewWithXPub(xPub, serverURL string) (*WalletClient, error) { return newWalletClient( &WithXPub{XPubString: &xPub}, &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: &sign}, + &WithSignRequest{Sign: Ptr(false)}, ) } @@ -56,12 +56,12 @@ func NewWalletClientWithXPublic(xPub, serverURL string, sign bool) (*WalletClien // - `adminKey`: The extended private key used for administrative operations. // - `serverURL`: The URL of the server the client will interact with. // - `sign`: A boolean flag to determine if the outgoing requests should be signed. -func NewWalletClientWithAdminKey(adminKey, serverURL string, sign bool) (*WalletClient, error) { +func NewWithAdminKey(adminKey, serverURL string) (*WalletClient, error) { return newWalletClient( - &WithXPriv{XPrivString: &adminKey}, + &WithXPriv{XPrivString: &adminKey}, // this need to be removed .. pls ignore for now &WithAdminKey{AdminKeyString: &adminKey}, &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: &sign}, + &WithSignRequest{Sign: Ptr(true)}, ) } @@ -70,11 +70,11 @@ func NewWalletClientWithAdminKey(adminKey, serverURL string, sign bool) (*Wallet // - `accessKey`: The access key used for API authentication. // - `serverURL`: The URL of the server the client will interact with. // - `sign`: A boolean flag to determine if the outgoing requests should be signed. -func NewWalletClientWithAccessKey(accessKey, serverURL string, sign bool) (*WalletClient, error) { +func NewWithAccessKey(accessKey, serverURL string) (*WalletClient, error) { return newWalletClient( &WithAccessKey{AccessKeyString: &accessKey}, &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: &sign}, + &WithSignRequest{Sign: Ptr(true)}, ) } @@ -149,3 +149,7 @@ func processMetadata(metadata *models.Metadata) *models.Metadata { func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) ResponseError { return setSignature(header, xPriv, bodyString) } + +func Ptr[T any](obj T) *T { + return &obj +} diff --git a/walletclient_test.go b/walletclient_test.go index d407245..6f1313f 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -23,7 +23,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPrivate success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client, err := NewWalletClientWithXPrivate(keys.XPriv(), server.URL, true) + client, err := NewWithXPriv(keys.XPriv(), server.URL) require.NoError(t, err) require.NotNil(t, client) require.Equal(t, keys.XPriv(), *client.xPrivString) @@ -38,7 +38,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPrivate fail", func(t *testing.T) { xPriv := "invalid_key" - client, err := NewWalletClientWithXPrivate(xPriv, "http://example.com", true) + client, err := NewWithXPriv(xPriv, "http://example.com") require.Error(t, err) // Expect error due to invalid key require.Nil(t, client) }) @@ -46,7 +46,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPublic success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client, err := NewWalletClientWithXPublic(keys.XPub().String(), server.URL, false) + client, err := NewWithXPub(keys.XPub().String(), server.URL) require.NoError(t, err) require.NotNil(t, client) require.Equal(t, keys.XPub().String(), *client.xPubString) @@ -61,13 +61,13 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPublic fail", func(t *testing.T) { xpub := "invalid_key" - client, err := NewWalletClientWithXPublic(xpub, server.URL, false) + client, err := NewWithXPub(xpub, server.URL) require.Error(t, err) // Expect error due to invalid key require.Nil(t, client) }) t.Run("NewWalletClientWithAdminKey success", func(t *testing.T) { - client, err := NewWalletClientWithAdminKey(fixtures.XPrivString, server.URL, true) + client, err := NewWithAdminKey(fixtures.XPrivString, server.URL) require.NoError(t, err) require.NotNil(t, client) require.Equal(t, fixtures.XPrivString, *client.xPrivString) @@ -84,7 +84,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithAdminKey fail", func(t *testing.T) { adminKey := "invalid_key" - client, err := NewWalletClientWithAdminKey(adminKey, server.URL, true) + client, err := NewWithAdminKey(adminKey, server.URL) require.Error(t, err) require.Nil(t, client) }) @@ -92,7 +92,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithAccessKey success", func(t *testing.T) { // Attempt to create a new WalletClient with an access key accessKey := fixtures.AccessKeyString - client, err := NewWalletClientWithAccessKey(accessKey, server.URL, true) + client, err := NewWithAccessKey(accessKey, server.URL) require.NoError(t, err) require.NotNil(t, client) @@ -110,7 +110,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithAccessKey fail", func(t *testing.T) { accessKey := "invalid_key" - client, err := NewWalletClientWithAccessKey(accessKey, server.URL, true) + client, err := NewWithAccessKey(accessKey, server.URL) require.Error(t, err) require.Nil(t, client) diff --git a/xpubs_test.go b/xpubs_test.go index 52b3bb9..9fc6e1c 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -43,7 +43,7 @@ func TestXpub(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client, err := NewWalletClientWithXPrivate(keys.XPriv(), server.URL, true) + client, err := NewWithXPriv(keys.XPriv(), server.URL) require.NoError(t, err) t.Run("GetXPub", func(t *testing.T) { From 6406f0c941f4fa2d76b51bf0c31fc8c861091353 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 15:52:47 +0200 Subject: [PATCH 15/27] changing adminkey constructor to use the admin key --- http.go | 80 ++++++++++++++++++++++---------------------- walletclient.go | 17 +++++++--- walletclient_test.go | 2 +- 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/http.go b/http.go index a9fd69f..0569d94 100644 --- a/http.go +++ b/http.go @@ -38,7 +38,7 @@ func (wc *WalletClient) SetAdminKey(adminKey *bip32.ExtendedKey) { func (wc *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, ResponseError) { var xPub models.Xpub if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/xpub", nil, wc.xPriv, true, &xPub, + ctx, http.MethodGet, "/xpub", nil, wc.getPrivKey(), true, &xPub, ); err != nil { return nil, err } @@ -57,7 +57,7 @@ func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models var xPub models.Xpub if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/xpub", jsonStr, wc.xPriv, true, &xPub, + ctx, http.MethodPatch, "/xpub", jsonStr, wc.getPrivKey(), true, &xPub, ); err != nil { return nil, err } @@ -69,7 +69,7 @@ func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models func (wc *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, + ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, wc.getPrivKey(), true, &accessKey, ); err != nil { return nil, err } @@ -87,7 +87,7 @@ func (wc *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *m } var accessKey []*models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key/search", jsonStr, wc.xPriv, true, &accessKey, + ctx, http.MethodPost, "/access-key/search", jsonStr, wc.getPrivKey(), true, &accessKey, ); err != nil { return nil, err } @@ -107,7 +107,7 @@ func (wc *WalletClient) GetAccessKeysCount(ctx context.Context, conditions map[s var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key/count", jsonStr, wc.xPriv, true, &count, + ctx, http.MethodPost, "/access-key/count", jsonStr, wc.getPrivKey(), true, &count, ); err != nil { return 0, err } @@ -119,7 +119,7 @@ func (wc *WalletClient) GetAccessKeysCount(ctx context.Context, conditions map[s func (wc *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, + ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, wc.getPrivKey(), true, &accessKey, ); err != nil { return nil, err } @@ -137,7 +137,7 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Me } var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key", jsonStr, wc.xPriv, true, &accessKey, + ctx, http.MethodPost, "/access-key", jsonStr, wc.getPrivKey(), true, &accessKey, ); err != nil { return nil, err } @@ -149,7 +149,7 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Me func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, wc.xPriv, true, &destination, + ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -161,7 +161,7 @@ func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*mod func (wc *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, wc.xPriv, true, &destination, + ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -173,7 +173,7 @@ func (wc *WalletClient) GetDestinationByAddress(ctx context.Context, address str func (wc *WalletClient) GetDestinationByLockingScript(ctx context.Context, lockingScript string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, wc.xPriv, true, &destination, + ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -191,7 +191,7 @@ func (wc *WalletClient) GetDestinations(ctx context.Context, metadataConditions } var destinations []*models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination/search", jsonStr, wc.xPriv, true, &destinations, + ctx, http.MethodPost, "/destination/search", jsonStr, wc.getPrivKey(), true, &destinations, ); err != nil { return nil, err } @@ -211,7 +211,7 @@ func (wc *WalletClient) GetDestinationsCount(ctx context.Context, conditions map var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination/count", jsonStr, wc.xPriv, true, &count, + ctx, http.MethodPost, "/destination/count", jsonStr, wc.getPrivKey(), true, &count, ); err != nil { return 0, err } @@ -229,7 +229,7 @@ func (wc *WalletClient) NewDestination(ctx context.Context, metadata *models.Met } var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination", jsonStr, wc.xPriv, true, &destination, + ctx, http.MethodPost, "/destination", jsonStr, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -251,7 +251,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id st var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, + ctx, http.MethodPatch, "/destination", jsonStr, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -273,7 +273,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, + ctx, http.MethodPatch, "/destination", jsonStr, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -295,7 +295,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Con var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, + ctx, http.MethodPatch, "/destination", jsonStr, wc.getPrivKey(), true, &destination, ); err != nil { return nil, err } @@ -306,7 +306,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Con // GetTransaction will get a transaction by ID func (wc *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, ResponseError) { var transaction models.Transaction - if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.xPriv, *wc.signRequest, &transaction); err != nil { + if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.getPrivKey(), *wc.signRequest, &transaction); err != nil { return nil, err } @@ -326,7 +326,7 @@ func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[stri var transactions []*models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/search", jsonStr, wc.xPriv, *wc.signRequest, &transactions, + ctx, http.MethodPost, "/transaction/search", jsonStr, wc.getPrivKey(), *wc.signRequest, &transactions, ); err != nil { return nil, err } @@ -348,7 +348,7 @@ func (wc *WalletClient) GetTransactionsCount(ctx context.Context, conditions map var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/count", jsonStr, wc.xPriv, *wc.signRequest, &count, + ctx, http.MethodPost, "/transaction/count", jsonStr, wc.getPrivKey(), *wc.signRequest, &count, ); err != nil { return 0, err } @@ -398,7 +398,7 @@ func (wc *WalletClient) createDraftTransaction(ctx context.Context, var draftTransaction *models.DraftTransaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction", jsonStr, wc.xPriv, true, &draftTransaction, + ctx, http.MethodPost, "/transaction", jsonStr, wc.getPrivKey(), true, &draftTransaction, ); err != nil { return nil, err } @@ -424,7 +424,7 @@ func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/record", jsonStr, wc.xPriv, *wc.signRequest, &transaction, + ctx, http.MethodPost, "/transaction/record", jsonStr, wc.getPrivKey(), *wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -446,7 +446,7 @@ func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID stri var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/transaction", jsonStr, wc.xPriv, *wc.signRequest, &transaction, + ctx, http.MethodPatch, "/transaction", jsonStr, wc.getPrivKey(), *wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -476,7 +476,7 @@ func (wc *WalletClient) GetUtxo(ctx context.Context, txID string, outputIndex ui var utxo models.Utxo if err := wc.doHTTPRequest( - ctx, http.MethodGet, url, nil, wc.xPriv, true, &utxo, + ctx, http.MethodGet, url, nil, wc.getPrivKey(), true, &utxo, ); err != nil { return nil, err } @@ -497,7 +497,7 @@ func (wc *WalletClient) GetUtxos(ctx context.Context, conditions map[string]inte var utxos []*models.Utxo if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/search", jsonStr, wc.xPriv, *wc.signRequest, &utxos, + ctx, http.MethodPost, "/utxo/search", jsonStr, wc.getPrivKey(), *wc.signRequest, &utxos, ); err != nil { return nil, err } @@ -517,7 +517,7 @@ func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions map[string var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/count", jsonStr, wc.xPriv, *wc.signRequest, &count, + ctx, http.MethodPost, "/utxo/count", jsonStr, wc.getPrivKey(), *wc.signRequest, &count, ); err != nil { return 0, err } @@ -625,7 +625,7 @@ func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []b // AcceptContact will accept the contact associated with the paymail func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.xPriv, *wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.getPrivKey(), *wc.signRequest, nil, ); err != nil { return err } @@ -636,7 +636,7 @@ func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) Respo // RejectContact will reject the contact associated with the paymail func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.xPriv, *wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.getPrivKey(), *wc.signRequest, nil, ); err != nil { return err } @@ -656,7 +656,7 @@ func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Cont } if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.xPriv, *wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.getPrivKey(), *wc.signRequest, nil, ); err != nil { return err } @@ -677,7 +677,7 @@ func (wc *WalletClient) GetContacts(ctx context.Context, conditions map[string]i var result []*models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/contact/search", jsonStr, wc.xPriv, *wc.signRequest, &result, + ctx, http.MethodPost, "/contact/search", jsonStr, wc.getPrivKey(), *wc.signRequest, &result, ); err != nil { return nil, err } @@ -707,7 +707,7 @@ func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fu var result models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.xPriv, *wc.signRequest, &result, + ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.getPrivKey(), *wc.signRequest, &result, ); err != nil { return nil, err } @@ -741,7 +741,7 @@ func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metada func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError) { var status bool if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/status", nil, wc.xPriv, true, &status, + ctx, http.MethodGet, "/admin/status", nil, wc.getPrivKey(), true, &status, ); err != nil { return false, err } @@ -753,7 +753,7 @@ func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) { var stats *models.AdminStats if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/stats", nil, wc.xPriv, true, &stats, + ctx, http.MethodGet, "/admin/stats", nil, wc.getPrivKey(), true, &stats, ); err != nil { return nil, err } @@ -829,7 +829,7 @@ func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*m var model *models.PaymailAddress if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.xPriv, true, &model, + ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.getPrivKey(), true, &model, ); err != nil { return nil, err } @@ -870,7 +870,7 @@ func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, var model *models.PaymailAddress if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.xPriv, true, &model, + ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.getPrivKey(), true, &model, ); err != nil { return nil, err } @@ -888,7 +888,7 @@ func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) } if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.xPriv, true, nil, + ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.getPrivKey(), true, nil, ); err != nil { return err } @@ -966,7 +966,7 @@ func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[strin } if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &models, + ctx, http.MethodPost, path, jsonStr, wc.getPrivKey(), true, &models, ); err != nil { return err } @@ -985,7 +985,7 @@ func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]in var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.xPriv, true, &count, + ctx, http.MethodPost, path, jsonStr, wc.getPrivKey(), true, &count, ); err != nil { return 0, err } @@ -1004,7 +1004,7 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.xPriv, *wc.signRequest, &transaction, + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.getPrivKey(), *wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -1016,7 +1016,7 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) func (wc *WalletClient) AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) { var model *models.SharedConfig if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/shared-config", nil, wc.xPriv, true, &model, + ctx, http.MethodGet, "/admin/shared-config", nil, wc.getPrivKey(), true, &model, ); err != nil { return nil, err } @@ -1076,7 +1076,7 @@ func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*mod // FinalizeTransaction will finalize the transaction func (wc *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, ResponseError) { - res, err := GetSignedHex(draft, wc.xPriv) + res, err := GetSignedHex(draft, wc.getPrivKey()) if err != nil { return "", WrapError(err) } diff --git a/walletclient.go b/walletclient.go index f350025..bf5e643 100644 --- a/walletclient.go +++ b/walletclient.go @@ -1,6 +1,7 @@ package walletclient import ( + "fmt" "net/http" "github.com/bitcoin-sv/spv-wallet/models" @@ -29,7 +30,6 @@ type WalletClient struct { // It configures the client with a specific server URL and a flag indicating whether requests should be signed. // - `xPriv`: The extended private key used for cryptographic operations. // - `serverURL`: The URL of the server the client will interact with. -// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWithXPriv(xPriv, serverURL string) (*WalletClient, error) { return newWalletClient( &WithXPriv{XPrivString: &xPriv}, @@ -42,7 +42,6 @@ func NewWithXPriv(xPriv, serverURL string) (*WalletClient, error) { // This client is configured for operations that require a public key, such as verifying signatures or receiving transactions. // - `xPub`: The extended public key used for cryptographic verification and other public operations. // - `serverURL`: The URL of the server the client will interact with. -// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWithXPub(xPub, serverURL string) (*WalletClient, error) { return newWalletClient( &WithXPub{XPubString: &xPub}, @@ -55,10 +54,8 @@ func NewWithXPub(xPub, serverURL string) (*WalletClient, error) { // This configuration is typically used for administrative tasks such as managing sub-wallets or configuring system-wide settings. // - `adminKey`: The extended private key used for administrative operations. // - `serverURL`: The URL of the server the client will interact with. -// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWithAdminKey(adminKey, serverURL string) (*WalletClient, error) { return newWalletClient( - &WithXPriv{XPrivString: &adminKey}, // this need to be removed .. pls ignore for now &WithAdminKey{AdminKeyString: &adminKey}, &WithHTTP{ServerURL: &serverURL}, &WithSignRequest{Sign: Ptr(true)}, @@ -69,7 +66,6 @@ func NewWithAdminKey(adminKey, serverURL string) (*WalletClient, error) { // This method is useful for scenarios where the client needs to authenticate using a less sensitive key than an xPriv. // - `accessKey`: The access key used for API authentication. // - `serverURL`: The URL of the server the client will interact with. -// - `sign`: A boolean flag to determine if the outgoing requests should be signed. func NewWithAccessKey(accessKey, serverURL string) (*WalletClient, error) { return newWalletClient( &WithAccessKey{AccessKeyString: &accessKey}, @@ -97,6 +93,7 @@ func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, // initializeKeys handles the initialization of keys based on the existing fields. func (c *WalletClient) initializeKeys() error { var err error + fmt.Printf("===== %+v\n", c) switch { case c.xPrivString != nil: if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*c.xPrivString); err != nil { @@ -111,6 +108,8 @@ func (c *WalletClient) initializeKeys() error { } case c.accessKeyString != nil: return c.initializeAccessKey() + case c.adminXPriv != nil: + return nil default: return errors.New("no keys provided for initialization") } @@ -153,3 +152,11 @@ func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString stri func Ptr[T any](obj T) *T { return &obj } + +// getPrivKey retrieves the client's private key. If the primary key is not set, +func (wc *WalletClient) getPrivKey() *bip32.ExtendedKey { + if wc.adminXPriv != nil { + return wc.adminXPriv + } + return wc.xPriv +} diff --git a/walletclient_test.go b/walletclient_test.go index 6f1313f..b03bca4 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -70,7 +70,7 @@ func TestNewWalletClient(t *testing.T) { client, err := NewWithAdminKey(fixtures.XPrivString, server.URL) require.NoError(t, err) require.NotNil(t, client) - require.Equal(t, fixtures.XPrivString, *client.xPrivString) + require.Nil(t, client.xPrivString) require.Equal(t, fixtures.XPrivString, client.adminXPriv.String()) require.Equal(t, server.URL, *client.server) require.NotNil(t, client.httpClient) From 8f37026a90980b4c8acb3904854b1011a0bcefa3 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 16:03:03 +0200 Subject: [PATCH 16/27] removing debug line --- walletclient.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/walletclient.go b/walletclient.go index bf5e643..22b4770 100644 --- a/walletclient.go +++ b/walletclient.go @@ -1,7 +1,6 @@ package walletclient import ( - "fmt" "net/http" "github.com/bitcoin-sv/spv-wallet/models" @@ -93,7 +92,6 @@ func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, // initializeKeys handles the initialization of keys based on the existing fields. func (c *WalletClient) initializeKeys() error { var err error - fmt.Printf("===== %+v\n", c) switch { case c.xPrivString != nil: if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*c.xPrivString); err != nil { From b54efea74d901956eb1d3dafc9583c22f27feaa0 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 16:52:03 +0200 Subject: [PATCH 17/27] addressing some not needed fields --- client_options.go | 36 ++++++++++++++++++++++++++--- walletclient.go | 58 +++++++++++------------------------------------ 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/client_options.go b/client_options.go index 07b84b9..7796575 100644 --- a/client_options.go +++ b/client_options.go @@ -4,6 +4,9 @@ import ( "net/http" "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/wif" + "github.com/pkg/errors" ) // WalletClientConfigurator is the interface for configuring WalletClient @@ -17,7 +20,10 @@ type WithXPriv struct { } func (w *WithXPriv) Configure(c *WalletClient) { - c.xPrivString = w.XPrivString + var err error + if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*w.XPrivString); err != nil { + c.xPriv = nil + } } // WithXPub sets the xPubString on the client @@ -26,7 +32,11 @@ type WithXPub struct { } func (w *WithXPub) Configure(c *WalletClient) { - c.xPubString = w.XPubString + var err error + if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(*w.XPubString); err != nil { + w.XPubString = nil + } + } // WithAccessKey sets the accessKeyString on the client @@ -35,7 +45,10 @@ type WithAccessKey struct { } func (w *WithAccessKey) Configure(c *WalletClient) { - c.accessKeyString = w.AccessKeyString + var err error + if c.accessKey, err = w.initializeAccessKey(); err != nil { + c.accessKey = nil + } } // WithAdminKey sets the admin key for creating new xpubs @@ -75,3 +88,20 @@ type WithSignRequest struct { func (w *WithSignRequest) Configure(c *WalletClient) { c.signRequest = w.Sign } + +// initializeAccessKey handles the specific initialization of the access key. +func (c *WithAccessKey) initializeAccessKey() (*bec.PrivateKey, error) { + var err error + var privateKey *bec.PrivateKey + var decodedWIF *wif.WIF + + if decodedWIF, err = wif.DecodeWIF(*c.AccessKeyString); err != nil { + if privateKey, err = bitcoin.PrivateKeyFromString(*c.AccessKeyString); err != nil { + return nil, errors.Wrap(err, "failed to decode access key") + } + } else { + privateKey = decodedWIF.PrivKey + } + + return privateKey, nil +} diff --git a/walletclient.go b/walletclient.go index 22b4770..2aae903 100644 --- a/walletclient.go +++ b/walletclient.go @@ -4,25 +4,20 @@ import ( "net/http" "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/libsv/go-bk/wif" "github.com/pkg/errors" ) // WalletClient is the spv wallet Go client representation. type WalletClient struct { - signRequest *bool - server *string - accessKeyString *string - xPrivString *string - xPubString *string - httpClient *http.Client - accessKey *bec.PrivateKey - adminXPriv *bip32.ExtendedKey - xPriv *bip32.ExtendedKey - xPub *bip32.ExtendedKey + signRequest *bool + server *string + httpClient *http.Client + accessKey *bec.PrivateKey + adminXPriv *bip32.ExtendedKey + xPriv *bip32.ExtendedKey + xPub *bip32.ExtendedKey } // NewWalletClientWithXPrivate creates a new WalletClient instance using a private key (xPriv). @@ -91,45 +86,18 @@ func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, // initializeKeys handles the initialization of keys based on the existing fields. func (c *WalletClient) initializeKeys() error { - var err error switch { - case c.xPrivString != nil: - if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*c.xPrivString); err != nil { - return err - } - if c.xPub, err = c.xPriv.Neuter(); err != nil { - return err - } - case c.xPubString != nil: - if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(*c.xPubString); err != nil { - return err - } - case c.accessKeyString != nil: - return c.initializeAccessKey() + case c.xPriv != nil: + return nil + case c.xPub != nil: + return nil + case c.accessKey != nil: + return nil case c.adminXPriv != nil: return nil default: return errors.New("no keys provided for initialization") } - return nil -} - -// initializeAccessKey handles the specific initialization of the access key. -func (c *WalletClient) initializeAccessKey() error { - var err error - var privateKey *bec.PrivateKey - var decodedWIF *wif.WIF - - if decodedWIF, err = wif.DecodeWIF(*c.accessKeyString); err != nil { - if privateKey, err = bitcoin.PrivateKeyFromString(*c.accessKeyString); err != nil { - return errors.Wrap(err, "failed to decode access key") - } - } else { - privateKey = decodedWIF.PrivKey - } - - c.accessKey = privateKey - return nil } // processMetadata will process the metadata From 5a1e03c9ab1d846ad445dd546dbdbfe32e5c584b Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 16:55:49 +0200 Subject: [PATCH 18/27] removed init keys as is not needed any more --- walletclient.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/walletclient.go b/walletclient.go index 2aae903..bc7dcb4 100644 --- a/walletclient.go +++ b/walletclient.go @@ -6,7 +6,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/models" "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" - "github.com/pkg/errors" ) // WalletClient is the spv wallet Go client representation. @@ -76,30 +75,9 @@ func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, configurator.Configure(client) } - // Initialize keys based on provided strings - if err := client.initializeKeys(); err != nil { - return nil, err - } - return client, nil } -// initializeKeys handles the initialization of keys based on the existing fields. -func (c *WalletClient) initializeKeys() error { - switch { - case c.xPriv != nil: - return nil - case c.xPub != nil: - return nil - case c.accessKey != nil: - return nil - case c.adminXPriv != nil: - return nil - default: - return errors.New("no keys provided for initialization") - } -} - // processMetadata will process the metadata func processMetadata(metadata *models.Metadata) *models.Metadata { if metadata == nil { From 513a5b6fc40f4296089d26affdc3542de330882a Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 17:41:10 +0200 Subject: [PATCH 19/27] correcting units per changes --- access_keys_test.go | 4 +- admin_contacts_test.go | 4 +- contacts_test.go | 4 +- destinations_test.go | 4 +- examples/http/http.go | 2 +- .../http_with_access_key.go | 2 +- examples/new_paymail/new_paymail.go | 2 +- examples/register_xpub/register_xpub.go | 2 +- totp_test.go | 37 ++++++-------- transactions_test.go | 4 +- walletclient.go | 12 ++--- walletclient_test.go | 49 +++++++------------ xpubs_test.go | 4 +- 13 files changed, 57 insertions(+), 73 deletions(-) diff --git a/access_keys_test.go b/access_keys_test.go index c19e6f9..4e8ffbc 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -33,8 +33,8 @@ func TestAccessKeys(t *testing.T) { })) defer server.Close() - client, err := NewWithAccessKey(fixtures.AccessKeyString, server.URL) - require.NoError(t, err) + client := NewWithAccessKey(fixtures.AccessKeyString, server.URL) + require.NotNil(t, client.accessKey) t.Run("GetAccessKey", func(t *testing.T) { accessKey, err := client.GetAccessKey(context.Background(), fixtures.AccessKey.ID) diff --git a/admin_contacts_test.go b/admin_contacts_test.go index f5221cf..fda9ee3 100644 --- a/admin_contacts_test.go +++ b/admin_contacts_test.go @@ -41,8 +41,8 @@ func TestAdminContactActions(t *testing.T) { })) defer server.Close() - client, err := NewWithAdminKey(fixtures.XPrivString, server.URL) - require.NoError(t, err) + client := NewWithAdminKey(fixtures.XPrivString, server.URL) + require.NotNil(t, client.adminXPriv) t.Run("AdminGetContacts", func(t *testing.T) { contacts, err := client.AdminGetContacts(context.Background(), nil, nil, nil) diff --git a/contacts_test.go b/contacts_test.go index fe0e126..71c2217 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -43,8 +43,8 @@ func TestContactActionsRouting(t *testing.T) { })) defer server.Close() - client, err := NewWithAccessKey(fixtures.AccessKeyString, server.URL) - require.NoError(t, err) + client := NewWithAccessKey(fixtures.AccessKeyString, server.URL) + require.NotNil(t, client.accessKey) t.Run("RejectContact", func(t *testing.T) { err := client.RejectContact(context.Background(), fixtures.PaymailAddress) diff --git a/destinations_test.go b/destinations_test.go index c1345ee..240eedc 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -41,8 +41,8 @@ func TestDestinations(t *testing.T) { })) defer server.Close() - client, err := NewWithAccessKey(fixtures.AccessKeyString, server.URL) - require.NoError(t, err) + client := NewWithAccessKey(fixtures.AccessKeyString, server.URL) + require.NotNil(t, client.accessKey) t.Run("GetDestinationByID", func(t *testing.T) { destination, err := client.GetDestinationByID(context.Background(), fixtures.Destination.ID) diff --git a/examples/http/http.go b/examples/http/http.go index e5a8ea1..ae1abca 100644 --- a/examples/http/http.go +++ b/examples/http/http.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc, _ := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") + wc := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") fmt.Println(wc.IsSignRequest()) } diff --git a/examples/http_with_access_key/http_with_access_key.go b/examples/http_with_access_key/http_with_access_key.go index 26b04f2..2468bef 100644 --- a/examples/http_with_access_key/http_with_access_key.go +++ b/examples/http_with_access_key/http_with_access_key.go @@ -9,6 +9,6 @@ func main() { exampleAccessKey := "some_generated_access_key" // Create a client - _, _ = walletclient.NewWithAccessKey(exampleAccessKey, "http://localhost:3003/v1") + _ = walletclient.NewWithAccessKey(exampleAccessKey, "http://localhost:3003/v1") } diff --git a/examples/new_paymail/new_paymail.go b/examples/new_paymail/new_paymail.go index 3ed39c1..72c49e9 100644 --- a/examples/new_paymail/new_paymail.go +++ b/examples/new_paymail/new_paymail.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc, _ := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") + wc := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") wc.AdminCreatePaymail(context.Background(), keys.XPub().String(), "foo@domain.com", "", "Foo") } diff --git a/examples/register_xpub/register_xpub.go b/examples/register_xpub/register_xpub.go index 78ac0ae..c5e42b6 100644 --- a/examples/register_xpub/register_xpub.go +++ b/examples/register_xpub/register_xpub.go @@ -15,7 +15,7 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc, _ := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3003/v1") + wc := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3003/v1") ctx := context.Background() _ = wc.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) diff --git a/totp_test.go b/totp_test.go index 7b058d7..de1055f 100644 --- a/totp_test.go +++ b/totp_test.go @@ -17,11 +17,10 @@ import ( func TestGenerateTotpForContact(t *testing.T) { t.Run("success", func(t *testing.T) { // given - sut, err := NewWithXPriv(fixtures.XPrivString, "localhost:3001") - require.NoError(t, err) + sut := NewWithXPriv(fixtures.XPrivString, "localhost:3001") + require.NotNil(t, sut.xPriv) contact := models.Contact{PubKey: fixtures.PubKey} - // when pass, err := sut.GenerateTotpForContact(&contact, 30, 2) @@ -32,11 +31,10 @@ func TestGenerateTotpForContact(t *testing.T) { t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { // given - sut, err := NewWithXPub(fixtures.XPubString, "localhost:3001") - require.NoError(t, err) - + sut := NewWithXPub(fixtures.XPubString, "localhost:3001") + require.NotNil(t, sut.xPub) // when - _, err = sut.GenerateTotpForContact(nil, 30, 2) + _, err := sut.GenerateTotpForContact(nil, 30, 2) // then require.ErrorIs(t, err, ErrClientInitNoXpriv) @@ -44,13 +42,12 @@ func TestGenerateTotpForContact(t *testing.T) { t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { // given - sut, err := NewWithXPriv(fixtures.XPrivString, "localhost:3001") - require.NoError(t, err) + sut := NewWithXPriv(fixtures.XPrivString, "localhost:3001") + require.NotNil(t, sut.xPriv) contact := models.Contact{PubKey: "invalid-pk-format"} - // when - _, err = sut.GenerateTotpForContact(&contact, 30, 2) + _, err := sut.GenerateTotpForContact(&contact, 30, 2) // then require.ErrorContains(t, err, "contact's PubKey is invalid:") @@ -73,10 +70,10 @@ func TestValidateTotpForContact(t *testing.T) { require.NoError(t, err) // Set up the WalletClient for Alice and Bob - clientAlice, err := NewWithXPriv(aliceKeys.XPriv(), server.URL) - require.NoError(t, err) - clientBob, err := NewWithXPriv(bobKeys.XPriv(), server.URL) - require.NoError(t, err) + clientAlice := NewWithXPriv(aliceKeys.XPriv(), server.URL) + require.NotNil(t, clientAlice.xPriv) + clientBob := NewWithXPriv(bobKeys.XPriv(), server.URL) + require.NotNil(t, clientBob.xPriv) aliceContact := &models.Contact{ PubKey: makeMockPKI(aliceKeys.XPub().String()), @@ -97,21 +94,19 @@ func TestValidateTotpForContact(t *testing.T) { }) t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { - client, err := NewWithXPub("invalid_xpub", server.URL) - require.Error(t, err) - require.Nil(t, client) + client := NewWithXPub("invalid_xpub", server.URL) + require.Nil(t, client.xPub) }) t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { - sut, err := NewWithXPriv(fixtures.XPrivString, server.URL) - require.NoError(t, err) + sut := NewWithXPriv(fixtures.XPrivString, server.URL) invalidContact := &models.Contact{ PubKey: "invalid_pub_key_format", Paymail: "invalid@example.com", } - _, err = sut.ValidateTotpForContact(invalidContact, "123456", "someone@example.com", 3600, 6) + _, err := sut.ValidateTotpForContact(invalidContact, "123456", "someone@example.com", 3600, 6) require.Error(t, err) require.Contains(t, err.Error(), "contact's PubKey is invalid") }) diff --git a/transactions_test.go b/transactions_test.go index c6ea732..da86efa 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -53,8 +53,8 @@ func TestTransactions(t *testing.T) { })) defer server.Close() - client, err := NewWithXPriv(fixtures.XPrivString, server.URL) - require.NoError(t, err) + client := NewWithXPriv(fixtures.XPrivString, server.URL) + require.NotNil(t, client.xPriv) t.Run("GetTransaction", func(t *testing.T) { tx, err := client.GetTransaction(context.Background(), fixtures.Transaction.ID) diff --git a/walletclient.go b/walletclient.go index bc7dcb4..0e73591 100644 --- a/walletclient.go +++ b/walletclient.go @@ -23,7 +23,7 @@ type WalletClient struct { // It configures the client with a specific server URL and a flag indicating whether requests should be signed. // - `xPriv`: The extended private key used for cryptographic operations. // - `serverURL`: The URL of the server the client will interact with. -func NewWithXPriv(xPriv, serverURL string) (*WalletClient, error) { +func NewWithXPriv(xPriv, serverURL string) *WalletClient { return newWalletClient( &WithXPriv{XPrivString: &xPriv}, &WithHTTP{ServerURL: &serverURL}, @@ -35,7 +35,7 @@ func NewWithXPriv(xPriv, serverURL string) (*WalletClient, error) { // This client is configured for operations that require a public key, such as verifying signatures or receiving transactions. // - `xPub`: The extended public key used for cryptographic verification and other public operations. // - `serverURL`: The URL of the server the client will interact with. -func NewWithXPub(xPub, serverURL string) (*WalletClient, error) { +func NewWithXPub(xPub, serverURL string) *WalletClient { return newWalletClient( &WithXPub{XPubString: &xPub}, &WithHTTP{ServerURL: &serverURL}, @@ -47,7 +47,7 @@ func NewWithXPub(xPub, serverURL string) (*WalletClient, error) { // This configuration is typically used for administrative tasks such as managing sub-wallets or configuring system-wide settings. // - `adminKey`: The extended private key used for administrative operations. // - `serverURL`: The URL of the server the client will interact with. -func NewWithAdminKey(adminKey, serverURL string) (*WalletClient, error) { +func NewWithAdminKey(adminKey, serverURL string) *WalletClient { return newWalletClient( &WithAdminKey{AdminKeyString: &adminKey}, &WithHTTP{ServerURL: &serverURL}, @@ -59,7 +59,7 @@ func NewWithAdminKey(adminKey, serverURL string) (*WalletClient, error) { // This method is useful for scenarios where the client needs to authenticate using a less sensitive key than an xPriv. // - `accessKey`: The access key used for API authentication. // - `serverURL`: The URL of the server the client will interact with. -func NewWithAccessKey(accessKey, serverURL string) (*WalletClient, error) { +func NewWithAccessKey(accessKey, serverURL string) *WalletClient { return newWalletClient( &WithAccessKey{AccessKeyString: &accessKey}, &WithHTTP{ServerURL: &serverURL}, @@ -68,14 +68,14 @@ func NewWithAccessKey(accessKey, serverURL string) (*WalletClient, error) { } // newWalletClient creates a new WalletClient using the provided configuration options. -func newWalletClient(configurators ...WalletClientConfigurator) (*WalletClient, error) { +func newWalletClient(configurators ...WalletClientConfigurator) *WalletClient { client := &WalletClient{} for _, configurator := range configurators { configurator.Configure(client) } - return client, nil + return client } // processMetadata will process the metadata diff --git a/walletclient_test.go b/walletclient_test.go index b03bca4..a3440c2 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -23,10 +23,9 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPrivate success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client, err := NewWithXPriv(keys.XPriv(), server.URL) - require.NoError(t, err) - require.NotNil(t, client) - require.Equal(t, keys.XPriv(), *client.xPrivString) + client := NewWithXPriv(keys.XPriv(), server.URL) + require.NotNil(t, client.xPriv) + require.Equal(t, keys.XPriv(), client.xPriv.String()) require.NotNil(t, client.httpClient) require.True(t, *client.signRequest) @@ -38,18 +37,16 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPrivate fail", func(t *testing.T) { xPriv := "invalid_key" - client, err := NewWithXPriv(xPriv, "http://example.com") - require.Error(t, err) // Expect error due to invalid key - require.Nil(t, client) + client := NewWithXPriv(xPriv, "http://example.com") + require.Nil(t, client.xPriv) }) t.Run("NewWalletClientWithXPublic success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client, err := NewWithXPub(keys.XPub().String(), server.URL) - require.NoError(t, err) - require.NotNil(t, client) - require.Equal(t, keys.XPub().String(), *client.xPubString) + client := NewWithXPub(keys.XPub().String(), server.URL) + require.NotNil(t, client.xPub) + require.Equal(t, keys.XPub().String(), client.xPub.String()) require.NotNil(t, client.httpClient) require.False(t, *client.signRequest) @@ -61,16 +58,14 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPublic fail", func(t *testing.T) { xpub := "invalid_key" - client, err := NewWithXPub(xpub, server.URL) - require.Error(t, err) // Expect error due to invalid key - require.Nil(t, client) + client := NewWithXPub(xpub, server.URL) + require.Nil(t, client.xPub) }) t.Run("NewWalletClientWithAdminKey success", func(t *testing.T) { - client, err := NewWithAdminKey(fixtures.XPrivString, server.URL) - require.NoError(t, err) - require.NotNil(t, client) - require.Nil(t, client.xPrivString) + client := NewWithAdminKey(fixtures.XPrivString, server.URL) + require.NotNil(t, client.adminXPriv) + require.Nil(t, client.xPriv) require.Equal(t, fixtures.XPrivString, client.adminXPriv.String()) require.Equal(t, server.URL, *client.server) require.NotNil(t, client.httpClient) @@ -84,20 +79,16 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithAdminKey fail", func(t *testing.T) { adminKey := "invalid_key" - client, err := NewWithAdminKey(adminKey, server.URL) - require.Error(t, err) - require.Nil(t, client) + client := NewWithAdminKey(adminKey, server.URL) + require.Nil(t, client.adminXPriv) }) t.Run("NewWalletClientWithAccessKey success", func(t *testing.T) { // Attempt to create a new WalletClient with an access key accessKey := fixtures.AccessKeyString - client, err := NewWithAccessKey(accessKey, server.URL) + client := NewWithAccessKey(accessKey, server.URL) + require.NotNil(t, client.accessKey) - require.NoError(t, err) - require.NotNil(t, client) - - require.Equal(t, &accessKey, client.accessKeyString) require.Equal(t, &server.URL, client.server) require.True(t, *client.signRequest) require.NotNil(t, client.httpClient) @@ -110,9 +101,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithAccessKey fail", func(t *testing.T) { accessKey := "invalid_key" - client, err := NewWithAccessKey(accessKey, server.URL) - - require.Error(t, err) - require.Nil(t, client) + client := NewWithAccessKey(accessKey, server.URL) + require.Nil(t, client.accessKey) }) } diff --git a/xpubs_test.go b/xpubs_test.go index 9fc6e1c..e20719b 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -43,8 +43,8 @@ func TestXpub(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client, err := NewWithXPriv(keys.XPriv(), server.URL) - require.NoError(t, err) + client := NewWithXPriv(keys.XPriv(), server.URL) + require.NotNil(t, client.xPriv) t.Run("GetXPub", func(t *testing.T) { xpub, err := client.GetXPub(context.Background()) From bf02213a26d5953e5744cb346a705c3b50e8eadd Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 17:47:34 +0200 Subject: [PATCH 20/27] renaming options --- client_options.go | 38 +++++++++++++++++++------------------- walletclient.go | 24 ++++++++++++------------ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/client_options.go b/client_options.go index 7796575..6384923 100644 --- a/client_options.go +++ b/client_options.go @@ -14,24 +14,24 @@ type WalletClientConfigurator interface { Configure(c *WalletClient) } -// WithXPriv sets the xPrivString field of a WalletClient -type WithXPriv struct { +// XPriv sets the xPrivString field of a WalletClient +type XPriv struct { XPrivString *string } -func (w *WithXPriv) Configure(c *WalletClient) { +func (w *XPriv) Configure(c *WalletClient) { var err error if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*w.XPrivString); err != nil { c.xPriv = nil } } -// WithXPub sets the xPubString on the client -type WithXPub struct { +// XPub sets the xPubString on the client +type XPub struct { XPubString *string } -func (w *WithXPub) Configure(c *WalletClient) { +func (w *XPub) Configure(c *WalletClient) { var err error if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(*w.XPubString); err != nil { w.XPubString = nil @@ -39,24 +39,24 @@ func (w *WithXPub) Configure(c *WalletClient) { } -// WithAccessKey sets the accessKeyString on the client -type WithAccessKey struct { +// AccessKey sets the accessKeyString on the client +type AccessKey struct { AccessKeyString *string } -func (w *WithAccessKey) Configure(c *WalletClient) { +func (w *AccessKey) Configure(c *WalletClient) { var err error if c.accessKey, err = w.initializeAccessKey(); err != nil { c.accessKey = nil } } -// WithAdminKey sets the admin key for creating new xpubs -type WithAdminKey struct { +// AdminKey sets the admin key for creating new xpubs +type AdminKey struct { AdminKeyString *string } -func (w *WithAdminKey) Configure(c *WalletClient) { +func (w *AdminKey) Configure(c *WalletClient) { var err error c.adminXPriv, err = bitcoin.GenerateHDKeyFromString(*w.AdminKeyString) if err != nil { @@ -64,13 +64,13 @@ func (w *WithAdminKey) Configure(c *WalletClient) { } } -// WithHTTP sets the URL and HTTP client of a WalletClient -type WithHTTP struct { +// HTTP sets the URL and HTTP client of a WalletClient +type HTTP struct { ServerURL *string HTTPClient *http.Client } -func (w *WithHTTP) Configure(c *WalletClient) { +func (w *HTTP) Configure(c *WalletClient) { c.server = w.ServerURL c.httpClient = w.HTTPClient if w.HTTPClient != nil { @@ -80,17 +80,17 @@ func (w *WithHTTP) Configure(c *WalletClient) { } } -// WithSignRequest configures whether to sign HTTP requests -type WithSignRequest struct { +// SignRequest configures whether to sign HTTP requests +type SignRequest struct { Sign *bool } -func (w *WithSignRequest) Configure(c *WalletClient) { +func (w *SignRequest) Configure(c *WalletClient) { c.signRequest = w.Sign } // initializeAccessKey handles the specific initialization of the access key. -func (c *WithAccessKey) initializeAccessKey() (*bec.PrivateKey, error) { +func (c *AccessKey) initializeAccessKey() (*bec.PrivateKey, error) { var err error var privateKey *bec.PrivateKey var decodedWIF *wif.WIF diff --git a/walletclient.go b/walletclient.go index 0e73591..efd8c85 100644 --- a/walletclient.go +++ b/walletclient.go @@ -25,9 +25,9 @@ type WalletClient struct { // - `serverURL`: The URL of the server the client will interact with. func NewWithXPriv(xPriv, serverURL string) *WalletClient { return newWalletClient( - &WithXPriv{XPrivString: &xPriv}, - &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: Ptr(true)}, + &XPriv{XPrivString: &xPriv}, + &HTTP{ServerURL: &serverURL}, + &SignRequest{Sign: Ptr(true)}, ) } @@ -37,9 +37,9 @@ func NewWithXPriv(xPriv, serverURL string) *WalletClient { // - `serverURL`: The URL of the server the client will interact with. func NewWithXPub(xPub, serverURL string) *WalletClient { return newWalletClient( - &WithXPub{XPubString: &xPub}, - &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: Ptr(false)}, + &XPub{XPubString: &xPub}, + &HTTP{ServerURL: &serverURL}, + &SignRequest{Sign: Ptr(false)}, ) } @@ -49,9 +49,9 @@ func NewWithXPub(xPub, serverURL string) *WalletClient { // - `serverURL`: The URL of the server the client will interact with. func NewWithAdminKey(adminKey, serverURL string) *WalletClient { return newWalletClient( - &WithAdminKey{AdminKeyString: &adminKey}, - &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: Ptr(true)}, + &AdminKey{AdminKeyString: &adminKey}, + &HTTP{ServerURL: &serverURL}, + &SignRequest{Sign: Ptr(true)}, ) } @@ -61,9 +61,9 @@ func NewWithAdminKey(adminKey, serverURL string) *WalletClient { // - `serverURL`: The URL of the server the client will interact with. func NewWithAccessKey(accessKey, serverURL string) *WalletClient { return newWalletClient( - &WithAccessKey{AccessKeyString: &accessKey}, - &WithHTTP{ServerURL: &serverURL}, - &WithSignRequest{Sign: Ptr(true)}, + &AccessKey{AccessKeyString: &accessKey}, + &HTTP{ServerURL: &serverURL}, + &SignRequest{Sign: Ptr(true)}, ) } From 83f9b023d4468e3e5a3ef1ee90ca3bd009601884 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Wed, 15 May 2024 18:11:09 +0200 Subject: [PATCH 21/27] reomved pointers to sign and server --- client_options.go | 4 ++-- http.go | 32 ++++++++++++++++---------------- walletclient.go | 4 ++-- walletclient_test.go | 12 ++++++------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/client_options.go b/client_options.go index 6384923..e773ff9 100644 --- a/client_options.go +++ b/client_options.go @@ -71,7 +71,7 @@ type HTTP struct { } func (w *HTTP) Configure(c *WalletClient) { - c.server = w.ServerURL + c.server = *w.ServerURL c.httpClient = w.HTTPClient if w.HTTPClient != nil { c.httpClient = w.HTTPClient @@ -86,7 +86,7 @@ type SignRequest struct { } func (w *SignRequest) Configure(c *WalletClient) { - c.signRequest = w.Sign + c.signRequest = *w.Sign } // initializeAccessKey handles the specific initialization of the access key. diff --git a/http.go b/http.go index 0569d94..bbbb4d3 100644 --- a/http.go +++ b/http.go @@ -21,12 +21,12 @@ import ( // SetSignRequest turn the signing of the http request on or off func (wc *WalletClient) SetSignRequest(signRequest bool) { - wc.signRequest = &signRequest + wc.signRequest = signRequest } // IsSignRequest return whether to sign all requests func (wc *WalletClient) IsSignRequest() bool { - return *wc.signRequest + return wc.signRequest } // SetAdminKey set the admin key @@ -306,7 +306,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Con // GetTransaction will get a transaction by ID func (wc *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, ResponseError) { var transaction models.Transaction - if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.getPrivKey(), *wc.signRequest, &transaction); err != nil { + if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.getPrivKey(), wc.signRequest, &transaction); err != nil { return nil, err } @@ -326,7 +326,7 @@ func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[stri var transactions []*models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/search", jsonStr, wc.getPrivKey(), *wc.signRequest, &transactions, + ctx, http.MethodPost, "/transaction/search", jsonStr, wc.getPrivKey(), wc.signRequest, &transactions, ); err != nil { return nil, err } @@ -348,7 +348,7 @@ func (wc *WalletClient) GetTransactionsCount(ctx context.Context, conditions map var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/count", jsonStr, wc.getPrivKey(), *wc.signRequest, &count, + ctx, http.MethodPost, "/transaction/count", jsonStr, wc.getPrivKey(), wc.signRequest, &count, ); err != nil { return 0, err } @@ -424,7 +424,7 @@ func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/record", jsonStr, wc.getPrivKey(), *wc.signRequest, &transaction, + ctx, http.MethodPost, "/transaction/record", jsonStr, wc.getPrivKey(), wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -446,7 +446,7 @@ func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID stri var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/transaction", jsonStr, wc.getPrivKey(), *wc.signRequest, &transaction, + ctx, http.MethodPatch, "/transaction", jsonStr, wc.getPrivKey(), wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -497,7 +497,7 @@ func (wc *WalletClient) GetUtxos(ctx context.Context, conditions map[string]inte var utxos []*models.Utxo if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/search", jsonStr, wc.getPrivKey(), *wc.signRequest, &utxos, + ctx, http.MethodPost, "/utxo/search", jsonStr, wc.getPrivKey(), wc.signRequest, &utxos, ); err != nil { return nil, err } @@ -517,7 +517,7 @@ func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions map[string var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/count", jsonStr, wc.getPrivKey(), *wc.signRequest, &count, + ctx, http.MethodPost, "/utxo/count", jsonStr, wc.getPrivKey(), wc.signRequest, &count, ); err != nil { return 0, err } @@ -559,7 +559,7 @@ func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *models func (wc *WalletClient) doHTTPRequest(ctx context.Context, method string, path string, rawJSON []byte, xPriv *bip32.ExtendedKey, sign bool, responseJSON interface{}, ) ResponseError { - req, err := http.NewRequestWithContext(ctx, method, *wc.server+path, bytes.NewBuffer(rawJSON)) + req, err := http.NewRequestWithContext(ctx, method, wc.server+path, bytes.NewBuffer(rawJSON)) if err != nil { return WrapError(err) } @@ -625,7 +625,7 @@ func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []b // AcceptContact will accept the contact associated with the paymail func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.getPrivKey(), *wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.getPrivKey(), wc.signRequest, nil, ); err != nil { return err } @@ -636,7 +636,7 @@ func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) Respo // RejectContact will reject the contact associated with the paymail func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.getPrivKey(), *wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.getPrivKey(), wc.signRequest, nil, ); err != nil { return err } @@ -656,7 +656,7 @@ func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Cont } if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.getPrivKey(), *wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.getPrivKey(), wc.signRequest, nil, ); err != nil { return err } @@ -677,7 +677,7 @@ func (wc *WalletClient) GetContacts(ctx context.Context, conditions map[string]i var result []*models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/contact/search", jsonStr, wc.getPrivKey(), *wc.signRequest, &result, + ctx, http.MethodPost, "/contact/search", jsonStr, wc.getPrivKey(), wc.signRequest, &result, ); err != nil { return nil, err } @@ -707,7 +707,7 @@ func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fu var result models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.getPrivKey(), *wc.signRequest, &result, + ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.getPrivKey(), wc.signRequest, &result, ); err != nil { return nil, err } @@ -1004,7 +1004,7 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.getPrivKey(), *wc.signRequest, &transaction, + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.getPrivKey(), wc.signRequest, &transaction, ); err != nil { return nil, err } diff --git a/walletclient.go b/walletclient.go index efd8c85..cd9bc6d 100644 --- a/walletclient.go +++ b/walletclient.go @@ -10,8 +10,8 @@ import ( // WalletClient is the spv wallet Go client representation. type WalletClient struct { - signRequest *bool - server *string + signRequest bool + server string httpClient *http.Client accessKey *bec.PrivateKey adminXPriv *bip32.ExtendedKey diff --git a/walletclient_test.go b/walletclient_test.go index a3440c2..e58fb6c 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -27,7 +27,7 @@ func TestNewWalletClient(t *testing.T) { require.NotNil(t, client.xPriv) require.Equal(t, keys.XPriv(), client.xPriv.String()) require.NotNil(t, client.httpClient) - require.True(t, *client.signRequest) + require.True(t, client.signRequest) // Ensure HTTP calls can be made resp, err := client.httpClient.Get(server.URL) @@ -48,7 +48,7 @@ func TestNewWalletClient(t *testing.T) { require.NotNil(t, client.xPub) require.Equal(t, keys.XPub().String(), client.xPub.String()) require.NotNil(t, client.httpClient) - require.False(t, *client.signRequest) + require.False(t, client.signRequest) // Ensure HTTP calls can be made resp, err := client.httpClient.Get(server.URL) @@ -67,9 +67,9 @@ func TestNewWalletClient(t *testing.T) { require.NotNil(t, client.adminXPriv) require.Nil(t, client.xPriv) require.Equal(t, fixtures.XPrivString, client.adminXPriv.String()) - require.Equal(t, server.URL, *client.server) + require.Equal(t, server.URL, client.server) require.NotNil(t, client.httpClient) - require.True(t, *client.signRequest) + require.True(t, client.signRequest) // Ensure HTTP calls can be made resp, err := client.httpClient.Get(server.URL) @@ -89,8 +89,8 @@ func TestNewWalletClient(t *testing.T) { client := NewWithAccessKey(accessKey, server.URL) require.NotNil(t, client.accessKey) - require.Equal(t, &server.URL, client.server) - require.True(t, *client.signRequest) + require.Equal(t, server.URL, client.server) + require.True(t, client.signRequest) require.NotNil(t, client.httpClient) // Ensure HTTP calls can be made From 6596a55b214247084dd60ee213a264e2889633cf Mon Sep 17 00:00:00 2001 From: ac4ch Date: Thu, 16 May 2024 09:12:58 +0200 Subject: [PATCH 22/27] addressing review comments --- access_keys_test.go | 2 +- admin_contacts_test.go | 2 +- client_options.go | 70 +++++++++---------- contacts_test.go | 2 +- destinations_test.go | 2 +- examples/http/http.go | 2 +- .../http_with_access_key.go | 2 +- examples/new_paymail/new_paymail.go | 2 +- examples/register_xpub/register_xpub.go | 2 +- totp_test.go | 14 ++-- transactions_test.go | 2 +- walletclient.go | 44 ++++++------ walletclient_test.go | 18 ++--- xpubs_test.go | 2 +- 14 files changed, 81 insertions(+), 85 deletions(-) diff --git a/access_keys_test.go b/access_keys_test.go index 4e8ffbc..c8b3a44 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -33,7 +33,7 @@ func TestAccessKeys(t *testing.T) { })) defer server.Close() - client := NewWithAccessKey(fixtures.AccessKeyString, server.URL) + client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) require.NotNil(t, client.accessKey) t.Run("GetAccessKey", func(t *testing.T) { diff --git a/admin_contacts_test.go b/admin_contacts_test.go index fda9ee3..da7f59d 100644 --- a/admin_contacts_test.go +++ b/admin_contacts_test.go @@ -41,7 +41,7 @@ func TestAdminContactActions(t *testing.T) { })) defer server.Close() - client := NewWithAdminKey(fixtures.XPrivString, server.URL) + client := NewWithAdminKey(server.URL, fixtures.XPrivString) require.NotNil(t, client.adminXPriv) t.Run("AdminGetContacts", func(t *testing.T) { diff --git a/client_options.go b/client_options.go index e773ff9..128e200 100644 --- a/client_options.go +++ b/client_options.go @@ -9,69 +9,69 @@ import ( "github.com/pkg/errors" ) -// WalletClientConfigurator is the interface for configuring WalletClient -type WalletClientConfigurator interface { +// Configurator is the interface for configuring WalletClient +type Configurator interface { Configure(c *WalletClient) } -// XPriv sets the xPrivString field of a WalletClient -type XPriv struct { - XPrivString *string +// xPrivConf sets the xPrivString field of a WalletClient +type xPrivConf struct { + XPrivString string } -func (w *XPriv) Configure(c *WalletClient) { +func (w *xPrivConf) Configure(c *WalletClient) { var err error - if c.xPriv, err = bitcoin.GenerateHDKeyFromString(*w.XPrivString); err != nil { + if c.xPriv, err = bitcoin.GenerateHDKeyFromString(w.XPrivString); err != nil { c.xPriv = nil } } -// XPub sets the xPubString on the client -type XPub struct { - XPubString *string +// xPubConf sets the xPubString on the client +type xPubConf struct { + XPubString string } -func (w *XPub) Configure(c *WalletClient) { +func (w *xPubConf) Configure(c *WalletClient) { var err error - if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(*w.XPubString); err != nil { - w.XPubString = nil + if c.xPub, err = bitcoin.GetHDKeyFromExtendedPublicKey(w.XPubString); err != nil { + c.xPub = nil } } -// AccessKey sets the accessKeyString on the client -type AccessKey struct { - AccessKeyString *string +// accessKeyConf sets the accessKeyString on the client +type accessKeyConf struct { + AccessKeyString string } -func (w *AccessKey) Configure(c *WalletClient) { +func (w *accessKeyConf) Configure(c *WalletClient) { var err error if c.accessKey, err = w.initializeAccessKey(); err != nil { c.accessKey = nil } } -// AdminKey sets the admin key for creating new xpubs -type AdminKey struct { - AdminKeyString *string +// adminKeyConf sets the admin key for creating new xpubs +type adminKeyConf struct { + AdminKeyString string } -func (w *AdminKey) Configure(c *WalletClient) { +func (w *adminKeyConf) Configure(c *WalletClient) { var err error - c.adminXPriv, err = bitcoin.GenerateHDKeyFromString(*w.AdminKeyString) + c.adminXPriv, err = bitcoin.GenerateHDKeyFromString(w.AdminKeyString) if err != nil { c.adminXPriv = nil } } -// HTTP sets the URL and HTTP client of a WalletClient -type HTTP struct { - ServerURL *string +// httpConf sets the URL and httpConf client of a WalletClient +type httpConf struct { + ServerURL string HTTPClient *http.Client } -func (w *HTTP) Configure(c *WalletClient) { - c.server = *w.ServerURL +func (w *httpConf) Configure(c *WalletClient) { + c.server = w.ServerURL c.httpClient = w.HTTPClient if w.HTTPClient != nil { c.httpClient = w.HTTPClient @@ -80,23 +80,23 @@ func (w *HTTP) Configure(c *WalletClient) { } } -// SignRequest configures whether to sign HTTP requests -type SignRequest struct { - Sign *bool +// signRequest configures whether to sign HTTP requests +type signRequest struct { + Sign bool } -func (w *SignRequest) Configure(c *WalletClient) { - c.signRequest = *w.Sign +func (w *signRequest) Configure(c *WalletClient) { + c.signRequest = w.Sign } // initializeAccessKey handles the specific initialization of the access key. -func (c *AccessKey) initializeAccessKey() (*bec.PrivateKey, error) { +func (c *accessKeyConf) initializeAccessKey() (*bec.PrivateKey, error) { var err error var privateKey *bec.PrivateKey var decodedWIF *wif.WIF - if decodedWIF, err = wif.DecodeWIF(*c.AccessKeyString); err != nil { - if privateKey, err = bitcoin.PrivateKeyFromString(*c.AccessKeyString); err != nil { + if decodedWIF, err = wif.DecodeWIF(c.AccessKeyString); err != nil { + if privateKey, err = bitcoin.PrivateKeyFromString(c.AccessKeyString); err != nil { return nil, errors.Wrap(err, "failed to decode access key") } } else { diff --git a/contacts_test.go b/contacts_test.go index 71c2217..6eb4373 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -43,7 +43,7 @@ func TestContactActionsRouting(t *testing.T) { })) defer server.Close() - client := NewWithAccessKey(fixtures.AccessKeyString, server.URL) + client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) require.NotNil(t, client.accessKey) t.Run("RejectContact", func(t *testing.T) { diff --git a/destinations_test.go b/destinations_test.go index 240eedc..afea85b 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -41,7 +41,7 @@ func TestDestinations(t *testing.T) { })) defer server.Close() - client := NewWithAccessKey(fixtures.AccessKeyString, server.URL) + client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) require.NotNil(t, client.accessKey) t.Run("GetDestinationByID", func(t *testing.T) { diff --git a/examples/http/http.go b/examples/http/http.go index ae1abca..dc26341 100644 --- a/examples/http/http.go +++ b/examples/http/http.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") + wc := walletclient.NewWithXPriv("localhost:3001", keys.XPriv()) fmt.Println(wc.IsSignRequest()) } diff --git a/examples/http_with_access_key/http_with_access_key.go b/examples/http_with_access_key/http_with_access_key.go index 2468bef..44c48d4 100644 --- a/examples/http_with_access_key/http_with_access_key.go +++ b/examples/http_with_access_key/http_with_access_key.go @@ -9,6 +9,6 @@ func main() { exampleAccessKey := "some_generated_access_key" // Create a client - _ = walletclient.NewWithAccessKey(exampleAccessKey, "http://localhost:3003/v1") + _ = walletclient.NewWithAccessKey("http://localhost:3003/v1", exampleAccessKey) } diff --git a/examples/new_paymail/new_paymail.go b/examples/new_paymail/new_paymail.go index 72c49e9..ec6165b 100644 --- a/examples/new_paymail/new_paymail.go +++ b/examples/new_paymail/new_paymail.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3001") + wc := walletclient.NewWithXPriv("localhost:3001", keys.XPriv()) wc.AdminCreatePaymail(context.Background(), keys.XPub().String(), "foo@domain.com", "", "Foo") } diff --git a/examples/register_xpub/register_xpub.go b/examples/register_xpub/register_xpub.go index c5e42b6..f18f8a1 100644 --- a/examples/register_xpub/register_xpub.go +++ b/examples/register_xpub/register_xpub.go @@ -15,7 +15,7 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc := walletclient.NewWithXPriv(keys.XPriv(), "localhost:3003/v1") + wc := walletclient.NewWithXPriv("localhost:3003/v1", keys.XPriv()) ctx := context.Background() _ = wc.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) diff --git a/totp_test.go b/totp_test.go index de1055f..ba9eb41 100644 --- a/totp_test.go +++ b/totp_test.go @@ -17,7 +17,7 @@ import ( func TestGenerateTotpForContact(t *testing.T) { t.Run("success", func(t *testing.T) { // given - sut := NewWithXPriv(fixtures.XPrivString, "localhost:3001") + sut := NewWithXPriv("localhost:3001", fixtures.XPrivString) require.NotNil(t, sut.xPriv) contact := models.Contact{PubKey: fixtures.PubKey} @@ -31,7 +31,7 @@ func TestGenerateTotpForContact(t *testing.T) { t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { // given - sut := NewWithXPub(fixtures.XPubString, "localhost:3001") + sut := NewWithXPub("localhost:3001", fixtures.XPubString) require.NotNil(t, sut.xPub) // when _, err := sut.GenerateTotpForContact(nil, 30, 2) @@ -42,7 +42,7 @@ func TestGenerateTotpForContact(t *testing.T) { t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { // given - sut := NewWithXPriv(fixtures.XPrivString, "localhost:3001") + sut := NewWithXPriv("localhost:3001", fixtures.XPrivString) require.NotNil(t, sut.xPriv) contact := models.Contact{PubKey: "invalid-pk-format"} @@ -70,9 +70,9 @@ func TestValidateTotpForContact(t *testing.T) { require.NoError(t, err) // Set up the WalletClient for Alice and Bob - clientAlice := NewWithXPriv(aliceKeys.XPriv(), server.URL) + clientAlice := NewWithXPriv(server.URL, aliceKeys.XPriv()) require.NotNil(t, clientAlice.xPriv) - clientBob := NewWithXPriv(bobKeys.XPriv(), server.URL) + clientBob := NewWithXPriv(server.URL, bobKeys.XPriv()) require.NotNil(t, clientBob.xPriv) aliceContact := &models.Contact{ @@ -94,12 +94,12 @@ func TestValidateTotpForContact(t *testing.T) { }) t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { - client := NewWithXPub("invalid_xpub", server.URL) + client := NewWithXPub(server.URL, "invalid_xpub") require.Nil(t, client.xPub) }) t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { - sut := NewWithXPriv(fixtures.XPrivString, server.URL) + sut := NewWithXPriv(server.URL, fixtures.XPrivString) invalidContact := &models.Contact{ PubKey: "invalid_pub_key_format", diff --git a/transactions_test.go b/transactions_test.go index da86efa..5d14d9d 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -53,7 +53,7 @@ func TestTransactions(t *testing.T) { })) defer server.Close() - client := NewWithXPriv(fixtures.XPrivString, server.URL) + client := NewWithXPriv(server.URL, fixtures.XPrivString) require.NotNil(t, client.xPriv) t.Run("GetTransaction", func(t *testing.T) { diff --git a/walletclient.go b/walletclient.go index cd9bc6d..d3aec24 100644 --- a/walletclient.go +++ b/walletclient.go @@ -23,11 +23,11 @@ type WalletClient struct { // It configures the client with a specific server URL and a flag indicating whether requests should be signed. // - `xPriv`: The extended private key used for cryptographic operations. // - `serverURL`: The URL of the server the client will interact with. -func NewWithXPriv(xPriv, serverURL string) *WalletClient { - return newWalletClient( - &XPriv{XPrivString: &xPriv}, - &HTTP{ServerURL: &serverURL}, - &SignRequest{Sign: Ptr(true)}, +func NewWithXPriv(serverURL, xPriv string) *WalletClient { + return makeClient( + &xPrivConf{XPrivString: xPriv}, + &httpConf{ServerURL: serverURL}, + &signRequest{Sign: true}, ) } @@ -35,11 +35,11 @@ func NewWithXPriv(xPriv, serverURL string) *WalletClient { // This client is configured for operations that require a public key, such as verifying signatures or receiving transactions. // - `xPub`: The extended public key used for cryptographic verification and other public operations. // - `serverURL`: The URL of the server the client will interact with. -func NewWithXPub(xPub, serverURL string) *WalletClient { - return newWalletClient( - &XPub{XPubString: &xPub}, - &HTTP{ServerURL: &serverURL}, - &SignRequest{Sign: Ptr(false)}, +func NewWithXPub(serverURL, xPub string) *WalletClient { + return makeClient( + &xPubConf{XPubString: xPub}, + &httpConf{ServerURL: serverURL}, + &signRequest{Sign: false}, ) } @@ -47,11 +47,11 @@ func NewWithXPub(xPub, serverURL string) *WalletClient { // This configuration is typically used for administrative tasks such as managing sub-wallets or configuring system-wide settings. // - `adminKey`: The extended private key used for administrative operations. // - `serverURL`: The URL of the server the client will interact with. -func NewWithAdminKey(adminKey, serverURL string) *WalletClient { - return newWalletClient( - &AdminKey{AdminKeyString: &adminKey}, - &HTTP{ServerURL: &serverURL}, - &SignRequest{Sign: Ptr(true)}, +func NewWithAdminKey(serverURL, adminKey string) *WalletClient { + return makeClient( + &adminKeyConf{AdminKeyString: adminKey}, + &httpConf{ServerURL: serverURL}, + &signRequest{Sign: true}, ) } @@ -59,16 +59,16 @@ func NewWithAdminKey(adminKey, serverURL string) *WalletClient { // This method is useful for scenarios where the client needs to authenticate using a less sensitive key than an xPriv. // - `accessKey`: The access key used for API authentication. // - `serverURL`: The URL of the server the client will interact with. -func NewWithAccessKey(accessKey, serverURL string) *WalletClient { - return newWalletClient( - &AccessKey{AccessKeyString: &accessKey}, - &HTTP{ServerURL: &serverURL}, - &SignRequest{Sign: Ptr(true)}, +func NewWithAccessKey(serverURL, accessKey string) *WalletClient { + return makeClient( + &accessKeyConf{AccessKeyString: accessKey}, + &httpConf{ServerURL: serverURL}, + &signRequest{Sign: true}, ) } -// newWalletClient creates a new WalletClient using the provided configuration options. -func newWalletClient(configurators ...WalletClientConfigurator) *WalletClient { +// makeClient creates a new WalletClient using the provided configuration options. +func makeClient(configurators ...Configurator) *WalletClient { client := &WalletClient{} for _, configurator := range configurators { diff --git a/walletclient_test.go b/walletclient_test.go index e58fb6c..16f5391 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -23,7 +23,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPrivate success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client := NewWithXPriv(keys.XPriv(), server.URL) + client := NewWithXPriv(server.URL, keys.XPriv()) require.NotNil(t, client.xPriv) require.Equal(t, keys.XPriv(), client.xPriv.String()) require.NotNil(t, client.httpClient) @@ -44,7 +44,7 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPublic success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client := NewWithXPub(keys.XPub().String(), server.URL) + client := NewWithXPub(server.URL, keys.XPub().String()) require.NotNil(t, client.xPub) require.Equal(t, keys.XPub().String(), client.xPub.String()) require.NotNil(t, client.httpClient) @@ -57,13 +57,12 @@ func TestNewWalletClient(t *testing.T) { }) t.Run("NewWalletClientWithXPublic fail", func(t *testing.T) { - xpub := "invalid_key" - client := NewWithXPub(xpub, server.URL) + client := NewWithXPub(server.URL, "invalid_key") require.Nil(t, client.xPub) }) t.Run("NewWalletClientWithAdminKey success", func(t *testing.T) { - client := NewWithAdminKey(fixtures.XPrivString, server.URL) + client := NewWithAdminKey(server.URL, fixtures.XPrivString) require.NotNil(t, client.adminXPriv) require.Nil(t, client.xPriv) require.Equal(t, fixtures.XPrivString, client.adminXPriv.String()) @@ -78,15 +77,13 @@ func TestNewWalletClient(t *testing.T) { }) t.Run("NewWalletClientWithAdminKey fail", func(t *testing.T) { - adminKey := "invalid_key" - client := NewWithAdminKey(adminKey, server.URL) + client := NewWithAdminKey(server.URL, "invalid_key") require.Nil(t, client.adminXPriv) }) t.Run("NewWalletClientWithAccessKey success", func(t *testing.T) { // Attempt to create a new WalletClient with an access key - accessKey := fixtures.AccessKeyString - client := NewWithAccessKey(accessKey, server.URL) + client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) require.NotNil(t, client.accessKey) require.Equal(t, server.URL, client.server) @@ -100,8 +97,7 @@ func TestNewWalletClient(t *testing.T) { }) t.Run("NewWalletClientWithAccessKey fail", func(t *testing.T) { - accessKey := "invalid_key" - client := NewWithAccessKey(accessKey, server.URL) + client := NewWithAccessKey(server.URL, "invalid_key") require.Nil(t, client.accessKey) }) } diff --git a/xpubs_test.go b/xpubs_test.go index e20719b..ed87041 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -43,7 +43,7 @@ func TestXpub(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client := NewWithXPriv(keys.XPriv(), server.URL) + client := NewWithXPriv(server.URL, keys.XPriv()) require.NotNil(t, client.xPriv) t.Run("GetXPub", func(t *testing.T) { From df9907930049dc56c18f939bca80a0c2966790e4 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Thu, 16 May 2024 12:07:38 +0200 Subject: [PATCH 23/27] addressed reviwer comments --- access_keys_test.go | 7 +- admin_contacts_test.go | 13 ++- authentication.go | 3 +- client_options.go | 40 ++++++++- contacts_test.go | 13 ++- destinations_test.go | 16 ++-- examples/http/http.go | 2 +- .../http_with_access_key.go | 2 +- examples/new_paymail/new_paymail.go | 2 +- examples/register_xpub/register_xpub.go | 2 +- fixtures/fixtures.go | 19 +++-- http.go | 83 +++++++++---------- totp_test.go | 15 ++-- transactions_test.go | 11 ++- walletclient.go | 31 +++---- walletclient_test.go | 25 +++--- xpubs_test.go | 6 +- 17 files changed, 158 insertions(+), 132 deletions(-) diff --git a/access_keys_test.go b/access_keys_test.go index c8b3a44..347495f 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -7,17 +7,16 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) // TestAccessKeys will test the AccessKey methods func TestAccessKeys(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/access-key": + case "/v1/access-key": if r.Method == http.MethodGet { json.NewEncoder(w).Encode(fixtures.AccessKey) } else if r.Method == http.MethodPost { @@ -25,7 +24,7 @@ func TestAccessKeys(t *testing.T) { } else if r.Method == http.MethodDelete { json.NewEncoder(w).Encode(fixtures.AccessKey) } - case "/access-key/search": + case "/v1/access-key/search": json.NewEncoder(w).Encode([]*models.AccessKey{fixtures.AccessKey}) default: w.WriteHeader(http.StatusNotFound) diff --git a/admin_contacts_test.go b/admin_contacts_test.go index da7f59d..610d0fa 100644 --- a/admin_contacts_test.go +++ b/admin_contacts_test.go @@ -7,31 +7,30 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) // TestAdminContactActions testing Admin contacts methods func TestAdminContactActions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.URL.Path == "/admin/contact/search" && r.Method == http.MethodPost: + case r.URL.Path == "/v1/admin/contact/search" && r.Method == http.MethodPost: c := fixtures.Contact c.ID = "1" contacts := []*models.Contact{c} json.NewEncoder(w).Encode(contacts) - case r.URL.Path == "/admin/contact/1" && r.Method == http.MethodPatch: + case r.URL.Path == "/v1/admin/contact/1" && r.Method == http.MethodPatch: contact := fixtures.Contact json.NewEncoder(w).Encode(contact) - case r.URL.Path == "/admin/contact/1" && r.Method == http.MethodDelete: + case r.URL.Path == "/v1/admin/contact/1" && r.Method == http.MethodDelete: w.WriteHeader(http.StatusOK) - case r.URL.Path == "/admin/contact/accepted/1" && r.Method == http.MethodPatch: + case r.URL.Path == "/v1/admin/contact/accepted/1" && r.Method == http.MethodPatch: contact := fixtures.Contact contact.Status = "accepted" json.NewEncoder(w).Encode(contact) - case r.URL.Path == "/admin/contact/rejected/1" && r.Method == http.MethodPatch: + case r.URL.Path == "/v1/admin/contact/rejected/1" && r.Method == http.MethodPatch: contact := fixtures.Contact contact.Status = "rejected" json.NewEncoder(w).Encode(contact) diff --git a/authentication.go b/authentication.go index e6e52a6..55d2386 100644 --- a/authentication.go +++ b/authentication.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/bitcoin-sv/spv-wallet-go-client/utils" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoin-sv/spv-wallet/models/apierrors" "github.com/bitcoinschema/go-bitcoin/v2" @@ -14,8 +15,6 @@ import ( "github.com/libsv/go-bt/v2" "github.com/libsv/go-bt/v2/bscript" "github.com/libsv/go-bt/v2/sighash" - - "github.com/bitcoin-sv/spv-wallet-go-client/utils" ) // SetSignature will set the signature on the header for the request diff --git a/client_options.go b/client_options.go index 128e200..31f8c0d 100644 --- a/client_options.go +++ b/client_options.go @@ -1,7 +1,9 @@ package walletclient import ( + "fmt" "net/http" + "net/url" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" @@ -9,8 +11,8 @@ import ( "github.com/pkg/errors" ) -// Configurator is the interface for configuring WalletClient -type Configurator interface { +// configurator is the interface for configuring WalletClient +type configurator interface { Configure(c *WalletClient) } @@ -71,7 +73,17 @@ type httpConf struct { } func (w *httpConf) Configure(c *WalletClient) { - c.server = w.ServerURL + // Ensure the ServerURL ends with a clean base URL + baseURL, err := validateAndCleanURL(w.ServerURL) + if err != nil { + // Handle the error appropriately + fmt.Println("Invalid URL provided:", err) + return + } + + const basePath = "/v1" + c.server = fmt.Sprintf("%s%s", baseURL, basePath) + c.httpClient = w.HTTPClient if w.HTTPClient != nil { c.httpClient = w.HTTPClient @@ -105,3 +117,25 @@ func (c *accessKeyConf) initializeAccessKey() (*bec.PrivateKey, error) { return privateKey, nil } + +// validateAndCleanURL ensures that the provided URL is valid, and strips it down to just the base URL. +func validateAndCleanURL(rawURL string) (string, error) { + if rawURL == "" { + return "", fmt.Errorf("empty URL") + } + + // Parse the URL to validate it + parsedURL, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("parsing URL failed: %w", err) + } + + // Rebuild the URL with only the scheme and host (and port if included) + cleanedURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host) + + if parsedURL.Path == "" || parsedURL.Path == "/" { + return cleanedURL, nil + } + + return cleanedURL, nil +} diff --git a/contacts_test.go b/contacts_test.go index 6eb4373..1d10411 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -3,37 +3,34 @@ package walletclient import ( "context" "encoding/json" - "fmt" "net/http" "net/http/httptest" "strings" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) // TestContactActionsRouting will test routing func TestContactActionsRouting(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Println("=== test", r.URL.Path) w.Header().Set("Content-Type", "application/json") switch { - case strings.HasPrefix(r.URL.Path, "/contact/rejected/"): + case strings.HasPrefix(r.URL.Path, "/v1/contact/rejected/"): if r.Method == http.MethodPatch { json.NewEncoder(w).Encode(map[string]string{"result": "rejected"}) } - case r.URL.Path == "/contact/accepted/": + case r.URL.Path == "/v1/contact/accepted/": if r.Method == http.MethodPost { json.NewEncoder(w).Encode(map[string]string{"result": "accepted"}) } - case r.URL.Path == "/contact/search": + case r.URL.Path == "/v1/contact/search": if r.Method == http.MethodPost { json.NewEncoder(w).Encode([]*models.Contact{fixtures.Contact}) } - case strings.HasPrefix(r.URL.Path, "/contact/"): + case strings.HasPrefix(r.URL.Path, "/v1/contact/"): if r.Method == http.MethodPost || r.Method == http.MethodPut { json.NewEncoder(w).Encode(map[string]string{"result": "upserted"}) } diff --git a/destinations_test.go b/destinations_test.go index afea85b..1ec8596 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -7,10 +7,9 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) func TestDestinations(t *testing.T) { @@ -23,24 +22,23 @@ func TestDestinations(t *testing.T) { } switch { - case r.URL.Path == "/destination/address/"+fixtures.Destination.Address && r.Method == http.MethodGet: + case r.URL.Path == "/v1/v1/destination/address/"+fixtures.Destination.Address && r.Method == http.MethodGet: sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/destination/lockingScript/"+fixtures.Destination.LockingScript && r.Method == http.MethodGet: + case r.URL.Path == "/v1/destination/lockingScript/"+fixtures.Destination.LockingScript && r.Method == http.MethodGet: sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/destination/search" && r.Method == http.MethodPost: + case r.URL.Path == "/v1/destination/search" && r.Method == http.MethodPost: sendJSONResponse([]*models.Destination{fixtures.Destination}) - case r.URL.Path == "/destination" && r.Method == http.MethodGet: + case r.URL.Path == "/v1/destination" && r.Method == http.MethodGet: sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/destination" && r.Method == http.MethodPatch: + case r.URL.Path == "/v1/destination" && r.Method == http.MethodPatch: sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/destination" && r.Method == http.MethodPost: + case r.URL.Path == "/v1/destination" && r.Method == http.MethodPost: sendJSONResponse(fixtures.Destination) default: w.WriteHeader(http.StatusNotFound) } })) defer server.Close() - client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) require.NotNil(t, client.accessKey) diff --git a/examples/http/http.go b/examples/http/http.go index dc26341..231b8bd 100644 --- a/examples/http/http.go +++ b/examples/http/http.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc := walletclient.NewWithXPriv("localhost:3001", keys.XPriv()) + wc := walletclient.NewWithXPriv("https://localhost:3001", keys.XPriv()) fmt.Println(wc.IsSignRequest()) } diff --git a/examples/http_with_access_key/http_with_access_key.go b/examples/http_with_access_key/http_with_access_key.go index 44c48d4..39d9f1a 100644 --- a/examples/http_with_access_key/http_with_access_key.go +++ b/examples/http_with_access_key/http_with_access_key.go @@ -9,6 +9,6 @@ func main() { exampleAccessKey := "some_generated_access_key" // Create a client - _ = walletclient.NewWithAccessKey("http://localhost:3003/v1", exampleAccessKey) + _ = walletclient.NewWithAccessKey("http://localhost:3003", exampleAccessKey) } diff --git a/examples/new_paymail/new_paymail.go b/examples/new_paymail/new_paymail.go index ec6165b..6930ea6 100644 --- a/examples/new_paymail/new_paymail.go +++ b/examples/new_paymail/new_paymail.go @@ -12,6 +12,6 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc := walletclient.NewWithXPriv("localhost:3001", keys.XPriv()) + wc := walletclient.NewWithXPriv("https://localhost:3001", keys.XPriv()) wc.AdminCreatePaymail(context.Background(), keys.XPub().String(), "foo@domain.com", "", "Foo") } diff --git a/examples/register_xpub/register_xpub.go b/examples/register_xpub/register_xpub.go index f18f8a1..f35bb17 100644 --- a/examples/register_xpub/register_xpub.go +++ b/examples/register_xpub/register_xpub.go @@ -15,7 +15,7 @@ func main() { keys, _ := xpriv.Generate() // Create a client - wc := walletclient.NewWithXPriv("localhost:3003/v1", keys.XPriv()) + wc := walletclient.NewWithXPriv("localhost:3003", keys.XPriv()) ctx := context.Background() _ = wc.AdminNewXpub(ctx, keys.XPub().String(), &models.Metadata{"example_field": "example_data"}) diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index f0fb28f..43a5985 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -8,13 +8,20 @@ import ( ) var ( - RequestType = "http" - ServerURL = "https://example.com/" - XPubString = "xpub661MyMwAqRbcFrBJbKwBGCB7d3fr2SaAuXGM95BA62X41m6eW2ehRQGW4xLi9wkEXUGnQZYxVVj4PxXnyrLk7jdqvBAs1Qq9gf6ykMvjR7J" - XPrivString = "xprv9s21ZrQH143K3N6qVJQAu4EP51qMcyrKYJLkLgmYXgz58xmVxVLSsbx2DfJUtjcnXK8NdvkHMKfmmg5AJT2nqqRWUrjSHX29qEJwBgBPkJQ" + // RequestType http or https + RequestType = "http" + // ServerURL ex. https://localhost + ServerURL = "https://example.com/" + // XPubString public key + XPubString = "xpub661MyMwAqRbcFrBJbKwBGCB7d3fr2SaAuXGM95BA62X41m6eW2ehRQGW4xLi9wkEXUGnQZYxVVj4PxXnyrLk7jdqvBAs1Qq9gf6ykMvjR7J" + // XPrivString private key + XPrivString = "xprv9s21ZrQH143K3N6qVJQAu4EP51qMcyrKYJLkLgmYXgz58xmVxVLSsbx2DfJUtjcnXK8NdvkHMKfmmg5AJT2nqqRWUrjSHX29qEJwBgBPkJQ" + // AccessKeyString access key AccessKeyString = "7779d24ca6f8821f225042bf55e8f80aa41b08b879b72827f51e41e6523b9cd0" - PaymailAddress = "address@paymail.com" - PubKey = "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" + // PaymailAddress ex. "address@paymail.com" + PaymailAddress = "address@paymail.com" + // PubKey ex. "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" + PubKey = "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" ) func MarshallForTestHandler(object any) string { diff --git a/http.go b/http.go index bbbb4d3..8972a58 100644 --- a/http.go +++ b/http.go @@ -10,13 +10,12 @@ import ( "net/http" "strconv" + "github.com/bitcoin-sv/spv-wallet-go-client/utils" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoin-sv/spv-wallet/models/apierrors" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" - - "github.com/bitcoin-sv/spv-wallet-go-client/utils" ) // SetSignRequest turn the signing of the http request on or off @@ -38,7 +37,7 @@ func (wc *WalletClient) SetAdminKey(adminKey *bip32.ExtendedKey) { func (wc *WalletClient) GetXPub(ctx context.Context) (*models.Xpub, ResponseError) { var xPub models.Xpub if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/xpub", nil, wc.getPrivKey(), true, &xPub, + ctx, http.MethodGet, "/xpub", nil, wc.xPriv, true, &xPub, ); err != nil { return nil, err } @@ -57,7 +56,7 @@ func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models var xPub models.Xpub if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/xpub", jsonStr, wc.getPrivKey(), true, &xPub, + ctx, http.MethodPatch, "/xpub", jsonStr, wc.xPriv, true, &xPub, ); err != nil { return nil, err } @@ -69,7 +68,7 @@ func (wc *WalletClient) UpdateXPubMetadata(ctx context.Context, metadata *models func (wc *WalletClient) GetAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, wc.getPrivKey(), true, &accessKey, + ctx, http.MethodGet, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -87,7 +86,7 @@ func (wc *WalletClient) GetAccessKeys(ctx context.Context, metadataConditions *m } var accessKey []*models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key/search", jsonStr, wc.getPrivKey(), true, &accessKey, + ctx, http.MethodPost, "/access-key/search", jsonStr, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -107,7 +106,7 @@ func (wc *WalletClient) GetAccessKeysCount(ctx context.Context, conditions map[s var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key/count", jsonStr, wc.getPrivKey(), true, &count, + ctx, http.MethodPost, "/access-key/count", jsonStr, wc.xPriv, true, &count, ); err != nil { return 0, err } @@ -119,7 +118,7 @@ func (wc *WalletClient) GetAccessKeysCount(ctx context.Context, conditions map[s func (wc *WalletClient) RevokeAccessKey(ctx context.Context, id string) (*models.AccessKey, ResponseError) { var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, wc.getPrivKey(), true, &accessKey, + ctx, http.MethodDelete, "/access-key?"+FieldID+"="+id, nil, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -137,7 +136,7 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Me } var accessKey models.AccessKey if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/access-key", jsonStr, wc.getPrivKey(), true, &accessKey, + ctx, http.MethodPost, "/access-key", jsonStr, wc.xPriv, true, &accessKey, ); err != nil { return nil, err } @@ -149,7 +148,7 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Me func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, wc.getPrivKey(), true, &destination, + ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -161,7 +160,7 @@ func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*mod func (wc *WalletClient) GetDestinationByAddress(ctx context.Context, address string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, wc.getPrivKey(), true, &destination, + ctx, http.MethodGet, "/destination?"+FieldAddress+"="+address, nil, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -173,7 +172,7 @@ func (wc *WalletClient) GetDestinationByAddress(ctx context.Context, address str func (wc *WalletClient) GetDestinationByLockingScript(ctx context.Context, lockingScript string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, wc.getPrivKey(), true, &destination, + ctx, http.MethodGet, "/destination?"+FieldLockingScript+"="+lockingScript, nil, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -191,7 +190,7 @@ func (wc *WalletClient) GetDestinations(ctx context.Context, metadataConditions } var destinations []*models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination/search", jsonStr, wc.getPrivKey(), true, &destinations, + ctx, http.MethodPost, "/destination/search", jsonStr, wc.xPriv, true, &destinations, ); err != nil { return nil, err } @@ -211,7 +210,7 @@ func (wc *WalletClient) GetDestinationsCount(ctx context.Context, conditions map var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination/count", jsonStr, wc.getPrivKey(), true, &count, + ctx, http.MethodPost, "/destination/count", jsonStr, wc.xPriv, true, &count, ); err != nil { return 0, err } @@ -229,7 +228,7 @@ func (wc *WalletClient) NewDestination(ctx context.Context, metadata *models.Met } var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/destination", jsonStr, wc.getPrivKey(), true, &destination, + ctx, http.MethodPost, "/destination", jsonStr, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -251,7 +250,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByID(ctx context.Context, id st var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.getPrivKey(), true, &destination, + ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -273,7 +272,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByAddress(ctx context.Context, var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.getPrivKey(), true, &destination, + ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -295,7 +294,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Con var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/destination", jsonStr, wc.getPrivKey(), true, &destination, + ctx, http.MethodPatch, "/destination", jsonStr, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -306,7 +305,7 @@ func (wc *WalletClient) UpdateDestinationMetadataByLockingScript(ctx context.Con // GetTransaction will get a transaction by ID func (wc *WalletClient) GetTransaction(ctx context.Context, txID string) (*models.Transaction, ResponseError) { var transaction models.Transaction - if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.getPrivKey(), wc.signRequest, &transaction); err != nil { + if err := wc.doHTTPRequest(ctx, http.MethodGet, "/transaction?"+FieldID+"="+txID, nil, wc.xPriv, wc.signRequest, &transaction); err != nil { return nil, err } @@ -326,7 +325,7 @@ func (wc *WalletClient) GetTransactions(ctx context.Context, conditions map[stri var transactions []*models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/search", jsonStr, wc.getPrivKey(), wc.signRequest, &transactions, + ctx, http.MethodPost, "/transaction/search", jsonStr, wc.xPriv, wc.signRequest, &transactions, ); err != nil { return nil, err } @@ -348,7 +347,7 @@ func (wc *WalletClient) GetTransactionsCount(ctx context.Context, conditions map var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/count", jsonStr, wc.getPrivKey(), wc.signRequest, &count, + ctx, http.MethodPost, "/transaction/count", jsonStr, wc.xPriv, wc.signRequest, &count, ); err != nil { return 0, err } @@ -398,7 +397,7 @@ func (wc *WalletClient) createDraftTransaction(ctx context.Context, var draftTransaction *models.DraftTransaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction", jsonStr, wc.getPrivKey(), true, &draftTransaction, + ctx, http.MethodPost, "/transaction", jsonStr, wc.xPriv, true, &draftTransaction, ); err != nil { return nil, err } @@ -424,7 +423,7 @@ func (wc *WalletClient) RecordTransaction(ctx context.Context, hex, referenceID var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/transaction/record", jsonStr, wc.getPrivKey(), wc.signRequest, &transaction, + ctx, http.MethodPost, "/transaction/record", jsonStr, wc.xPriv, wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -446,7 +445,7 @@ func (wc *WalletClient) UpdateTransactionMetadata(ctx context.Context, txID stri var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/transaction", jsonStr, wc.getPrivKey(), wc.signRequest, &transaction, + ctx, http.MethodPatch, "/transaction", jsonStr, wc.xPriv, wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -476,7 +475,7 @@ func (wc *WalletClient) GetUtxo(ctx context.Context, txID string, outputIndex ui var utxo models.Utxo if err := wc.doHTTPRequest( - ctx, http.MethodGet, url, nil, wc.getPrivKey(), true, &utxo, + ctx, http.MethodGet, url, nil, wc.xPriv, true, &utxo, ); err != nil { return nil, err } @@ -497,7 +496,7 @@ func (wc *WalletClient) GetUtxos(ctx context.Context, conditions map[string]inte var utxos []*models.Utxo if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/search", jsonStr, wc.getPrivKey(), wc.signRequest, &utxos, + ctx, http.MethodPost, "/utxo/search", jsonStr, wc.xPriv, wc.signRequest, &utxos, ); err != nil { return nil, err } @@ -517,7 +516,7 @@ func (wc *WalletClient) GetUtxosCount(ctx context.Context, conditions map[string var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/utxo/count", jsonStr, wc.getPrivKey(), wc.signRequest, &count, + ctx, http.MethodPost, "/utxo/count", jsonStr, wc.xPriv, wc.signRequest, &count, ); err != nil { return 0, err } @@ -625,7 +624,7 @@ func (wc *WalletClient) authenticateWithAccessKey(req *http.Request, rawJSON []b // AcceptContact will accept the contact associated with the paymail func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.getPrivKey(), wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/accepted/"+paymail, nil, wc.xPriv, wc.signRequest, nil, ); err != nil { return err } @@ -636,7 +635,7 @@ func (wc *WalletClient) AcceptContact(ctx context.Context, paymail string) Respo // RejectContact will reject the contact associated with the paymail func (wc *WalletClient) RejectContact(ctx context.Context, paymail string) ResponseError { if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.getPrivKey(), wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/rejected/"+paymail, nil, wc.xPriv, wc.signRequest, nil, ); err != nil { return err } @@ -656,7 +655,7 @@ func (wc *WalletClient) ConfirmContact(ctx context.Context, contact *models.Cont } if err := wc.doHTTPRequest( - ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.getPrivKey(), wc.signRequest, nil, + ctx, http.MethodPatch, "/contact/confirmed/"+contact.Paymail, nil, wc.xPriv, wc.signRequest, nil, ); err != nil { return err } @@ -677,7 +676,7 @@ func (wc *WalletClient) GetContacts(ctx context.Context, conditions map[string]i var result []*models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/contact/search", jsonStr, wc.getPrivKey(), wc.signRequest, &result, + ctx, http.MethodPost, "/contact/search", jsonStr, wc.xPriv, wc.signRequest, &result, ); err != nil { return nil, err } @@ -707,7 +706,7 @@ func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fu var result models.Contact if err := wc.doHTTPRequest( - ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.getPrivKey(), wc.signRequest, &result, + ctx, http.MethodPut, "/contact/"+paymail, jsonStr, wc.xPriv, wc.signRequest, &result, ); err != nil { return nil, err } @@ -741,7 +740,7 @@ func (wc *WalletClient) AdminNewXpub(ctx context.Context, rawXPub string, metada func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError) { var status bool if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/status", nil, wc.getPrivKey(), true, &status, + ctx, http.MethodGet, "/admin/status", nil, wc.adminXPriv, true, &status, ); err != nil { return false, err } @@ -753,7 +752,7 @@ func (wc *WalletClient) AdminGetStatus(ctx context.Context) (bool, ResponseError func (wc *WalletClient) AdminGetStats(ctx context.Context) (*models.AdminStats, ResponseError) { var stats *models.AdminStats if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/stats", nil, wc.getPrivKey(), true, &stats, + ctx, http.MethodGet, "/admin/stats", nil, wc.adminXPriv, true, &stats, ); err != nil { return nil, err } @@ -829,7 +828,7 @@ func (wc *WalletClient) AdminGetPaymail(ctx context.Context, address string) (*m var model *models.PaymailAddress if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.getPrivKey(), true, &model, + ctx, http.MethodPost, "/admin/paymail/get", jsonStr, wc.adminXPriv, true, &model, ); err != nil { return nil, err } @@ -870,7 +869,7 @@ func (wc *WalletClient) AdminCreatePaymail(ctx context.Context, rawXPub string, var model *models.PaymailAddress if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.getPrivKey(), true, &model, + ctx, http.MethodPost, "/admin/paymail/create", jsonStr, wc.adminXPriv, true, &model, ); err != nil { return nil, err } @@ -888,7 +887,7 @@ func (wc *WalletClient) AdminDeletePaymail(ctx context.Context, address string) } if err := wc.doHTTPRequest( - ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.getPrivKey(), true, nil, + ctx, http.MethodDelete, "/admin/paymail/delete", jsonStr, wc.adminXPriv, true, nil, ); err != nil { return err } @@ -966,7 +965,7 @@ func (wc *WalletClient) adminGetModels(ctx context.Context, conditions map[strin } if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.getPrivKey(), true, &models, + ctx, http.MethodPost, path, jsonStr, wc.adminXPriv, true, &models, ); err != nil { return err } @@ -985,7 +984,7 @@ func (wc *WalletClient) adminCount(ctx context.Context, conditions map[string]in var count int64 if err := wc.doHTTPRequest( - ctx, http.MethodPost, path, jsonStr, wc.getPrivKey(), true, &count, + ctx, http.MethodPost, path, jsonStr, wc.adminXPriv, true, &count, ); err != nil { return 0, err } @@ -1004,7 +1003,7 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) var transaction models.Transaction if err := wc.doHTTPRequest( - ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.getPrivKey(), wc.signRequest, &transaction, + ctx, http.MethodPost, "/admin/transactions/record", jsonStr, wc.adminXPriv, wc.signRequest, &transaction, ); err != nil { return nil, err } @@ -1016,7 +1015,7 @@ func (wc *WalletClient) AdminRecordTransaction(ctx context.Context, hex string) func (wc *WalletClient) AdminGetSharedConfig(ctx context.Context) (*models.SharedConfig, ResponseError) { var model *models.SharedConfig if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/admin/shared-config", nil, wc.getPrivKey(), true, &model, + ctx, http.MethodGet, "/admin/shared-config", nil, wc.adminXPriv, true, &model, ); err != nil { return nil, err } @@ -1076,7 +1075,7 @@ func (wc *WalletClient) AdminRejectContact(ctx context.Context, id string) (*mod // FinalizeTransaction will finalize the transaction func (wc *WalletClient) FinalizeTransaction(draft *models.DraftTransaction) (string, ResponseError) { - res, err := GetSignedHex(draft, wc.getPrivKey()) + res, err := GetSignedHex(draft, wc.xPriv) if err != nil { return "", WrapError(err) } diff --git a/totp_test.go b/totp_test.go index ba9eb41..e6be2e4 100644 --- a/totp_test.go +++ b/totp_test.go @@ -2,16 +2,16 @@ package walletclient import ( "encoding/hex" + "fmt" "net/http" "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" "github.com/bitcoin-sv/spv-wallet/models" "github.com/libsv/go-bk/bip32" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" - "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" ) func TestGenerateTotpForContact(t *testing.T) { @@ -63,6 +63,7 @@ func TestValidateTotpForContact(t *testing.T) { })) defer server.Close() + serverURL := fmt.Sprintf("%s/v1", server.URL) t.Run("success", func(t *testing.T) { aliceKeys, err := xpriv.Generate() require.NoError(t, err) @@ -70,9 +71,9 @@ func TestValidateTotpForContact(t *testing.T) { require.NoError(t, err) // Set up the WalletClient for Alice and Bob - clientAlice := NewWithXPriv(server.URL, aliceKeys.XPriv()) + clientAlice := NewWithXPriv(serverURL, aliceKeys.XPriv()) require.NotNil(t, clientAlice.xPriv) - clientBob := NewWithXPriv(server.URL, bobKeys.XPriv()) + clientBob := NewWithXPriv(serverURL, bobKeys.XPriv()) require.NotNil(t, clientBob.xPriv) aliceContact := &models.Contact{ @@ -94,12 +95,12 @@ func TestValidateTotpForContact(t *testing.T) { }) t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { - client := NewWithXPub(server.URL, "invalid_xpub") + client := NewWithXPub(serverURL, "invalid_xpub") require.Nil(t, client.xPub) }) t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { - sut := NewWithXPriv(server.URL, fixtures.XPrivString) + sut := NewWithXPriv(serverURL, fixtures.XPrivString) invalidContact := &models.Contact{ PubKey: "invalid_pub_key_format", diff --git a/transactions_test.go b/transactions_test.go index 5d14d9d..0d11f93 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -7,16 +7,15 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) func TestTransactions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/transaction": + case "/v1/transaction": switch r.Method { case http.MethodGet: json.NewEncoder(w).Encode(fixtures.Transaction) @@ -35,11 +34,11 @@ func TestTransactions(t *testing.T) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } - case "/transaction/search": + case "/v1/transaction/search": json.NewEncoder(w).Encode([]*models.Transaction{fixtures.Transaction}) - case "/transaction/count": + case "/v1/transaction/count": json.NewEncoder(w).Encode(1) - case "/transaction/record": + case "/v1/transaction/record": if r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") response := fixtures.Transaction diff --git a/walletclient.go b/walletclient.go index d3aec24..7e989ac 100644 --- a/walletclient.go +++ b/walletclient.go @@ -19,10 +19,10 @@ type WalletClient struct { xPub *bip32.ExtendedKey } -// NewWalletClientWithXPrivate creates a new WalletClient instance using a private key (xPriv). +// NewWithXPrivate creates a new WalletClient instance using a private key (xPriv). // It configures the client with a specific server URL and a flag indicating whether requests should be signed. // - `xPriv`: The extended private key used for cryptographic operations. -// - `serverURL`: The URL of the server the client will interact with. +// - `serverURL`: The URL of the server the client will interact with. ex. https://hostname:3003 func NewWithXPriv(serverURL, xPriv string) *WalletClient { return makeClient( &xPrivConf{XPrivString: xPriv}, @@ -31,10 +31,10 @@ func NewWithXPriv(serverURL, xPriv string) *WalletClient { ) } -// NewWalletClientWithXPublic creates a new WalletClient instance using a public key (xPub). +// NewWithXPublic creates a new WalletClient instance using a public key (xPub). // This client is configured for operations that require a public key, such as verifying signatures or receiving transactions. // - `xPub`: The extended public key used for cryptographic verification and other public operations. -// - `serverURL`: The URL of the server the client will interact with. +// - `serverURL`: The URL of the server the client will interact with. ex. https://hostname:3003 func NewWithXPub(serverURL, xPub string) *WalletClient { return makeClient( &xPubConf{XPubString: xPub}, @@ -43,10 +43,10 @@ func NewWithXPub(serverURL, xPub string) *WalletClient { ) } -// NewWalletClientWithAdminKey creates a new WalletClient using an administrative key for advanced operations. +// NewWithAdminKey creates a new WalletClient using an administrative key for advanced operations. // This configuration is typically used for administrative tasks such as managing sub-wallets or configuring system-wide settings. // - `adminKey`: The extended private key used for administrative operations. -// - `serverURL`: The URL of the server the client will interact with. +// - `serverURL`: The URL of the server the client will interact with. ex. https://hostname:3003 func NewWithAdminKey(serverURL, adminKey string) *WalletClient { return makeClient( &adminKeyConf{AdminKeyString: adminKey}, @@ -55,10 +55,10 @@ func NewWithAdminKey(serverURL, adminKey string) *WalletClient { ) } -// NewWalletClientWithAccessKey creates a new WalletClient configured with an access key for API authentication. +// NewWithAccessKey creates a new WalletClient configured with an access key for API authentication. // This method is useful for scenarios where the client needs to authenticate using a less sensitive key than an xPriv. // - `accessKey`: The access key used for API authentication. -// - `serverURL`: The URL of the server the client will interact with. +// - `serverURL`: The URL of the server the client will interact with. ex. https://hostname:3003 func NewWithAccessKey(serverURL, accessKey string) *WalletClient { return makeClient( &accessKeyConf{AccessKeyString: accessKey}, @@ -68,7 +68,7 @@ func NewWithAccessKey(serverURL, accessKey string) *WalletClient { } // makeClient creates a new WalletClient using the provided configuration options. -func makeClient(configurators ...Configurator) *WalletClient { +func makeClient(configurators ...configurator) *WalletClient { client := &WalletClient{} for _, configurator := range configurators { @@ -93,14 +93,7 @@ func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString stri return setSignature(header, xPriv, bodyString) } -func Ptr[T any](obj T) *T { - return &obj -} - -// getPrivKey retrieves the client's private key. If the primary key is not set, -func (wc *WalletClient) getPrivKey() *bip32.ExtendedKey { - if wc.adminXPriv != nil { - return wc.adminXPriv - } - return wc.xPriv +func (wc *WalletClient) SetAdminKeyByString(adminKey string) { + keyConf := accessKeyConf{AccessKeyString: adminKey} + keyConf.Configure(wc) } diff --git a/walletclient_test.go b/walletclient_test.go index 16f5391..8d99e7c 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -1,6 +1,7 @@ package walletclient import ( + "fmt" "net/http" "net/http/httptest" "testing" @@ -19,18 +20,20 @@ func TestNewWalletClient(t *testing.T) { })) defer server.Close() + serverURL := fmt.Sprintf("%s/v1", server.URL) // Test creating a client with a valid xPriv t.Run("NewWalletClientWithXPrivate success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client := NewWithXPriv(server.URL, keys.XPriv()) + client := NewWithXPriv(serverURL, keys.XPriv()) require.NotNil(t, client.xPriv) require.Equal(t, keys.XPriv(), client.xPriv.String()) require.NotNil(t, client.httpClient) require.True(t, client.signRequest) // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(server.URL) + c := client.httpClient + resp, err := client.httpClient.Get(serverURL) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) @@ -44,20 +47,20 @@ func TestNewWalletClient(t *testing.T) { t.Run("NewWalletClientWithXPublic success", func(t *testing.T) { keys, err := xpriv.Generate() require.NoError(t, err) - client := NewWithXPub(server.URL, keys.XPub().String()) + client := NewWithXPub(serverURL, keys.XPub().String()) require.NotNil(t, client.xPub) require.Equal(t, keys.XPub().String(), client.xPub.String()) require.NotNil(t, client.httpClient) require.False(t, client.signRequest) // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(server.URL) + resp, err := client.httpClient.Get(serverURL) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("NewWalletClientWithXPublic fail", func(t *testing.T) { - client := NewWithXPub(server.URL, "invalid_key") + client := NewWithXPub(serverURL, "invalid_key") require.Nil(t, client.xPub) }) @@ -66,18 +69,18 @@ func TestNewWalletClient(t *testing.T) { require.NotNil(t, client.adminXPriv) require.Nil(t, client.xPriv) require.Equal(t, fixtures.XPrivString, client.adminXPriv.String()) - require.Equal(t, server.URL, client.server) + require.Equal(t, serverURL, client.server) require.NotNil(t, client.httpClient) require.True(t, client.signRequest) // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(server.URL) + resp, err := client.httpClient.Get(serverURL) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("NewWalletClientWithAdminKey fail", func(t *testing.T) { - client := NewWithAdminKey(server.URL, "invalid_key") + client := NewWithAdminKey(serverURL, "invalid_key") require.Nil(t, client.adminXPriv) }) @@ -86,18 +89,18 @@ func TestNewWalletClient(t *testing.T) { client := NewWithAccessKey(server.URL, fixtures.AccessKeyString) require.NotNil(t, client.accessKey) - require.Equal(t, server.URL, client.server) + require.Equal(t, serverURL, client.server) require.True(t, client.signRequest) require.NotNil(t, client.httpClient) // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(server.URL) + resp, err := client.httpClient.Get(serverURL) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("NewWalletClientWithAccessKey fail", func(t *testing.T) { - client := NewWithAccessKey(server.URL, "invalid_key") + client := NewWithAccessKey(serverURL, "invalid_key") require.Nil(t, client.accessKey) }) } diff --git a/xpubs_test.go b/xpubs_test.go index ed87041..2430662 100644 --- a/xpubs_test.go +++ b/xpubs_test.go @@ -7,10 +7,9 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" ) type xpub struct { @@ -25,7 +24,7 @@ func TestXpub(t *testing.T) { var response xpub // Check path and method to customize the response switch { - case r.URL.Path == "/xpub": + case r.URL.Path == "/v1/xpub": metadata := &models.Metadata{"key": "value"} if update { metadata = &models.Metadata{"updated": "info"} @@ -39,7 +38,6 @@ func TestXpub(t *testing.T) { w.Write(respBytes) })) defer server.Close() - keys, err := xpriv.Generate() require.NoError(t, err) From 06b0eeafcde1f2c45b61e45447ceb3e745566373 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Thu, 16 May 2024 12:12:56 +0200 Subject: [PATCH 24/27] reomved unused var --- walletclient_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/walletclient_test.go b/walletclient_test.go index 8d99e7c..508f3c0 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -32,7 +32,6 @@ func TestNewWalletClient(t *testing.T) { require.True(t, client.signRequest) // Ensure HTTP calls can be made - c := client.httpClient resp, err := client.httpClient.Get(serverURL) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) From a7959371ce4673bc81ad6e70cff9e5c06b4f09b2 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Fri, 17 May 2024 04:38:53 +0200 Subject: [PATCH 25/27] corrected comments and other code challanges --- access_keys_test.go | 1 + client_options.go | 6 ++--- destinations_test.go | 8 +++--- fixtures/fixtures.go | 9 +++++++ http.go | 3 ++- totp.go | 1 + walletclient.go | 5 ++-- walletclient_test.go | 63 +++++++++++++++++++++++++++++++++++++++----- xpriv/xpriv.go | 1 + 9 files changed, 82 insertions(+), 15 deletions(-) diff --git a/access_keys_test.go b/access_keys_test.go index 347495f..66b5be1 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -1,3 +1,4 @@ +// Package walletclient here we are testing walletclient public methods package walletclient import ( diff --git a/client_options.go b/client_options.go index 31f8c0d..a5d06fa 100644 --- a/client_options.go +++ b/client_options.go @@ -102,13 +102,13 @@ func (w *signRequest) Configure(c *WalletClient) { } // initializeAccessKey handles the specific initialization of the access key. -func (c *accessKeyConf) initializeAccessKey() (*bec.PrivateKey, error) { +func (w *accessKeyConf) initializeAccessKey() (*bec.PrivateKey, error) { var err error var privateKey *bec.PrivateKey var decodedWIF *wif.WIF - if decodedWIF, err = wif.DecodeWIF(c.AccessKeyString); err != nil { - if privateKey, err = bitcoin.PrivateKeyFromString(c.AccessKeyString); err != nil { + if decodedWIF, err = wif.DecodeWIF(w.AccessKeyString); err != nil { + if privateKey, err = bitcoin.PrivateKeyFromString(w.AccessKeyString); err != nil { return nil, errors.Wrap(err, "failed to decode access key") } } else { diff --git a/destinations_test.go b/destinations_test.go index 1ec8596..dbd9483 100644 --- a/destinations_test.go +++ b/destinations_test.go @@ -21,6 +21,8 @@ func TestDestinations(t *testing.T) { } } + const dest = "/v1/destination" + switch { case r.URL.Path == "/v1/v1/destination/address/"+fixtures.Destination.Address && r.Method == http.MethodGet: sendJSONResponse(fixtures.Destination) @@ -28,11 +30,11 @@ func TestDestinations(t *testing.T) { sendJSONResponse(fixtures.Destination) case r.URL.Path == "/v1/destination/search" && r.Method == http.MethodPost: sendJSONResponse([]*models.Destination{fixtures.Destination}) - case r.URL.Path == "/v1/destination" && r.Method == http.MethodGet: + case r.URL.Path == dest && r.Method == http.MethodGet: sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/v1/destination" && r.Method == http.MethodPatch: + case r.URL.Path == dest && r.Method == http.MethodPatch: sendJSONResponse(fixtures.Destination) - case r.URL.Path == "/v1/destination" && r.Method == http.MethodPost: + case r.URL.Path == dest && r.Method == http.MethodPost: sendJSONResponse(fixtures.Destination) default: w.WriteHeader(http.StatusNotFound) diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index 43a5985..fb7f464 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -1,3 +1,4 @@ +// Package fixtures contains fixtures for testing package fixtures import ( @@ -24,6 +25,7 @@ var ( PubKey = "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" ) +// MarshallForTestHandler its marshaling test handler func MarshallForTestHandler(object any) string { json, err := json.Marshal(object) if err != nil { @@ -34,8 +36,10 @@ func MarshallForTestHandler(object any) string { return string(json) } +// TestMetadata model for metadata var TestMetadata = &models.Metadata{"test-key": "test-value"} +// Xpub model for testing var Xpub = &models.Xpub{ Model: common.Model{Metadata: *TestMetadata}, ID: "cba0be1e753a7609e1a2f792d2e80ea6fce241be86f0690ec437377477809ccc", @@ -44,6 +48,7 @@ var Xpub = &models.Xpub{ NextExternalNum: 1, } +// AccessKey model for testing var AccessKey = &models.AccessKey{ Model: common.Model{Metadata: *TestMetadata}, ID: "access-key-id", @@ -51,6 +56,7 @@ var AccessKey = &models.AccessKey{ Key: AccessKeyString, } +// Destination model for testing var Destination = &models.Destination{ Model: common.Model{Metadata: *TestMetadata}, ID: "90d10acb85f37dd009238fe7ec61a1411725825c82099bd8432fcb47ad8326ce", @@ -63,6 +69,7 @@ var Destination = &models.Destination{ DraftID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", } +// Transaction model for testing var Transaction = &models.Transaction{ Model: common.Model{Metadata: *TestMetadata}, ID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", @@ -82,6 +89,7 @@ var Transaction = &models.Transaction{ TransactionDirection: "incoming", } +// DraftTx model for testing var DraftTx = &models.DraftTransaction{ Model: common.Model{Metadata: *TestMetadata}, ID: "3a0e1fdd9ac6046c0c82aa36b462e477a455880ceeb08d3aabb1bf031553d1df", @@ -198,6 +206,7 @@ var DraftTx = &models.DraftTransaction{ FinalTxID: "caae6e799210dfea7591e3d55455437eb7e1091bb01463ae1e7ddf9e29c75eda", } +// Contact model for testing var Contact = &models.Contact{ ID: "68af358bde7d8641621c7dd3de1a276c9a62cfa9e2d0740494519f1ba61e2f4a", FullName: "Test User", diff --git a/http.go b/http.go index 8972a58..7f12cfb 100644 --- a/http.go +++ b/http.go @@ -148,7 +148,7 @@ func (wc *WalletClient) CreateAccessKey(ctx context.Context, metadata *models.Me func (wc *WalletClient) GetDestinationByID(ctx context.Context, id string) (*models.Destination, ResponseError) { var destination models.Destination if err := wc.doHTTPRequest( - ctx, http.MethodGet, "/destination?"+FieldID+"="+id, nil, wc.xPriv, true, &destination, + ctx, http.MethodGet, fmt.Sprintf("/destination?%s=%s", FieldID, id), nil, wc.xPriv, true, &destination, ); err != nil { return nil, err } @@ -689,6 +689,7 @@ func (wc *WalletClient) UpsertContact(ctx context.Context, paymail, fullName str return wc.UpsertContactForPaymail(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 in their contacts. func (wc *WalletClient) UpsertContactForPaymail(ctx context.Context, paymail, fullName string, metadata *models.Metadata, requesterPaymail string) (*models.Contact, ResponseError) { payload := map[string]interface{}{ "fullName": fullName, diff --git a/totp.go b/totp.go index eac2cee..336f6d6 100644 --- a/totp.go +++ b/totp.go @@ -16,6 +16,7 @@ import ( "github.com/pquerna/otp/totp" ) +// ErrClientInitNoXpriv error per init client with first xpriv key var ErrClientInitNoXpriv = errors.New("init client with xPriv first") const ( diff --git a/walletclient.go b/walletclient.go index 7e989ac..b676f6e 100644 --- a/walletclient.go +++ b/walletclient.go @@ -19,7 +19,7 @@ type WalletClient struct { xPub *bip32.ExtendedKey } -// NewWithXPrivate creates a new WalletClient instance using a private key (xPriv). +// NewWithXPriv creates a new WalletClient instance using a private key (xPriv). // It configures the client with a specific server URL and a flag indicating whether requests should be signed. // - `xPriv`: The extended private key used for cryptographic operations. // - `serverURL`: The URL of the server the client will interact with. ex. https://hostname:3003 @@ -31,7 +31,7 @@ func NewWithXPriv(serverURL, xPriv string) *WalletClient { ) } -// NewWithXPublic creates a new WalletClient instance using a public key (xPub). +// NewWithXPub creates a new WalletClient instance using a public key (xPub). // This client is configured for operations that require a public key, such as verifying signatures or receiving transactions. // - `xPub`: The extended public key used for cryptographic verification and other public operations. // - `serverURL`: The URL of the server the client will interact with. ex. https://hostname:3003 @@ -93,6 +93,7 @@ func addSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString stri return setSignature(header, xPriv, bodyString) } +// SetAdminKeyByString will set aminXPriv key func (wc *WalletClient) SetAdminKeyByString(adminKey string) { keyConf := accessKeyConf{AccessKeyString: adminKey} keyConf.Configure(wc) diff --git a/walletclient_test.go b/walletclient_test.go index 508f3c0..0c7fc07 100644 --- a/walletclient_test.go +++ b/walletclient_test.go @@ -1,15 +1,16 @@ package walletclient import ( + "context" "fmt" "net/http" "net/http/httptest" "testing" - - "github.com/stretchr/testify/require" + "time" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet-go-client/xpriv" + "github.com/stretchr/testify/require" ) func TestNewWalletClient(t *testing.T) { @@ -31,8 +32,21 @@ func TestNewWalletClient(t *testing.T) { require.NotNil(t, client.httpClient) require.True(t, client.signRequest) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", serverURL, nil) + if err != nil { + t.Fatalf("Failed to create HTTP request: %v", err) + } + // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(serverURL) + resp, err := client.httpClient.Do(req) + if err != nil { + t.Fatalf("Failed to make HTTP request: %v", err) + } + defer resp.Body.Close() + require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) @@ -51,9 +65,20 @@ func TestNewWalletClient(t *testing.T) { require.Equal(t, keys.XPub().String(), client.xPub.String()) require.NotNil(t, client.httpClient) require.False(t, client.signRequest) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", serverURL, nil) + if err != nil { + t.Fatalf("Failed to create HTTP request: %v", err) + } // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(serverURL) + resp, err := client.httpClient.Do(req) + if err != nil { + t.Fatalf("Failed to make HTTP request: %v", err) + } + defer resp.Body.Close() require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) @@ -72,8 +97,21 @@ func TestNewWalletClient(t *testing.T) { require.NotNil(t, client.httpClient) require.True(t, client.signRequest) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", serverURL, nil) + if err != nil { + t.Fatalf("Failed to create HTTP request: %v", err) + } + // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(serverURL) + resp, err := client.httpClient.Do(req) + if err != nil { + t.Fatalf("Failed to make HTTP request: %v", err) + } + defer resp.Body.Close() + require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) @@ -92,8 +130,21 @@ func TestNewWalletClient(t *testing.T) { require.True(t, client.signRequest) require.NotNil(t, client.httpClient) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", serverURL, nil) + if err != nil { + t.Fatalf("Failed to create HTTP request: %v", err) + } + // Ensure HTTP calls can be made - resp, err := client.httpClient.Get(serverURL) + resp, err := client.httpClient.Do(req) + if err != nil { + t.Fatalf("Failed to make HTTP request: %v", err) + } + defer resp.Body.Close() + require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) }) diff --git a/xpriv/xpriv.go b/xpriv/xpriv.go index a73b001..549f591 100644 --- a/xpriv/xpriv.go +++ b/xpriv/xpriv.go @@ -1,3 +1,4 @@ +// Package xpriv manges keys package xpriv import ( From d2471de36b3546cfd5c9d0ef9233683eb88971be Mon Sep 17 00:00:00 2001 From: ac4ch Date: Fri, 17 May 2024 08:33:48 +0200 Subject: [PATCH 26/27] addressing review comments per unit tests --- access_keys_test.go | 10 ++++----- client_options_test.go | 33 ++++++++++++++++++++++++++++ transactions_test.go | 50 ++++++++++++++++++++++++------------------ 3 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 client_options_test.go diff --git a/access_keys_test.go b/access_keys_test.go index 66b5be1..4bdfaa7 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -8,9 +8,10 @@ import ( "net/http/httptest" "testing" - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) // TestAccessKeys will test the AccessKey methods @@ -18,11 +19,8 @@ func TestAccessKeys(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/access-key": - if r.Method == http.MethodGet { - json.NewEncoder(w).Encode(fixtures.AccessKey) - } else if r.Method == http.MethodPost { - json.NewEncoder(w).Encode(fixtures.AccessKey) - } else if r.Method == http.MethodDelete { + switch r.Method { + case http.MethodGet, http.MethodPost, http.MethodDelete: json.NewEncoder(w).Encode(fixtures.AccessKey) } case "/v1/access-key/search": diff --git a/client_options_test.go b/client_options_test.go new file mode 100644 index 0000000..5627afa --- /dev/null +++ b/client_options_test.go @@ -0,0 +1,33 @@ +package walletclient + +import "testing" + +func TestValidateAndCleanURL(t *testing.T) { + tests := []struct { + name string + rawURL string + expected string + wantErr bool + }{ + {"Empty URL", "", "", true}, + {"Valid URL with path", "http://example.com/path", "http://example.com", false}, + {"Valid URL without path", "http://example.com", "http://example.com", false}, + {"Valid URL with port", "http://example.com:8080", "http://example.com:8080", false}, + {"Invalid URL", "http://%41:8080/", "", true}, + {"HTTPS URL", "https://example.com", "https://example.com", false}, + {"HTTPS URL with path", "https://example.com/path", "https://example.com", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateAndCleanURL(tt.rawURL) + if (err != nil) != tt.wantErr { + t.Errorf("validateAndCleanURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.expected { + t.Errorf("validateAndCleanURL() = %v, expected %v", got, tt.expected) + } + }) + } +} diff --git a/transactions_test.go b/transactions_test.go index 0d11f93..481d0e2 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -7,33 +7,17 @@ import ( "net/http/httptest" "testing" - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) func TestTransactions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/transaction": - switch r.Method { - case http.MethodGet: - json.NewEncoder(w).Encode(fixtures.Transaction) - case http.MethodPost: - json.NewEncoder(w).Encode(fixtures.Transaction) - case http.MethodPatch: - var input map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "bad request"}) - return - } - response := fixtures.Transaction - response.Metadata = input["metadata"].(map[string]interface{}) - response.ID = input["id"].(string) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - } + handleTransaction(w, r) case "/v1/transaction/search": json.NewEncoder(w).Encode([]*models.Transaction{fixtures.Transaction}) case "/v1/transaction/count": @@ -41,8 +25,7 @@ func TestTransactions(t *testing.T) { case "/v1/transaction/record": if r.Method == http.MethodPost { w.Header().Set("Content-Type", "application/json") - response := fixtures.Transaction - json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(fixtures.Transaction) } else { w.WriteHeader(http.StatusMethodNotAllowed) } @@ -100,3 +83,28 @@ func TestTransactions(t *testing.T) { require.Equal(t, fixtures.Transaction, tx) }) } + +func handleTransaction(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet, http.MethodPost: + json.NewEncoder(w).Encode(fixtures.Transaction) + case http.MethodPatch: + var input map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "bad request"}) + return + } + response := fixtures.Transaction + if metadata, ok := input["metadata"].(map[string]interface{}); ok { + response.Metadata = metadata + } + if id, ok := input["id"].(string); ok { + response.ID = id + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} From 7f1c206f3d155ff20f218d01257ecf1a88db04b0 Mon Sep 17 00:00:00 2001 From: ac4ch Date: Fri, 17 May 2024 08:38:06 +0200 Subject: [PATCH 27/27] addressing lint errors --- access_keys_test.go | 3 +-- transactions_test.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/access_keys_test.go b/access_keys_test.go index 4bdfaa7..9df5437 100644 --- a/access_keys_test.go +++ b/access_keys_test.go @@ -8,10 +8,9 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) // TestAccessKeys will test the AccessKey methods diff --git a/transactions_test.go b/transactions_test.go index 481d0e2..77174cb 100644 --- a/transactions_test.go +++ b/transactions_test.go @@ -7,10 +7,9 @@ import ( "net/http/httptest" "testing" + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/require" - - "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" ) func TestTransactions(t *testing.T) { @@ -87,12 +86,16 @@ func TestTransactions(t *testing.T) { func handleTransaction(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodPost: - json.NewEncoder(w).Encode(fixtures.Transaction) + if err := json.NewEncoder(w).Encode(fixtures.Transaction); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } case http.MethodPatch: var input map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&input); err != nil { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "bad request"}) + if err := json.NewEncoder(w).Encode(map[string]string{"error": "bad request"}); err != nil { + http.Error(w, "Failed to encode error response", http.StatusInternalServerError) + } return } response := fixtures.Transaction @@ -103,7 +106,9 @@ func handleTransaction(w http.ResponseWriter, r *http.Request) { response.ID = id } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } default: w.WriteHeader(http.StatusMethodNotAllowed) }