From 0785517d678050675b0538c7460ba2ce515c43b5 Mon Sep 17 00:00:00 2001 From: mgosek-4chain Date: Thu, 9 Jan 2025 15:27:47 +0100 Subject: [PATCH] refactor(SPV-1306): old go version client replacement. --- domain/transactions/transactions_service.go | 4 +- domain/users/interfaces.go | 6 +- go.mod | 7 +- go.sum | 19 +- notification/notification.go | 20 +- tests/mocks/users_interfaces_mq.go | 7 +- .../transactions/transactions_service_test.go | 1 + .../endpoints/api/transactions/endpoints.go | 1 + transports/spvwallet/admin_client.go | 65 ++-- transports/spvwallet/client.go | 272 --------------- .../spvwallet/client_factory_adapter.go | 40 +-- transports/spvwallet/methods.go | 10 +- transports/spvwallet/user_client.go | 310 ++++++++++++++++++ 13 files changed, 406 insertions(+), 356 deletions(-) delete mode 100644 transports/spvwallet/client.go create mode 100644 transports/spvwallet/user_client.go diff --git a/domain/transactions/transactions_service.go b/domain/transactions/transactions_service.go index 08a5d21..3875121 100644 --- a/domain/transactions/transactions_service.go +++ b/domain/transactions/transactions_service.go @@ -5,7 +5,7 @@ import ( "time" "github.com/avast/retry-go/v4" - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" "github.com/bitcoin-sv/spv-wallet-web-backend/domain/users" "github.com/bitcoin-sv/spv-wallet-web-backend/notification" "github.com/bitcoin-sv/spv-wallet-web-backend/spverrors" @@ -38,7 +38,7 @@ func (s *TransactionService) CreateTransaction(userPaymail, xpriv, recipient str return spverrors.ErrCreateTransaction.Wrap(err) } - var recipients = []*walletclient.Recipients{ + var recipients = []*commands.Recipients{ { Satoshis: satoshis, To: recipient, diff --git a/domain/users/interfaces.go b/domain/users/interfaces.go index 98e9608..44b3d04 100644 --- a/domain/users/interfaces.go +++ b/domain/users/interfaces.go @@ -4,7 +4,7 @@ import ( "context" "time" - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/libsv/go-bk/bip32" @@ -65,11 +65,11 @@ type ( // XPub Key methods GetXPub() (PubKey, error) // Transaction methods - SendToRecipients(recipients []*walletclient.Recipients, senderPaymail string) (Transaction, error) + SendToRecipients(recipients []*commands.Recipients, senderPaymail string) (Transaction, error) GetTransactions(queryParam *filter.QueryParams, userPaymail string) ([]Transaction, error) GetTransaction(transactionID, userPaymail string) (FullTransaction, error) GetTransactionsCount() (int64, error) - CreateAndFinalizeTransaction(recipients []*walletclient.Recipients, metadata map[string]any) (DraftTransaction, error) + CreateAndFinalizeTransaction(recipients []*commands.Recipients, metadata map[string]any) (DraftTransaction, error) RecordTransaction(hex, draftTxID string, metadata map[string]any) (*models.Transaction, error) // Contacts methods UpsertContact(ctx context.Context, paymail, fullName, requesterPaymail string, metadata map[string]any) (*models.Contact, error) diff --git a/go.mod b/go.mod index b92f816..ed0bc0a 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.22.6 require ( github.com/avast/retry-go/v4 v4.6.0 - github.com/bitcoin-sv/spv-wallet-go-client v1.0.0-beta.16 - github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.34 + github.com/bitcoin-sv/spv-wallet-go-client v1.0.0-beta.21 + github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.39 github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/centrifugal/centrifuge v0.33.4 github.com/gin-contrib/sessions v1.0.1 @@ -28,7 +28,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bitcoin-sv/go-sdk v1.1.14 // indirect + github.com/bitcoin-sv/go-sdk v1.1.16 // indirect github.com/boombuler/barcode v1.0.2 // indirect github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect @@ -48,6 +48,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/go-resty/resty/v2 v2.15.3 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect diff --git a/go.sum b/go.sum index 5fde032..e387390 100644 --- a/go.sum +++ b/go.sum @@ -12,12 +12,12 @@ github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinR github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitcoin-sv/go-sdk v1.1.14 h1:65PrWc8H5in138fplswpxpXuV3HwQQgyXDBegioyVbY= -github.com/bitcoin-sv/go-sdk v1.1.14/go.mod h1:khREs6RkJuYkxFD3ZB9I+zcGjfxoHeACg9Zr3f7Fn7s= -github.com/bitcoin-sv/spv-wallet-go-client v1.0.0-beta.16 h1:PGlo27UEdJKq4vup9pyE6BtZ2YWS2En3SJhjBu0EiEs= -github.com/bitcoin-sv/spv-wallet-go-client v1.0.0-beta.16/go.mod h1:E0G3YfQj6KDKs6CoMLbSxW2fMIqn5bMeOSi5G4BL4a0= -github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.34 h1:9MMqlZSyCP+RFt0FNnwcbJEgUsRKy5w9Ga7FXHs/XXo= -github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.34/go.mod h1:PEJdH9ZWKOiKHyOZkzYsRbKuZjzlRaEJy3GsM75Icdo= +github.com/bitcoin-sv/go-sdk v1.1.16 h1:n2X0RiENFGD/1fQ/1y6osbostRB7I/xq9I7tcIKcCPY= +github.com/bitcoin-sv/go-sdk v1.1.16/go.mod h1:3CsNdEDBwB+SIv6UBcJPC9bTvPqxQvg3GULt7wsuL58= +github.com/bitcoin-sv/spv-wallet-go-client v1.0.0-beta.21 h1:N6JbmVjoifPJwKu5DedS8xEKwHYIRv9AewD7eefxwyg= +github.com/bitcoin-sv/spv-wallet-go-client v1.0.0-beta.21/go.mod h1:WaO6ESKa4RX9QEpy1/6v3jcTcznwPx+MYlADg15c1dY= +github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.39 h1:qo74o72mcdj7AYJoCq7RG3enHJiqtbkFEY9uXvEEG2M= +github.com/bitcoin-sv/spv-wallet/models v1.0.0-beta.39/go.mod h1:UdY5AGsO9IomUEYSPilcSY+3BTQRJswdfZNveLt6LZQ= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -92,6 +92,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= +github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -121,6 +123,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -226,6 +230,7 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -319,6 +324,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/notification/notification.go b/notification/notification.go index 4079c8f..f567264 100644 --- a/notification/notification.go +++ b/notification/notification.go @@ -6,6 +6,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-web-backend/transports/spvwallet" "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/response" ) // BaseEvent represents base of notification. @@ -34,7 +35,24 @@ type Transaction struct { // PrepareTransactionEvent prepares event in NewTransactionEvent struct. func PrepareTransactionEvent(tx *models.Transaction) TransactionEvent { - sender, receiver := spvwallet.GetPaymailsFromMetadata(tx, "unknown") + sender, receiver := spvwallet.GetPaymailsFromMetadata(&response.Transaction{ + Model: response.Model(tx.Model), + ID: tx.ID, + Hex: tx.Hex, + XpubInIDs: tx.XpubInIDs, + XpubOutIDs: tx.XpubOutIDs, + BlockHash: tx.BlockHash, + BlockHeight: tx.BlockHeight, + Fee: tx.Fee, + NumberOfInputs: tx.NumberOfInputs, + NumberOfOutputs: tx.NumberOfOutputs, + DraftID: tx.DraftID, + TotalValue: tx.TotalValue, + OutputValue: tx.OutputValue, + Outputs: tx.Outputs, + Status: tx.Status, + TransactionDirection: tx.TransactionDirection, + }, "unknown") status := "unconfirmed" if tx.BlockHeight > 0 { status = "confirmed" diff --git a/tests/mocks/users_interfaces_mq.go b/tests/mocks/users_interfaces_mq.go index 5a8f74d..53d134d 100644 --- a/tests/mocks/users_interfaces_mq.go +++ b/tests/mocks/users_interfaces_mq.go @@ -8,8 +8,7 @@ import ( context "context" reflect "reflect" time "time" - - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" users "github.com/bitcoin-sv/spv-wallet-web-backend/domain/users" models "github.com/bitcoin-sv/spv-wallet/models" filter "github.com/bitcoin-sv/spv-wallet/models/filter" @@ -563,7 +562,7 @@ func (mr *MockUserWalletClientMockRecorder) CreateAccessKey() *gomock.Call { } // CreateAndFinalizeTransaction mocks base method. -func (m *MockUserWalletClient) CreateAndFinalizeTransaction(recipients []*walletclient.Recipients, metadata map[string]any) (users.DraftTransaction, error) { +func (m *MockUserWalletClient) CreateAndFinalizeTransaction(recipients []*commands.Recipients, metadata map[string]any) (users.DraftTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAndFinalizeTransaction", recipients, metadata) ret0, _ := ret[0].(users.DraftTransaction) @@ -727,7 +726,7 @@ func (mr *MockUserWalletClientMockRecorder) RevokeAccessKey(accessKeyID interfac } // SendToRecipients mocks base method. -func (m *MockUserWalletClient) SendToRecipients(recipients []*walletclient.Recipients, senderPaymail string) (users.Transaction, error) { +func (m *MockUserWalletClient) SendToRecipients(recipients []*commands.Recipients, senderPaymail string) (users.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendToRecipients", recipients, senderPaymail) ret0, _ := ret[0].(users.Transaction) diff --git a/tests/unit/domain/transactions/transactions_service_test.go b/tests/unit/domain/transactions/transactions_service_test.go index 4a52557..3ba2465 100644 --- a/tests/unit/domain/transactions/transactions_service_test.go +++ b/tests/unit/domain/transactions/transactions_service_test.go @@ -12,6 +12,7 @@ import ( mock "github.com/bitcoin-sv/spv-wallet-web-backend/tests/mocks" "github.com/bitcoin-sv/spv-wallet-web-backend/tests/utils" "github.com/bitcoin-sv/spv-wallet-web-backend/transports/spvwallet" + "github.com/brianvoe/gofakeit/v6" "github.com/golang/mock/gomock" "github.com/rs/zerolog" diff --git a/transports/http/endpoints/api/transactions/endpoints.go b/transports/http/endpoints/api/transactions/endpoints.go index 7f09443..64f5a0b 100644 --- a/transports/http/endpoints/api/transactions/endpoints.go +++ b/transports/http/endpoints/api/transactions/endpoints.go @@ -12,6 +12,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-web-backend/transports/http/auth" router "github.com/bitcoin-sv/spv-wallet-web-backend/transports/http/endpoints/routes" "github.com/bitcoin-sv/spv-wallet-web-backend/transports/spvwallet" + "github.com/bitcoin-sv/spv-wallet-web-backend/transports/websocket" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/gin-gonic/gin" diff --git a/transports/spvwallet/admin_client.go b/transports/spvwallet/admin_client.go index 95fd0ad..c546281 100644 --- a/transports/spvwallet/admin_client.go +++ b/transports/spvwallet/admin_client.go @@ -1,6 +1,3 @@ -// Package spvwallet contains spv-wallet client adapters -// -//nolint:wrapcheck // error wrapped by service package spvwallet import ( @@ -8,6 +5,9 @@ import ( "fmt" walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + walletclientCfg "github.com/bitcoin-sv/spv-wallet-go-client/config" + "github.com/bitcoin-sv/spv-wallet-web-backend/config" "github.com/bitcoin-sv/spv-wallet/models" "github.com/libsv/go-bk/bip32" @@ -15,37 +15,29 @@ import ( "github.com/spf13/viper" ) -// AdminWalletClient is a wrapper for Admin SPV Wallet Client. -type AdminWalletClient struct { - client *walletclient.WalletClient - log *zerolog.Logger +type AdminClientAdapter struct { + log *zerolog.Logger + api *walletclient.AdminAPI } -// RegisterXpub registers xpub in SPV Wallet. -func (c *AdminWalletClient) RegisterXpub(xpriv *bip32.ExtendedKey) (string, error) { +func (a *AdminClientAdapter) RegisterXpub(xpriv *bip32.ExtendedKey) (string, error) { // Get xpub from xpriv. xpub, err := xpriv.Neuter() - if err != nil { - c.log.Error().Msgf("Error while creating new xPub: %v", err.Error()) + a.log.Error().Msgf("Error while creating new xPub: %v", err.Error()) return "", err } - // Register new xpub in SPV Wallet. - err = c.client.AdminNewXpub(context.Background(), xpub.String(), nil) - + _, err = a.api.CreateXPub(context.TODO(), &commands.CreateUserXpub{XPub: xpriv.String()}) if err != nil { - c.log.Error(). - Str("xpub", xpub.String()). - Msgf("Error while registering new xPub: %v", err.Error()) + a.log.Error().Str("xpub", xpub.String()).Msgf("Error while registering new xPub: %v", err.Error()) return "", err } return xpub.String(), nil } -// RegisterPaymail registers new paymail in SPV Wallet. -func (c *AdminWalletClient) RegisterPaymail(alias, xpub string) (string, error) { +func (a *AdminClientAdapter) RegisterPaymail(alias, xpub string) (string, error) { // Get paymail domain from env. domain := viper.GetString(config.EnvPaymailDomain) @@ -55,21 +47,40 @@ func (c *AdminWalletClient) RegisterPaymail(alias, xpub string) (string, error) // Get avatar url from env. avatar := viper.GetString(config.EnvPaymailAvatar) - _, err := c.client.AdminCreatePaymail(context.Background(), xpub, address, alias, avatar) - + _, err := a.api.CreatePaymail(context.TODO(), &commands.CreatePaymail{ + Key: xpub, + Address: address, + PublicName: alias, + Avatar: avatar, + }) if err != nil { - c.log.Error().Msgf("Error while registering new paymail: %v", err.Error()) + a.log.Error().Msgf("Error while registering new paymail: %v", err.Error()) return "", err } + return address, nil } -// GetSharedConfig returns shared config from SPV Wallet. -func (c *AdminWalletClient) GetSharedConfig() (*models.SharedConfig, error) { - sharedConfig, err := c.client.GetSharedConfig(context.Background()) +func (a *AdminClientAdapter) GetSharedConfig() (*models.SharedConfig, error) { + sharedConfig, err := a.api.SharedConfig(context.TODO()) if err != nil { - c.log.Error().Msgf("Error while getting shared config: %v", err.Error()) + a.log.Error().Msgf("Error while getting shared config: %v", err.Error()) return nil, err } - return sharedConfig, nil + + return &models.SharedConfig{ + PaymailDomains: sharedConfig.PaymailDomains, + ExperimentalFeatures: sharedConfig.ExperimentalFeatures, + }, nil +} + +func NewAdminClientAdapter(log *zerolog.Logger) (*AdminClientAdapter, error) { + adminKey := viper.GetString(config.EnvAdminXpriv) + serverURL := viper.GetString(config.EnvServerURL) + api, err := walletclient.NewAdminAPIWithXPriv(walletclientCfg.New(walletclientCfg.WithAddr(serverURL)), adminKey) + if err != nil { + return nil, fmt.Errorf("failed to initialize admin API: %w", err) + } + + return &AdminClientAdapter{api: api, log: log}, nil } diff --git a/transports/spvwallet/client.go b/transports/spvwallet/client.go deleted file mode 100644 index 2ff641e..0000000 --- a/transports/spvwallet/client.go +++ /dev/null @@ -1,272 +0,0 @@ -package spvwallet - -import ( - "context" - "fmt" - "math" - - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" - "github.com/bitcoin-sv/spv-wallet-web-backend/domain/users" - "github.com/bitcoin-sv/spv-wallet/models" - "github.com/bitcoin-sv/spv-wallet/models/filter" - "github.com/pkg/errors" - "github.com/rs/zerolog" -) - -// Client implements UserWalletClient interface which wraps the spv-wallet-go-client and provides methods for user. -type Client struct { - client *walletclient.WalletClient - log *zerolog.Logger -} - -// CreateAccessKey creates new access key for user. -func (c *Client) CreateAccessKey() (users.AccKey, error) { - accessKey, err := c.client.CreateAccessKey(context.Background(), nil) - if err != nil { - c.log.Error().Msgf("Error while creating new accessKey: %v", err.Error()) - return nil, errors.Wrap(err, "error while creating new accessKey ") - } - - accessKeyData := AccessKey{ - ID: accessKey.ID, - Key: accessKey.Key, - } - - return &accessKeyData, nil -} - -// GetAccessKey checks if access key is valid. -func (c *Client) GetAccessKey(accessKeyID string) (users.AccKey, error) { - accessKey, err := c.client.GetAccessKey(context.Background(), accessKeyID) - if err != nil { - c.log.Error(). - Str("accessKeyID", accessKeyID). - Msgf("Error while getting accessKey: %v", err.Error()) - return nil, errors.Wrap(err, "error while getting accessKey") - } - - accessKeyData := AccessKey{ - ID: accessKey.ID, - Key: accessKey.Key, - } - - return &accessKeyData, nil -} - -// RevokeAccessKey revokes access key. -func (c *Client) RevokeAccessKey(accessKeyID string) (users.AccKey, error) { - accessKey, err := c.client.RevokeAccessKey(context.Background(), accessKeyID) - if err != nil { - c.log.Error(). - Str("accessKeyID", accessKeyID). - Msgf("Error while revoking accessKey: %v", err.Error()) - return nil, errors.Wrap(err, "error while revoking accessKey") - } - - accessKeyData := AccessKey{ - ID: accessKey.ID, - Key: accessKey.Key, - } - - return &accessKeyData, nil -} - -// GetXPub returns xpub. -func (c *Client) GetXPub() (users.PubKey, error) { - xpub, err := c.client.GetXPub(context.Background()) - if err != nil { - c.log.Error().Msgf("Error while getting new xPub: %v", err.Error()) - return nil, errors.Wrap(err, "error while getting new xPub") - } - - xPub := XPub{ - ID: xpub.ID, - CurrentBalance: xpub.CurrentBalance, - } - - return &xPub, nil -} - -// SendToRecipients sends satoshis to recipients. -func (c *Client) SendToRecipients(recipients []*walletclient.Recipients, senderPaymail string) (users.Transaction, error) { - // Create matadata with sender and receiver paymails. - metadata := map[string]any{ - "receiver": recipients[0].To, - "sender": senderPaymail, - } - - // Send transaction. - transaction, err := c.client.SendToRecipients(context.Background(), recipients, metadata) - if err != nil { - c.log.Error().Msgf("Error while creating new tx: %v", err.Error()) - return nil, errors.Wrap(err, "error while creating new tx") - } - - t := &Transaction{ - ID: transaction.ID, - Direction: fmt.Sprint(transaction.TransactionDirection), - TotalValue: transaction.TotalValue, - Status: transaction.Status, - CreatedAt: transaction.Model.CreatedAt, - } - return t, nil -} - -// CreateAndFinalizeTransaction creates draft transaction and finalizes it. -func (c *Client) CreateAndFinalizeTransaction(recipients []*walletclient.Recipients, metadata map[string]any) (users.DraftTransaction, error) { - // Create draft transaction. - draftTx, err := c.client.DraftToRecipients(context.Background(), recipients, metadata) - if err != nil { - c.log.Error().Msgf("Error while creating new draft tx: %v", err.Error()) - return nil, errors.Wrap(err, "error while creating new draft tx") - } - - // Finalize draft transaction. - hex, err := c.client.FinalizeTransaction(draftTx) - if err != nil { - c.log.Error().Str("draftTxID", draftTx.ID).Msgf("Error while finalizing tx: %v", err.Error()) - return nil, errors.Wrap(err, "error while finalizing tx") - } - - draftTransaction := DraftTransaction{ - TxDraftID: draftTx.ID, - TxHex: hex, - } - - return &draftTransaction, nil -} - -// RecordTransaction records transaction in SPV Wallet. -func (c *Client) RecordTransaction(hex, draftTxID string, metadata map[string]any) (*models.Transaction, error) { - tx, err := c.client.RecordTransaction(context.Background(), hex, draftTxID, metadata) - if err != nil { - c.log.Error().Str("draftTxID", draftTxID).Msgf("Error while recording tx: %v", err.Error()) - return nil, errors.Wrap(err, "error while recording tx") - } - return tx, nil -} - -// GetTransactions returns all transactions. -func (c *Client) GetTransactions(queryParam *filter.QueryParams, userPaymail string) ([]users.Transaction, error) { - if queryParam.OrderByField == "" { - queryParam.OrderByField = "created_at" - } - - if queryParam.SortDirection == "" { - queryParam.SortDirection = "desc" - } - - transactions, err := c.client.GetTransactions(context.Background(), nil, nil, queryParam) - if err != nil { - c.log.Error(). - Str("userPaymail", userPaymail). - Msgf("Error while getting transactions: %v", err.Error()) - return nil, errors.Wrap(err, "error while getting transactions") - } - - var transactionsData = make([]users.Transaction, 0) - for _, transaction := range transactions { - sender, receiver := GetPaymailsFromMetadata(transaction, userPaymail) - status := "unconfirmed" - if transaction.BlockHeight > 0 { - status = "confirmed" - } - transactionData := Transaction{ - ID: transaction.ID, - Direction: fmt.Sprint(transaction.TransactionDirection), - TotalValue: getAbsoluteValue(transaction.OutputValue), - Fee: transaction.Fee, - Status: status, - CreatedAt: transaction.Model.CreatedAt, - Sender: sender, - Receiver: receiver, - } - transactionsData = append(transactionsData, &transactionData) - } - - return transactionsData, nil -} - -// GetTransaction returns transaction by id. -func (c *Client) GetTransaction(transactionID, userPaymail string) (users.FullTransaction, error) { - transaction, err := c.client.GetTransaction(context.Background(), transactionID) - if err != nil { - c.log.Error(). - Str("transactionId", transactionID). - Str("userPaymail", userPaymail). - Msgf("Error while getting transaction: %v", err.Error()) - return nil, errors.Wrap(err, "error while getting transaction") - } - - sender, receiver := GetPaymailsFromMetadata(transaction, userPaymail) - - transactionData := FullTransaction{ - ID: transaction.ID, - BlockHash: transaction.BlockHash, - BlockHeight: transaction.BlockHeight, - TotalValue: getAbsoluteValue(transaction.OutputValue), - Direction: fmt.Sprint(transaction.TransactionDirection), - Status: transaction.Status, - Fee: transaction.Fee, - NumberOfInputs: transaction.NumberOfInputs, - NumberOfOutputs: transaction.NumberOfOutputs, - CreatedAt: transaction.Model.CreatedAt, - Sender: sender, - Receiver: receiver, - } - - return &transactionData, nil -} - -// GetTransactionsCount returns number of transactions. -func (c *Client) GetTransactionsCount() (int64, error) { - count, err := c.client.GetTransactionsCount(context.Background(), nil, nil) - if err != nil { - c.log.Error().Msgf("Error while getting transactions count: %v", err.Error()) - return 0, errors.Wrap(err, "error while getting transactions count") - } - return count, nil -} - -// UpsertContact creates or updates contact. -func (c *Client) UpsertContact(ctx context.Context, paymail, fullName, requesterPaymail string, metadata map[string]any) (*models.Contact, error) { - contact, err := c.client.UpsertContact(ctx, paymail, fullName, requesterPaymail, metadata) - if err != nil { - return nil, errors.Wrap(err, "upsert contact error") - } - return contact, nil -} - -// AcceptContact accepts contact. -func (c *Client) AcceptContact(ctx context.Context, paymail string) error { - return errors.Wrap(c.client.AcceptContact(ctx, paymail), "accept contact error") -} - -// RejectContact rejects contact. -func (c *Client) RejectContact(ctx context.Context, paymail string) error { - return errors.Wrap(c.client.RejectContact(ctx, paymail), "reject contact error") -} - -// ConfirmContact confirms contact. -func (c *Client) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) error { - return errors.Wrap(c.client.ConfirmContact(ctx, contact, passcode, requesterPaymail, period, digits), "confirm contact error") -} - -// GetContacts returns all contacts. -func (c *Client) GetContacts(ctx context.Context, conditions *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, error) { - resp, err := c.client.GetContacts(ctx, conditions, metadata, queryParams) - if err != nil { - return nil, errors.Wrap(err, "get contacts error") - } - return resp, nil -} - -// GenerateTotpForContact generates TOTP for contact. -func (c *Client) GenerateTotpForContact(contact *models.Contact, period, digits uint) (string, error) { - totp, err := c.client.GenerateTotpForContact(contact, period, digits) - return totp, errors.Wrap(err, "error while generating TOTP for contact") -} - -func getAbsoluteValue(value int64) uint64 { - return uint64(math.Abs(float64(value))) -} diff --git a/transports/spvwallet/client_factory_adapter.go b/transports/spvwallet/client_factory_adapter.go index 986d657..bd9f1ec 100644 --- a/transports/spvwallet/client_factory_adapter.go +++ b/transports/spvwallet/client_factory_adapter.go @@ -1,10 +1,9 @@ package spvwallet import ( - walletclient "github.com/bitcoin-sv/spv-wallet-go-client" "github.com/bitcoin-sv/spv-wallet-web-backend/config" "github.com/bitcoin-sv/spv-wallet-web-backend/domain/users" - "github.com/bitcoin-sv/spv-wallet-web-backend/spverrors" + "github.com/rs/zerolog" "github.com/spf13/viper" ) @@ -23,48 +22,17 @@ func NewWalletClientFactory(log *zerolog.Logger) users.WalletClientFactory { // CreateAdminClient returns AdminWalletClient as spv-wallet-go-client instance with admin key. func (bf *walletClientFactory) CreateAdminClient() (users.AdminWalletClient, error) { - adminKey := viper.GetString(config.EnvAdminXpriv) - serverURL := getServerData() - - adminWalletClient, err := walletclient.NewWithAdminKey(serverURL, adminKey) - if err != nil { - return nil, spverrors.ErrCreateClientAdminKey.Wrap(err) - } - - return &AdminWalletClient{ - client: adminWalletClient, - log: bf.log, - }, nil + return NewAdminClientAdapter(bf.log) } // CreateWithXpriv returns UserWalletClient as spv-wallet-go-client instance with given xpriv. func (bf *walletClientFactory) CreateWithXpriv(xpriv string) (users.UserWalletClient, error) { - serverURL := getServerData() - - userWalletClient, err := walletclient.NewWithXPriv(serverURL, xpriv) - if err != nil { - return nil, spverrors.ErrInvalidCredentials.Wrap(err) - } - - return &Client{ - client: userWalletClient, - log: bf.log, - }, nil + return NewUserClientAdapterWithXPriv(bf.log, xpriv) } // CreateWithAccessKey returns UserWalletClient as spv-wallet-go-client instance with given access key. func (bf *walletClientFactory) CreateWithAccessKey(accessKey string) (users.UserWalletClient, error) { - serverURL := getServerData() - - userWalletClient, err := walletclient.NewWithAccessKey(serverURL, accessKey) - if err != nil { - return nil, spverrors.ErrInvalidCredentials.Wrap(err) - } - - return &Client{ - client: userWalletClient, - log: bf.log, - }, nil + return NewUserClientAdapterWithAccessKey(bf.log, accessKey) } func getServerData() string { diff --git a/transports/spvwallet/methods.go b/transports/spvwallet/methods.go index 9c75302..673bbec 100644 --- a/transports/spvwallet/methods.go +++ b/transports/spvwallet/methods.go @@ -1,12 +1,14 @@ package spvwallet import ( - "github.com/bitcoin-sv/spv-wallet/models" + "math" + + "github.com/bitcoin-sv/spv-wallet/models/response" ) // GetPaymailsFromMetadata returns sender and receiver paymails from metadata. // If no paymail was found in metadata, fallback paymail is returned. -func GetPaymailsFromMetadata(transaction *models.Transaction, fallbackPaymail string) (string, string) { +func GetPaymailsFromMetadata(transaction *response.Transaction, fallbackPaymail string) (string, string) { senderPaymail := "" receiverPaymail := "" @@ -42,3 +44,7 @@ func GetPaymailsFromMetadata(transaction *models.Transaction, fallbackPaymail st return senderPaymail, receiverPaymail } + +func getAbsoluteValue(value int64) uint64 { + return uint64(math.Abs(float64(value))) +} diff --git a/transports/spvwallet/user_client.go b/transports/spvwallet/user_client.go new file mode 100644 index 0000000..9b2083d --- /dev/null +++ b/transports/spvwallet/user_client.go @@ -0,0 +1,310 @@ +package spvwallet + +import ( + "context" + "fmt" + + walletclient "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + walletclientCfg "github.com/bitcoin-sv/spv-wallet-go-client/config" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet-web-backend/config" + "github.com/bitcoin-sv/spv-wallet-web-backend/domain/users" + + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/common" + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/spf13/viper" +) + +type UserClientAdapter struct { + api *walletclient.UserAPI + log *zerolog.Logger +} + +func (u *UserClientAdapter) CreateAccessKey() (users.AccKey, error) { + accessKey, err := u.api.GenerateAccessKey(context.TODO(), &commands.GenerateAccessKey{}) + if err != nil { + u.log.Error().Msgf("Error while creating new accessKey: %v", err.Error()) + return nil, errors.Wrap(err, "error while creating new accessKey ") + } + + return &AccessKey{ID: accessKey.ID, Key: accessKey.Key}, nil +} + +func (u *UserClientAdapter) GetAccessKey(accessKeyID string) (users.AccKey, error) { + accessKey, err := u.api.AccessKey(context.TODO(), accessKeyID) + if err != nil { + u.log.Error().Str("accessKeyID", accessKeyID).Msgf("Error while getting accessKey: %v", err.Error()) + return nil, errors.Wrap(err, "error while getting accessKey") + } + + return &AccessKey{ID: accessKey.ID, Key: accessKey.Key}, nil +} + +func (u *UserClientAdapter) RevokeAccessKey(accessKeyID string) (users.AccKey, error) { + accessKey, err := u.api.AccessKey(context.TODO(), accessKeyID) + if err != nil { + u.log.Error().Str("accessKeyID", accessKeyID).Msgf("Error while fetching accessKey: %v", err.Error()) + return nil, errors.Wrap(err, "error while fetching accessKey") + } + + err = u.api.RevokeAccessKey(context.TODO(), accessKeyID) + if err != nil { + u.log.Error().Str("accessKeyID", accessKeyID).Msgf("Error while revoking accessKey: %v", err.Error()) + return nil, errors.Wrap(err, "error while revoking accessKey") + } + + return &AccessKey{ID: accessKey.ID, Key: accessKey.Key}, nil +} + +// XPub Key methods +func (u *UserClientAdapter) GetXPub() (users.PubKey, error) { + xpub, err := u.api.XPub(context.TODO()) + if err != nil { + u.log.Error().Msgf("Error while getting new xPub: %v", err.Error()) + return nil, errors.Wrap(err, "error while getting new xPub") + } + + return &XPub{ID: xpub.ID, CurrentBalance: xpub.CurrentBalance}, nil +} + +func (u *UserClientAdapter) SendToRecipients(recipients []*commands.Recipients, senderPaymail string) (users.Transaction, error) { + // Send transaction. + transaction, err := u.api.SendToRecipients(context.Background(), &commands.SendToRecipients{ + Recipients: recipients, + Metadata: map[string]any{ + "receiver": recipients[0].To, + "sender": senderPaymail, + }, + }) + if err != nil { + u.log.Error().Msgf("Error while creating new tx: %v", err.Error()) + return nil, errors.Wrap(err, "error while creating new tx") + } + + return &Transaction{ + ID: transaction.ID, + Direction: fmt.Sprint(transaction.TransactionDirection), + TotalValue: transaction.TotalValue, + Status: transaction.Status, + CreatedAt: transaction.Model.CreatedAt, + }, nil +} + +func (u *UserClientAdapter) GetTransactions(queryParam *filter.QueryParams, userPaymail string) ([]users.Transaction, error) { + if queryParam.OrderByField == "" { + queryParam.OrderByField = "created_at" + } + + if queryParam.SortDirection == "" { + queryParam.SortDirection = "desc" + } + + page, err := u.api.Transactions(context.TODO(), queries.QueryWithPageFilter[filter.TransactionFilter](filter.Page{ + Number: queryParam.Page, + Size: queryParam.PageSize, + Sort: queryParam.SortDirection, + SortBy: queryParam.OrderByField, + })) + if err != nil { + u.log.Error().Str("userPaymail", userPaymail).Msgf("Error while getting transactions: %v", err.Error()) + return nil, errors.Wrap(err, "error while getting transactions") + } + + var transactionsData = make([]users.Transaction, 0) + for _, transaction := range page.Content { + sender, receiver := GetPaymailsFromMetadata(transaction, userPaymail) + status := "unconfirmed" + if transaction.BlockHeight > 0 { + status = "confirmed" + } + + transactionsData = append(transactionsData, &Transaction{ + ID: transaction.ID, + Direction: fmt.Sprint(transaction.TransactionDirection), + TotalValue: getAbsoluteValue(transaction.OutputValue), + Fee: transaction.Fee, + Status: status, + CreatedAt: transaction.Model.CreatedAt, + Sender: sender, + Receiver: receiver, + }) + } + + return transactionsData, nil +} + +func (u *UserClientAdapter) GetTransaction(transactionID, userPaymail string) (users.FullTransaction, error) { + transaction, err := u.api.Transaction(context.TODO(), transactionID) + if err != nil { + u.log.Error().Str("transactionId", transactionID).Str("userPaymail", userPaymail).Msgf("Error while getting transaction: %v", err.Error()) + return nil, errors.Wrap(err, "error while getting transaction") + } + + sender, receiver := GetPaymailsFromMetadata(transaction, userPaymail) + return &FullTransaction{ + ID: transaction.ID, + BlockHash: transaction.BlockHash, + BlockHeight: transaction.BlockHeight, + TotalValue: getAbsoluteValue(transaction.OutputValue), + Direction: fmt.Sprint(transaction.TransactionDirection), + Status: transaction.Status, + Fee: transaction.Fee, + NumberOfInputs: transaction.NumberOfInputs, + NumberOfOutputs: transaction.NumberOfOutputs, + CreatedAt: transaction.Model.CreatedAt, + Sender: sender, + Receiver: receiver, + }, nil +} + +func (u *UserClientAdapter) GetTransactionsCount() (int64, error) { + return 0, nil // Note: Functionality it's not a part of the SPV Wallet Go client. +} + +func (u *UserClientAdapter) CreateAndFinalizeTransaction(recipients []*commands.Recipients, metadata map[string]any) (users.DraftTransaction, error) { + draftTx, err := u.api.SendToRecipients(context.TODO(), &commands.SendToRecipients{ + Recipients: recipients, + Metadata: metadata, + }) + if err != nil { + u.log.Error().Msgf("Error while sending to recipients: %v", err.Error()) + return nil, errors.Wrap(err, "error while sending to recipients") + } + + return &DraftTransaction{ + TxDraftID: draftTx.ID, + TxHex: draftTx.Hex, + }, nil +} + +func (u *UserClientAdapter) RecordTransaction(hex, draftTxID string, metadata map[string]any) (*models.Transaction, error) { + tx, err := u.api.RecordTransaction(context.Background(), &commands.RecordTransaction{ + Metadata: metadata, + Hex: hex, + ReferenceID: draftTxID, + }) + if err != nil { + u.log.Error().Str("draftTxID", draftTxID).Msgf("Error while recording tx: %v", err.Error()) + return nil, errors.Wrap(err, "error while recording tx") + } + + return &models.Transaction{ + Model: common.Model{}, + ID: tx.ID, + Hex: tx.Hex, + XpubInIDs: tx.XpubInIDs, + XpubOutIDs: tx.XpubOutIDs, + BlockHash: tx.BlockHash, + BlockHeight: tx.BlockHeight, + Fee: tx.Fee, + NumberOfInputs: tx.NumberOfInputs, + NumberOfOutputs: tx.NumberOfOutputs, + DraftID: tx.DraftID, + TotalValue: tx.TotalValue, + OutputValue: tx.OutputValue, + Outputs: tx.Outputs, + Status: tx.Status, + TransactionDirection: tx.TransactionDirection, + }, nil +} + +// Contacts methods +func (u *UserClientAdapter) UpsertContact(ctx context.Context, paymail, fullName, requesterPaymail string, metadata map[string]any) (*models.Contact, error) { + contact, err := u.api.UpsertContact(context.TODO(), commands.UpsertContact{ + ContactPaymail: paymail, + FullName: fullName, + Metadata: metadata, + RequesterPaymail: requesterPaymail, + }) + if err != nil { + return nil, errors.Wrap(err, "upsert contact error") + } + + return &models.Contact{ + Model: common.Model(contact.Model), + ID: contact.ID, + FullName: contact.FullName, + Paymail: contact.Paymail, + PubKey: contact.PubKey, + Status: contact.Status, + }, nil +} + +func (u *UserClientAdapter) AcceptContact(ctx context.Context, paymail string) error { + return errors.Wrap(u.api.AcceptInvitation(ctx, paymail), "accept contact error") +} + +func (u *UserClientAdapter) RejectContact(ctx context.Context, paymail string) error { + return errors.Wrap(u.api.RejectInvitation(ctx, paymail), "reject contact error") +} + +func (u *UserClientAdapter) ConfirmContact(ctx context.Context, contact *models.Contact, passcode, requesterPaymail string, period, digits uint) error { + return errors.Wrap(u.api.ConfirmContact(ctx, contact, passcode, requesterPaymail, period, digits), "confirm contact error") +} + +func (u *UserClientAdapter) GetContacts(ctx context.Context, conditions *filter.ContactFilter, metadata map[string]any, queryParams *filter.QueryParams) (*models.SearchContactsResponse, error) { + opts := []queries.QueryOption[filter.ContactFilter]{ + queries.QueryWithMetadataFilter[filter.ContactFilter](metadata), + queries.QueryWithPageFilter[filter.ContactFilter](filter.Page{ + Number: queryParams.Page, + Size: queryParams.PageSize, + Sort: queryParams.SortDirection, + SortBy: queryParams.OrderByField, + }), + queries.QueryWithFilter(filter.ContactFilter{ + ModelFilter: conditions.ModelFilter, + ID: conditions.ID, + FullName: conditions.FullName, + Paymail: conditions.Paymail, + PubKey: conditions.PubKey, + Status: conditions.Status, + }), + } + + res, err := u.api.Contacts(context.TODO(), opts...) + if err != nil { + u.log.Error().Msgf("Error while fetching contacts: %v", err.Error()) + return nil, errors.Wrap(err, "error while fetching contacts") + } + + return &models.SearchContactsResponse{ + Content: []*models.Contact{}, + Page: models.Page{ + OrderByField: &queryParams.OrderByField, + SortDirection: &queryParams.SortDirection, + TotalElements: int64(res.Page.TotalElements), + TotalPages: res.Page.TotalPages, + Size: res.Page.Size, + Number: res.Page.Number, + }, + }, nil +} + +func (u *UserClientAdapter) GenerateTotpForContact(contact *models.Contact, period, digits uint) (string, error) { + totp, err := u.api.GenerateTotpForContact(contact, period, digits) + return totp, errors.Wrap(err, "error while generating TOTP for contact") +} + +func NewUserClientAdapterWithXPriv(log *zerolog.Logger, xPriv string) (*UserClientAdapter, error) { + serverURL := viper.GetString(config.EnvServerURL) + api, err := walletclient.NewUserAPIWithXPriv(walletclientCfg.New(walletclientCfg.WithAddr(serverURL)), xPriv) + if err != nil { + return nil, fmt.Errorf("failed to initialize user API: %w", err) + } + + return &UserClientAdapter{api: api, log: log}, nil +} + +func NewUserClientAdapterWithAccessKey(log *zerolog.Logger, accessKey string) (*UserClientAdapter, error) { + serverURL := viper.GetString(config.EnvServerURL) + api, err := walletclient.NewUserAPIWithAccessKey(walletclientCfg.New(walletclientCfg.WithAddr(serverURL)), accessKey) + if err != nil { + return nil, fmt.Errorf("failed to initialize user API: %w", err) + } + + return &UserClientAdapter{api: api, log: log}, nil +}