diff --git a/actions/transactions/broadcast_callback.go b/actions/transactions/broadcast_callback.go index 907f84878..878bc29f6 100644 --- a/actions/transactions/broadcast_callback.go +++ b/actions/transactions/broadcast_callback.go @@ -12,7 +12,6 @@ import ( // broadcastCallback will handle a broadcastCallback call from the broadcast api func broadcastCallback(c *gin.Context) { logger := reqctx.Logger(c) - config := reqctx.AppConfig(c) var callbackResp chainmodels.TXInfo err := c.Bind(&callbackResp) @@ -21,11 +20,7 @@ func broadcastCallback(c *gin.Context) { return } - if config.ExperimentalFeatures.V2 { - err = reqctx.Engine(c).TxSyncService().Handle(c, callbackResp) - } else { - err = reqctx.Engine(c).HandleTxCallback(c, &callbackResp) - } + err = reqctx.Engine(c).HandleTxCallback(c, &callbackResp) if err != nil { logger.Err(err).Any("TxInfo", callbackResp).Msgf("failed to update transaction in ARC broadcast callback handler") diff --git a/actions/v2/callback/callbacks.go b/actions/v2/callback/callbacks.go new file mode 100644 index 000000000..3779afe93 --- /dev/null +++ b/actions/v2/callback/callbacks.go @@ -0,0 +1,51 @@ +package callback + +import ( + "context" + "net/http" + + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/engine" + "github.com/bitcoin-sv/spv-wallet/engine/chain/models" + "github.com/bitcoin-sv/spv-wallet/engine/spverrors" + "github.com/bitcoin-sv/spv-wallet/engine/v2/utils/must" + "github.com/bitcoin-sv/spv-wallet/server/middleware" + "github.com/bitcoin-sv/spv-wallet/server/reqctx" + "github.com/gin-gonic/gin" +) + +type txSyncService interface { + Handle(ctx context.Context, txInfo chainmodels.TXInfo) error +} + +// RegisterRoutes registers endpoints for callbacks. +func RegisterRoutes(ginEngine *gin.Engine, cfg *config.AppConfig, engine engine.V2Interface) { + if cfg.ARCCallbackEnabled() { + callbackURL, err := cfg.ARC.Callback.ShouldGetURL() + must.HaveNoErrorf(err, "couldn't get callback URL from configuration") + + broadcastCallback := ginEngine.Group("", middleware.CallbackTokenMiddleware()) + broadcastCallback.POST(callbackURL.Path, broadcastCallbackHandler(engine.TxSyncService())) + } +} + +func broadcastCallbackHandler(service txSyncService) gin.HandlerFunc { + return func(c *gin.Context) { + logger := reqctx.Logger(c) + var callbackResp chainmodels.TXInfo + + err := c.Bind(&callbackResp) + if err != nil { + spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, logger) + return + } + + err = service.Handle(c, callbackResp) + + if err != nil { + logger.Err(err).Ctx(c).Any("TxInfo", callbackResp).Msgf("failed to update transaction in ARC broadcast callback handler") + } + + c.Status(http.StatusOK) + } +} diff --git a/actions/v2/register.go b/actions/v2/register.go index d61c32aca..34ddf2e2f 100644 --- a/actions/v2/register.go +++ b/actions/v2/register.go @@ -1,11 +1,17 @@ package v2 import ( + "github.com/bitcoin-sv/spv-wallet/actions/paymailserver" + "github.com/bitcoin-sv/spv-wallet/actions/v2/callback" "github.com/bitcoin-sv/spv-wallet/actions/v2/swagger" - "github.com/bitcoin-sv/spv-wallet/server/handlers" + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/engine" + "github.com/gin-gonic/gin" ) // RegisterNonOpenAPIRoutes collects all the action's routes that aren't part of the Open API documentation and registers them using the handlersManager. -func RegisterNonOpenAPIRoutes(handlersManager *handlers.Manager) { - swagger.RegisterRoutes(handlersManager) +func RegisterNonOpenAPIRoutes(ginEngine *gin.Engine, cfg *config.AppConfig, engine engine.V2Interface) { + paymailserver.Register(engine.PaymailServerConfiguration(), ginEngine) + swagger.RegisterRoutes(ginEngine, cfg) + callback.RegisterRoutes(ginEngine, cfg, engine) } diff --git a/actions/v2/swagger/swagger-ui.go b/actions/v2/swagger/swagger-ui.go index 7a9df99b0..9adce4543 100644 --- a/actions/v2/swagger/swagger-ui.go +++ b/actions/v2/swagger/swagger-ui.go @@ -6,22 +6,22 @@ import ( "strings" "github.com/bitcoin-sv/spv-wallet/api" - routes "github.com/bitcoin-sv/spv-wallet/server/handlers" + "github.com/bitcoin-sv/spv-wallet/config" "github.com/gin-gonic/gin" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) // RegisterRoutes creates the specific package routes -func RegisterRoutes(handlersManager *routes.Manager) { - root := handlersManager.Get(routes.GroupRoot) +func RegisterRoutes(engine *gin.Engine, cfg *config.AppConfig) { + root := engine.Group("") root.GET("v2/swagger", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "v2/swagger/index.html") }) - api.Yaml = strings.Replace(api.Yaml, "version: main", fmt.Sprintf("version: '%s'", handlersManager.APIVersion()), 1) - api.Yaml = strings.Replace(api.Yaml, "https://github.com/bitcoin-sv/spv-wallet/blob/main", fmt.Sprintf("https://github.com/bitcoin-sv/spv-wallet/blob/%s", handlersManager.APIVersion()), 1) + api.Yaml = strings.Replace(api.Yaml, "version: main", fmt.Sprintf("version: '%s'", cfg.Version), 1) + api.Yaml = strings.Replace(api.Yaml, "https://github.com/bitcoin-sv/spv-wallet/blob/main", fmt.Sprintf("https://github.com/bitcoin-sv/spv-wallet/blob/%s", cfg.Version), 1) root.GET("/api/gen.api.yaml", func(c *gin.Context) { c.Header("Content-Type", "application/yaml") diff --git a/engine/client.go b/engine/client.go index ac2dae182..5b3542466 100644 --- a/engine/client.go +++ b/engine/client.go @@ -17,15 +17,7 @@ import ( paymailclient "github.com/bitcoin-sv/spv-wallet/engine/paymail" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" - "github.com/bitcoin-sv/spv-wallet/engine/v2/addresses" - "github.com/bitcoin-sv/spv-wallet/engine/v2/data" - "github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository" - "github.com/bitcoin-sv/spv-wallet/engine/v2/operations" - "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/record" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txsync" - "github.com/bitcoin-sv/spv-wallet/engine/v2/users" + "github.com/bitcoin-sv/spv-wallet/engine/v2/engine" "github.com/bitcoin-sv/spv-wallet/models/bsv" "github.com/go-resty/resty/v2" "github.com/mrz1836/go-cachestore" @@ -37,39 +29,32 @@ type ( // Client is the SPV Wallet Engine client & options Client struct { options *clientOptions + // TEMPORARY: to limit the changes in the codebase + V2Interface } // clientOptions holds all the configuration for the client clientOptions struct { - cacheStore *cacheStoreOptions // Configuration options for Cachestore (ristretto, redis, etc.) - cluster *clusterOptions // Configuration options for the cluster coordinator - dataStore *dataStoreOptions // Configuration options for the DataStore (PostgreSQL, etc.) - debug bool // If the client is in debug mode - encryptionKey string // Encryption key for encrypting sensitive information (IE: paymail xPub) (hex encoded key) - httpClient *resty.Client // HTTP client to use for http calls - iuc bool // (Input UTXO Check) True will check input utxos when saving transactions - logger *zerolog.Logger // Internal logging - metrics *metrics.Metrics // Metrics with a collector interface - notifications *notificationsOptions // Configuration options for Notifications - paymail *paymailOptions // Paymail options & client - transactionOutlinesService outlines.Service // Service for transaction outlines - transactionRecordService *record.Service // Service for recording transactions - taskManager *taskManagerOptions // Configuration options for the TaskManager (TaskQ, etc.) - userAgent string // User agent for all outgoing requests - chainService chain.Service // Chain service - arcConfig chainmodels.ARCConfig // Configuration for ARC - bhsConfig chainmodels.BHSConfig // Configuration for BHS - feeUnit *bsv.FeeUnit // Fee unit for transactions - - // v2 - repositories *repository.All // Repositories for all db models - users *users.Service // User domain service - paymails *paymails.Service // Paymail domain service - addresses *addresses.Service - operations *operations.Service - txSync *txsync.Service - data *data.Service - config *config.AppConfig + cacheStore *cacheStoreOptions // Configuration options for Cachestore (ristretto, redis, etc.) + cluster *clusterOptions // Configuration options for the cluster coordinator + dataStore *dataStoreOptions // Configuration options for the DataStore (PostgreSQL, etc.) + debug bool // If the client is in debug mode + encryptionKey string // Encryption key for encrypting sensitive information (IE: paymail xPub) (hex encoded key) + httpClient *resty.Client // HTTP client to use for http calls + iuc bool // (Input UTXO Check) True will check input utxos when saving transactions + logger *zerolog.Logger // Internal logging + metrics *metrics.Metrics // Metrics with a collector interface + notifications *notificationsOptions // Configuration options for Notifications + paymail *paymailOptions // Paymail options & client + taskManager *taskManagerOptions // Configuration options for the TaskManager (TaskQ, etc.) + userAgent string // User agent for all outgoing requests + feeUnit *bsv.FeeUnit // Fee unit for transactions + + chainService chain.Service // Chain service + arcConfig chainmodels.ARCConfig // Configuration for ARC + bhsConfig chainmodels.BHSConfig // Configuration for BHS + + config *config.AppConfig } // cacheStoreOptions holds the cache configuration and client @@ -138,6 +123,16 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) client.options.logger = logging.GetDefaultLogger() } + if client.options.config != nil && client.options.config.ExperimentalFeatures.V2 { + client.V2Interface = engine.NewEngine( + client.options.config, + *client.options.logger, + engine.WithResty(client.options.httpClient), + engine.WithPaymailClient(client.options.paymail.client), + ) + return client, nil + } + // Load the Cachestore client var err error if err = client.loadCache(ctx); err != nil { @@ -158,14 +153,6 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) return nil, err } - client.loadRepositories() - - client.loadUsersService() - client.loadPaymailsService() - client.loadAddressesService() - client.loadDataService() - client.loadOperationsService() - // Load the Paymail client and service (if does not exist) if err = client.loadPaymailComponents(); err != nil { return nil, err @@ -182,11 +169,6 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) } client.loadChainService() - client.loadTxSyncService() - - if err = client.loadTransactionRecordService(); err != nil { - return nil, err - } // Register all cron jobs if err = client.registerCronJobs(); err != nil { @@ -203,10 +185,6 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) } } - if err = client.loadTransactionOutlinesService(); err != nil { - return nil, err - } - // Return the client return client, nil } @@ -229,6 +207,14 @@ func (c *Client) Cluster() cluster.ClientInterface { // Close will safely close any open connections (cache, datastore, etc.) func (c *Client) Close(ctx context.Context) error { + if c.V2Interface != nil { + err := c.V2Interface.Close(ctx) + if err != nil { + return spverrors.Wrapf(err, "failed to close envine V2") + } + return nil + } + // Close WebhookManager if c.options.notifications != nil && c.options.notifications.webhookManager != nil { c.options.notifications.webhookManager.Stop() @@ -351,38 +337,3 @@ func (c *Client) LogBHSReadiness(ctx context.Context) { func (c *Client) FeeUnit() bsv.FeeUnit { return *c.options.feeUnit } - -// Repositories will return all the repositories -func (c *Client) Repositories() *repository.All { - return c.options.repositories -} - -// UsersService will return the user domain service -func (c *Client) UsersService() *users.Service { - return c.options.users -} - -// PaymailsService will return the paymail domain service -func (c *Client) PaymailsService() *paymails.Service { - return c.options.paymails -} - -// AddressesService will return the address domain service -func (c *Client) AddressesService() *addresses.Service { - return c.options.addresses -} - -// DataService will return the data domain service -func (c *Client) DataService() *data.Service { - return c.options.data -} - -// OperationsService will return the operations domain service -func (c *Client) OperationsService() *operations.Service { - return c.options.operations -} - -// TxSyncService will return the transaction sync service -func (c *Client) TxSyncService() *txsync.Service { - return c.options.txSync -} diff --git a/engine/client_internal.go b/engine/client_internal.go index 4de368116..f825d3991 100644 --- a/engine/client_internal.go +++ b/engine/client_internal.go @@ -12,18 +12,6 @@ import ( "github.com/bitcoin-sv/spv-wallet/engine/paymail" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" - "github.com/bitcoin-sv/spv-wallet/engine/v2/addresses" - "github.com/bitcoin-sv/spv-wallet/engine/v2/data" - "github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository" - "github.com/bitcoin-sv/spv-wallet/engine/v2/operations" - "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails" - paymailprovider "github.com/bitcoin-sv/spv-wallet/engine/v2/paymailserver" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/beef" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines/utxo" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/record" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txsync" - "github.com/bitcoin-sv/spv-wallet/engine/v2/users" "github.com/mrz1836/go-cachestore" ) @@ -76,7 +64,7 @@ func (c *Client) autoMigrate(ctx context.Context) error { } db := c.Datastore().DB().WithContext(ctx) - models := AllDBModels(c.options.paymail.serverConfig.ExperimentalProvider) + models := AllDBModels() if err := db.AutoMigrate(models...); err != nil { return spverrors.Wrapf(err, "failed to auto-migrate models") @@ -170,70 +158,6 @@ func (c *Client) loadPaymailComponents() (err error) { return } -func (c *Client) loadTransactionOutlinesService() error { - if c.options.transactionOutlinesService == nil { - logger := c.Logger().With().Str("subservice", "transactionOutlines").Logger() - utxoSelector := utxo.NewSelector(c.Datastore().DB(), c.FeeUnit()) - beefService := beef.NewService(c.Repositories().Transactions) - - c.options.transactionOutlinesService = outlines.NewService(c.PaymailService(), c.options.paymails, beefService, utxoSelector, c.FeeUnit(), logger, c.UsersService()) - } - return nil -} - -func (c *Client) loadTransactionRecordService() error { - if c.options.transactionRecordService == nil { - logger := c.Logger().With().Str("subservice", "transactionRecord").Logger() - c.options.transactionRecordService = record.NewService( - logger, - c.AddressesService(), - c.UsersService(), - c.Repositories().Outputs, - c.Repositories().Operations, - c.Repositories().Transactions, - c.Chain(), - c.PaymailService(), - ) - } - return nil -} - -func (c *Client) loadRepositories() { - if c.options.repositories == nil { - c.options.repositories = repository.NewRepositories(c.Datastore().DB()) - } -} - -func (c *Client) loadUsersService() { - if c.options.users == nil { - c.options.users = users.NewService(c.Repositories().Users, c.options.config) - } -} - -func (c *Client) loadPaymailsService() { - if c.options.paymails == nil { - c.options.paymails = paymails.NewService(c.Repositories().Paymails, c.UsersService(), c.options.config) - } -} - -func (c *Client) loadAddressesService() { - if c.options.addresses == nil { - c.options.addresses = addresses.NewService(c.Repositories().Addresses) - } -} - -func (c *Client) loadDataService() { - if c.options.data == nil { - c.options.data = data.NewService(c.Repositories().Data) - } -} - -func (c *Client) loadOperationsService() { - if c.options.operations == nil { - c.options.operations = operations.NewService(c.Repositories().Operations) - } -} - func (c *Client) loadChainService() { if c.options.chainService == nil { logger := c.Logger().With().Str("subservice", "chain").Logger() @@ -242,13 +166,6 @@ func (c *Client) loadChainService() { } } -func (c *Client) loadTxSyncService() { - if c.options.txSync == nil { - logger := c.Logger().With().Str("subservice", "tx_sync").Logger() - c.options.txSync = txsync.NewService(logger, c.Repositories().Transactions) - } -} - // loadTaskmanager will load the TaskManager and start the TaskManager client func (c *Client) loadTaskmanager(ctx context.Context) (err error) { // Load if a custom interface was NOT provided @@ -298,21 +215,7 @@ func (c *Client) loadPaymailServer() (err error) { // Create the paymail configuration using the client and default service provider paymailLocator := &paymailserver.PaymailServiceLocator{} - var serviceProvider paymailserver.PaymailServiceProvider - if c.options.paymail.serverConfig.ExperimentalProvider { - paymailServiceLogger := c.Logger().With().Str("subservice", "paymail-service-provider").Logger() - serviceProvider = paymailprovider.NewServiceProvider( - &paymailServiceLogger, - c.PaymailsService(), - c.UsersService(), - c.AddressesService(), - c.Chain(), - c.TransactionRecordService(), - ) - } else { - serviceProvider = &PaymailDefaultServiceProvider{client: c} - } - + serviceProvider := &PaymailDefaultServiceProvider{client: c} paymailLocator.RegisterPaymailService(serviceProvider) paymailLocator.RegisterPikeContactService(&PikeContactServiceProvider{client: c}) paymailLocator.RegisterPikePaymentService(&PikePaymentServiceProvider{client: c}) diff --git a/engine/client_options.go b/engine/client_options.go index dccb4e3cf..46f1ea59a 100644 --- a/engine/client_options.go +++ b/engine/client_options.go @@ -67,9 +67,6 @@ func defaultClientOptions() *clientOptions { }, }, - // Blank transaction outline - transactionOutlinesService: nil, - // Blank TaskManager config taskManager: &taskManagerOptions{ TaskEngine: nil, @@ -349,13 +346,6 @@ func WithPaymailPikePaymentSupport() ClientOps { } } -// WithPaymailExperimentalNewTransactionFlow switches to the new transaction flow (experimental) -func WithPaymailExperimentalNewTransactionFlow() ClientOps { - return func(c *clientOptions) { - c.paymail.serverConfig.ExperimentalProvider = true - } -} - // ----------------------------------------------------------------- // TASK MANAGER // ----------------------------------------------------------------- diff --git a/engine/client_transaction.go b/engine/client_transaction.go deleted file mode 100644 index 7dbf451fe..000000000 --- a/engine/client_transaction.go +++ /dev/null @@ -1,16 +0,0 @@ -package engine - -import ( - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" - "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/record" -) - -// TransactionOutlinesService will return the outlines.Service if it exists -func (c *Client) TransactionOutlinesService() outlines.Service { - return c.options.transactionOutlinesService -} - -// TransactionRecordService will return the record.Service if it exists -func (c *Client) TransactionRecordService() *record.Service { - return c.options.transactionRecordService -} diff --git a/engine/definitions.go b/engine/definitions.go index 97c8892b9..e3c26a324 100644 --- a/engine/definitions.go +++ b/engine/definitions.go @@ -2,8 +2,6 @@ package engine import ( "time" - - "github.com/bitcoin-sv/spv-wallet/engine/v2/database" ) // Defaults for engine functionality @@ -114,8 +112,8 @@ const ( ) // AllDBModels returns all the database models, e.g. for migrations. -func AllDBModels(v2 bool) []any { - legacyModels := []any{ +func AllDBModels() []any { + return []any{ &Xpub{}, &AccessKey{}, &DraftTransaction{}, @@ -126,14 +124,4 @@ func AllDBModels(v2 bool) []any { &Webhook{}, &PaymailAddress{}, } - - if !v2 { - return legacyModels - } - - // New models from database package - // NOTE: Our intention is to move all models to the database package in the future - dbModels := database.Models() - - return append(legacyModels, dbModels...) } diff --git a/engine/interface.go b/engine/interface.go index 6fe29dade..4734b8d5f 100644 --- a/engine/interface.go +++ b/engine/interface.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/go-paymail/server" "github.com/bitcoin-sv/spv-wallet/engine/chain" "github.com/bitcoin-sv/spv-wallet/engine/chain/models" "github.com/bitcoin-sv/spv-wallet/engine/cluster" @@ -25,6 +26,7 @@ import ( "github.com/bitcoin-sv/spv-wallet/models/bsv" "github.com/mrz1836/go-cachestore" "github.com/rs/zerolog" + "gorm.io/gorm" ) // AccessKeyService is the access key actions @@ -64,8 +66,6 @@ type ClientService interface { Notifications() *notifications.Notifications PaymailClient() paymail.ClientInterface PaymailService() paymailclient.ServiceClient - TransactionOutlinesService() outlines.Service - TransactionRecordService() *record.Service Taskmanager() taskmanager.TaskEngine } @@ -160,15 +160,22 @@ type XPubService interface { UpdateXpubMetadata(ctx context.Context, xPubID string, metadata Metadata) (*Xpub, error) } -// V2 contains services for version 2 -type V2 interface { +// V2Interface is the V2 interface comprised of all services/actions +// Deprecated: V2Interface will be removed in a future version. +type V2Interface interface { + DB() *gorm.DB + Chain() chain.Service Repositories() *repository.All UsersService() *users.Service PaymailsService() *paymails.Service AddressesService() *addresses.Service DataService() *data.Service OperationsService() *operations.Service + TransactionOutlinesService() outlines.Service + TransactionRecordService() *record.Service + PaymailServerConfiguration() *server.Configuration TxSyncService() *txsync.Service + Close(context.Context) error } // ClientInterface is the client (spv wallet engine) interface comprised of all services/actions @@ -197,5 +204,5 @@ type ClientInterface interface { Chain() chain.Service LogBHSReadiness(ctx context.Context) FeeUnit() bsv.FeeUnit - V2 + V2Interface } diff --git a/engine/paymail/paymail_service_client.go b/engine/paymail/paymail_service_client.go index ae5df7eaf..439deb696 100644 --- a/engine/paymail/paymail_service_client.go +++ b/engine/paymail/paymail_service_client.go @@ -17,14 +17,17 @@ import ( const cacheKeyCapabilities = "paymail-capabilities-" const cacheTTLCapabilities = 2 * time.Minute +// ClientInterface is an interface for the paymail client +type ClientInterface = paymail.ClientInterface + type service struct { - cache cachestore.ClientInterface + cache Cache paymailClient paymail.ClientInterface log zerolog.Logger } // NewServiceClient creates a new paymail service client -func NewServiceClient(cache cachestore.ClientInterface, paymailClient paymail.ClientInterface, log zerolog.Logger) ServiceClient { +func NewServiceClient(cache Cache, paymailClient ClientInterface, log zerolog.Logger) ServiceClient { if paymailClient == nil { panic(spverrors.Newf("paymail client is required to create a new paymail service")) } diff --git a/engine/paymail/paymail_service_client_interface.go b/engine/paymail/paymail_service_client_interface.go index 7bf698bb0..04a3dd3f5 100644 --- a/engine/paymail/paymail_service_client_interface.go +++ b/engine/paymail/paymail_service_client_interface.go @@ -2,12 +2,19 @@ package paymail import ( "context" + "time" "github.com/bitcoin-sv/go-paymail" trx "github.com/bitcoin-sv/go-sdk/transaction" "github.com/bitcoin-sv/spv-wallet/models/bsv" ) +// Cache is an interface that defines the methods to interact with a cache. +type Cache interface { + GetModel(ctx context.Context, key string, model interface{}) error + SetModel(ctx context.Context, key string, model interface{}, ttl time.Duration, dependencies ...string) error +} + // ServiceClient is a service that aims to make easier paymail operations. type ServiceClient interface { GetSanitizedPaymail(addr string) (*paymail.SanitisedPaymail, error) diff --git a/engine/v2/database/testabilities/fixture_database.go b/engine/v2/database/testabilities/fixture_database.go index 47bab91d4..665909a09 100644 --- a/engine/v2/database/testabilities/fixture_database.go +++ b/engine/v2/database/testabilities/fixture_database.go @@ -45,7 +45,7 @@ type databaseFixture struct { func Given(t testing.TB, opts ...testengine.ConfigOpts) (given DatabaseFixture, cleanup func()) { engineWithConfig, cleanup := testengine.Given(t).EngineWithConfiguration(opts...) - db := engineWithConfig.Engine.Datastore().DB() + db := engineWithConfig.Engine.DB() fixture := &databaseFixture{ t: t, db: db, diff --git a/engine/v2/engine/engine.go b/engine/v2/engine/engine.go new file mode 100644 index 000000000..f3816939a --- /dev/null +++ b/engine/v2/engine/engine.go @@ -0,0 +1,256 @@ +package engine + +import ( + "context" + "errors" + + "github.com/bitcoin-sv/go-paymail/server" + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/engine/chain" + "github.com/bitcoin-sv/spv-wallet/engine/chain/models" + "github.com/bitcoin-sv/spv-wallet/engine/paymail" + "github.com/bitcoin-sv/spv-wallet/engine/spverrors" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/bitcoin-sv/spv-wallet/engine/v2/addresses" + "github.com/bitcoin-sv/spv-wallet/engine/v2/data" + "github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository" + "github.com/bitcoin-sv/spv-wallet/engine/v2/engine/internal" + "github.com/bitcoin-sv/spv-wallet/engine/v2/fee" + "github.com/bitcoin-sv/spv-wallet/engine/v2/operations" + "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails" + "github.com/bitcoin-sv/spv-wallet/engine/v2/paymailserver" + "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/beef" + "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" + "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/record" + "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/txsync" + "github.com/bitcoin-sv/spv-wallet/engine/v2/users" + "github.com/bitcoin-sv/spv-wallet/engine/v2/utils/must" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog" + "gorm.io/gorm" +) + +// V2 is the engine of the wallet, it is creating all needed services, and preparing database connection. +type V2 struct { + cfg *config.AppConfig + storage *internal.Storage + + repositories *repository.All + + chainService chain.Service + + usersService *users.Service + paymailsService *paymails.Service + addressesService *addresses.Service + dataService *data.Service + operationsService *operations.Service + transactionsOutlineService outlines.Service + transactionsRecordService *record.Service + txSyncService *txsync.Service + paymailServerConfig *server.Configuration +} + +// NewEngine creates a new engine.V2 instance. +func NewEngine(cfg *config.AppConfig, logger zerolog.Logger, overridesOpts ...InternalsOverride) *V2 { + logger = logger.With().Int("v", 2).Str("service", "engine").Logger() + + overridesToApply := &overrides{} + for _, opt := range overridesOpts { + opt(overridesToApply) + } + + // Database + storage := internal.NewStorage(cfg, logger) + err := storage.Start() + must.HaveNoErrorf(err, "failed to start wallet storage") + + repos := storage.CreateRepositories() + + // Low level services + var httpClient *resty.Client + if overridesToApply.resty != nil { + httpClient = overridesToApply.resty + } else { + httpClient = resty.New() + if overridesToApply.transport != nil { + httpClient.SetTransport(overridesToApply.transport) + } + } + + paymailClient := setupPaymailClient(overridesToApply, httpClient) + + cache := internal.NewCache(cfg, logger) + + chainService := chain.NewChainService(logger, httpClient, extractARCConfig(cfg), extractBHSConfig(cfg)) + feeService := fee.NewService(cfg, chainService, logger) + + feeUnit, err := feeService.GetFeeUnit(context.Background()) + must.HaveNoErrorf(err, "failed to setup fee unit") + + paymailServiceClient := paymail.NewServiceClient(cache, paymailClient, logger) + + utxoSelector := storage.CreateUTXOSelector(feeService) + + beefService := beef.NewService(repos.Transactions) + + userService := users.NewService(repos.Users, cfg) + paymailService := paymails.NewService(repos.Paymails, userService, cfg) + addressesService := addresses.NewService(repos.Addresses) + dataService := data.NewService(repos.Data) + operationsService := operations.NewService(repos.Operations) + txSyncService := txsync.NewService(logger, repos.Transactions) + + transactionsOutlineService := outlines.NewService( + paymailServiceClient, + paymailService, + beefService, + utxoSelector, + feeUnit, + logger, + userService, + ) + + transactionsRecordService := record.NewService( + logger, + addressesService, + userService, + repos.Outputs, + repos.Operations, + repos.Transactions, + chainService, + paymailServiceClient, + ) + + paymailServiceProvider := paymailserver.NewServiceProvider( + logger, + paymailService, + userService, + addressesService, + chainService, + transactionsRecordService, + ) + + paymailServerConfig := setupPaymailServer(cfg, logger, paymailServiceProvider) + + return &V2{ + cfg: cfg, + storage: storage, + repositories: repos, + chainService: chainService, + usersService: userService, + paymailsService: paymailService, + addressesService: addressesService, + dataService: dataService, + operationsService: operationsService, + transactionsOutlineService: transactionsOutlineService, + transactionsRecordService: transactionsRecordService, + txSyncService: txSyncService, + paymailServerConfig: paymailServerConfig, + } +} + +// Close closes the V2 and all its services +func (e *V2) Close(_ context.Context) error { + var allErrors error + err := e.storage.Close() + if err != nil { + allErrors = errors.Join(allErrors, spverrors.Wrapf(err, "couldn't close storage")) + } + return allErrors +} + +// DB returns the database +// Deprecated: DB used as adapter for engine v1 +func (e *V2) DB() *gorm.DB { + return e.storage.DB() +} + +// Repositories returns all repositories +func (e *V2) Repositories() *repository.All { + return e.repositories +} + +// Chain returns the chain service +func (e *V2) Chain() chain.Service { + return e.chainService +} + +// UsersService returns the users service +func (e *V2) UsersService() *users.Service { + return e.usersService +} + +// PaymailsService returns the paymails service +func (e *V2) PaymailsService() *paymails.Service { + return e.paymailsService +} + +// AddressesService returns the addresses service +func (e *V2) AddressesService() *addresses.Service { + return e.addressesService +} + +// DataService returns the data service +func (e *V2) DataService() *data.Service { + return e.dataService +} + +// OperationsService returns the operations service +func (e *V2) OperationsService() *operations.Service { + return e.operationsService +} + +// TransactionOutlinesService returns the transaction outlines service +func (e *V2) TransactionOutlinesService() outlines.Service { + return e.transactionsOutlineService +} + +// TransactionRecordService returns the transaction record service +func (e *V2) TransactionRecordService() *record.Service { + return e.transactionsRecordService +} + +// TxSyncService returns the tx sync service +func (e *V2) TxSyncService() *txsync.Service { + return e.txSyncService +} + +// PaymailServerConfiguration returns the paymail server configuration +func (e *V2) PaymailServerConfiguration() *server.Configuration { + return e.paymailServerConfig +} + +func extractBHSConfig(cfg *config.AppConfig) chainmodels.BHSConfig { + return chainmodels.BHSConfig{ + URL: cfg.BHS.URL, + AuthToken: cfg.BHS.AuthToken, + } +} + +func extractARCConfig(cfg *config.AppConfig) chainmodels.ARCConfig { + arcCfg := chainmodels.ARCConfig{ + URL: cfg.ARC.URL, + Token: cfg.ARC.Token, + DeploymentID: cfg.ARC.DeploymentID, + WaitFor: cfg.ARC.WaitForStatus, + } + + if cfg.ARC.Callback.Enabled { + var err error + if cfg.ARC.Callback.Token == "" { + // This also sets the token to the config reference and, it is used in the callbacktoken_middleware + cfg.ARC.Callback.Token, err = utils.HashAdler32(config.DefaultAdminXpub) + must.HaveNoErrorf(err, "error while generating callback token") + } + arcCfg.Callback = &chainmodels.ARCCallbackConfig{ + URL: cfg.ARC.Callback.Host + config.BroadcastCallbackRoute, + Token: cfg.ARC.Callback.Token, + } + } + + if cfg.ExperimentalFeatures != nil && cfg.ExperimentalFeatures.UseJunglebus { + arcCfg.UseJunglebus = true + } + + return arcCfg +} diff --git a/engine/v2/engine/engine_overrides.go b/engine/v2/engine/engine_overrides.go new file mode 100644 index 000000000..924c80953 --- /dev/null +++ b/engine/v2/engine/engine_overrides.go @@ -0,0 +1,42 @@ +package engine + +import ( + "net/http" + + "github.com/bitcoin-sv/spv-wallet/engine/paymail" + "github.com/go-resty/resty/v2" +) + +// InternalsOverride is a function that can be used to override internal dependencies. +// This is meant to be used for testing purposes. +type InternalsOverride = func(*overrides) + +type overrides struct { + transport http.RoundTripper + resty *resty.Client + paymailClient paymail.ClientInterface +} + +// WithResty is a function that can be used to override the resty.Client used by the engine. +// This is meant to be used for testing purposes. +func WithResty(resty *resty.Client) InternalsOverride { + return func(o *overrides) { + o.resty = resty + } +} + +// WithTransport is a function that can be used to override the http.RoundTripper used by the engine. +// This is meant to be used for testing purposes. +func WithTransport(transport http.RoundTripper) InternalsOverride { + return func(o *overrides) { + o.transport = transport + } +} + +// WithPaymailClient is a function that can be used to override the paymail.ClientInterface used by the engine. +// This is meant to be used for testing purposes. +func WithPaymailClient(client paymail.ClientInterface) InternalsOverride { + return func(o *overrides) { + o.paymailClient = client + } +} diff --git a/engine/v2/engine/internal/cache.go b/engine/v2/engine/internal/cache.go new file mode 100644 index 000000000..be6eedc1b --- /dev/null +++ b/engine/v2/engine/internal/cache.go @@ -0,0 +1,66 @@ +package internal + +import ( + "context" + "fmt" + "time" + + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/bitcoin-sv/spv-wallet/engine/v2/utils/must" + "github.com/mrz1836/go-cachestore" + "github.com/rs/zerolog" +) + +// Cache is an interface for cache storage. +type Cache interface { + GetModel(ctx context.Context, key string, model interface{}) error + SetModel(ctx context.Context, key string, model interface{}, ttl time.Duration, dependencies ...string) error +} + +type cacheOptions struct { + opts []cachestore.ClientOps +} + +// NewCache creates a new cache storage instance. +func NewCache(cfg *config.AppConfig, logger zerolog.Logger) Cache { + logger.With().Str("service", "cache").Logger() + + options := cacheOptions{make([]cachestore.ClientOps, 0)} + + options.configureLogger(cfg, logger).configureEngine(cfg) + + cache, err := cachestore.NewClient(context.Background(), options.opts...) + must.HaveNoErrorf(err, "failed to create cache storage: %v", err) + + return cache +} + +func (o *cacheOptions) configureLogger(cfg *config.AppConfig, logger zerolog.Logger) *cacheOptions { + cachestoreLogger := logging.CreateGormLoggerAdapter(&logger, "cachestore") + o.opts = append(o.opts, cachestore.WithLogger(cachestoreLogger)) + if logger.GetLevel() == zerolog.DebugLevel || logger.GetLevel() == zerolog.TraceLevel { + o.opts = append(o.opts, cachestore.WithDebugging()) + } + return o +} + +func (o *cacheOptions) configureEngine(cfg *config.AppConfig) *cacheOptions { + if cfg.Cache.Engine == cachestore.Redis { + o.opts = append(o.opts, cachestore.WithRedis(&cachestore.RedisConfig{ + DependencyMode: cfg.Cache.Redis.DependencyMode, + MaxActiveConnections: cfg.Cache.Redis.MaxActiveConnections, + MaxConnectionLifetime: cfg.Cache.Redis.MaxConnectionLifetime, + MaxIdleConnections: cfg.Cache.Redis.MaxIdleConnections, + MaxIdleTimeout: cfg.Cache.Redis.MaxIdleTimeout, + URL: cfg.Cache.Redis.URL, + UseTLS: cfg.Cache.Redis.UseTLS, + })) + } else if cfg.Cache.Engine == cachestore.FreeCache { + o.opts = append(o.opts, cachestore.WithFreeCache()) + } else { + panic(fmt.Sprintf("invalid configuration: unsupported cache engine: %s", cfg.Cache.Engine)) + } + + return o +} diff --git a/engine/v2/engine/internal/storage.go b/engine/v2/engine/internal/storage.go new file mode 100644 index 000000000..3330dfcbb --- /dev/null +++ b/engine/v2/engine/internal/storage.go @@ -0,0 +1,176 @@ +package internal + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/engine/datastore" + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/bitcoin-sv/spv-wallet/engine/spverrors" + "github.com/bitcoin-sv/spv-wallet/engine/v2/database" + "github.com/bitcoin-sv/spv-wallet/engine/v2/database/repository" + "github.com/bitcoin-sv/spv-wallet/engine/v2/fee" + "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines" + "github.com/bitcoin-sv/spv-wallet/engine/v2/transaction/outlines/utxo" + "github.com/bitcoin-sv/spv-wallet/engine/v2/utils/must" + "github.com/rs/zerolog" + "gorm.io/gorm" +) + +// Storage is a struct that holds the database connection. +type Storage struct { + logger zerolog.Logger + config *config.AppConfig + db *gorm.DB +} + +// NewStorage creates a new instance of the storage. +func NewStorage(cfg *config.AppConfig, logger zerolog.Logger) *Storage { + logger = logger.With().Str("subservice", "database").Logger() + + storage := &Storage{ + logger: logger, + config: cfg, + } + + storage.initGormDB() + + return storage +} + +// CreateRepositories creates a new instance of the repositories. +func (s *Storage) CreateRepositories() *repository.All { + must.BeTrue(s.db != nil, "Trying to create repositories on closed database connection") + return repository.NewRepositories(s.db) +} + +// CreateUTXOSelector creates a new instance of the UTXO selector. +func (s *Storage) CreateUTXOSelector(feeService *fee.Service) outlines.UTXOSelector { + must.BeTrue(s.db != nil, "Trying to create utxo selector on closed database connection") + // TODO: pass feeService instead of simply fee + feeUnit, err := feeService.GetFeeUnit(context.Background()) + must.HaveNoErrorf(err, "failed to setup fee unit") + utxoSelector := utxo.NewSelector(s.db, feeUnit) + return utxoSelector +} + +// Start starts the database connection and migrates the database. +func (s *Storage) Start() error { + must.BeTrue(s.db != nil, "Trying to start on closed database connection") + err := s.migrateDatabase() + if err != nil { + return err + } + return nil +} + +// Close closes the database connection. +func (s *Storage) Close() error { + if s.db == nil { + return nil + } + sqlDB, err := s.db.DB() + if err != nil { + return spverrors.Wrapf(err, "failed to close the database connection") + } + err = sqlDB.Close() + return spverrors.Wrapf(err, "failed to close the database connection") +} + +func (s *Storage) initGormDB() { + opts := make([]datastore.ClientOps, 0) + + opts = s.configureLogger(opts) + + opts = s.configureSQL(opts) + + store, err := datastore.NewClient(opts...) + must.HaveNoErrorf(err, "failed to prepare database connection") + s.db = store.DB() +} + +func (s *Storage) configureLogger(opts []datastore.ClientOps) []datastore.ClientOps { + var datastoreLogger *logging.GormLoggerAdapter + loggingLevel := s.logger.GetLevel() + if loggingLevel == zerolog.InfoLevel { + warnLvlLogger := s.logger.Level(zerolog.WarnLevel) + datastoreLogger = logging.CreateGormLoggerAdapter(&warnLvlLogger, "datastore") + } else { + datastoreLogger = logging.CreateGormLoggerAdapter(&s.logger, "datastore") + } + opts = append(opts, datastore.WithLogger(&datastore.DatabaseLogWrapper{GormLoggerInterface: datastoreLogger})) + + if loggingLevel == zerolog.DebugLevel || loggingLevel == zerolog.TraceLevel { + opts = append(opts, datastore.WithDebugging()) + } + return opts +} + +func (s *Storage) configureSQL(options []datastore.ClientOps) []datastore.ClientOps { + // Select the datastore + if s.config.Db.Datastore.Engine == datastore.SQLite { + tablePrefix := s.config.Db.Datastore.TablePrefix + if len(s.config.Db.SQLite.TablePrefix) > 0 { + tablePrefix = s.config.Db.SQLite.TablePrefix + } + options = append(options, datastore.WithSQLite(&datastore.SQLiteConfig{ + CommonConfig: datastore.CommonConfig{ + Debug: s.config.Db.Datastore.Debug, + MaxConnectionIdleTime: s.config.Db.SQLite.MaxConnectionIdleTime, + MaxConnectionTime: s.config.Db.SQLite.MaxConnectionTime, + MaxIdleConnections: s.config.Db.SQLite.MaxIdleConnections, + MaxOpenConnections: s.config.Db.SQLite.MaxOpenConnections, + TablePrefix: tablePrefix, + }, + DatabasePath: s.config.Db.SQLite.DatabasePath, // "" for in memory + Shared: s.config.Db.SQLite.Shared, + ExistingConnection: s.config.Db.SQLite.ExistingConnection, + })) + } else if s.config.Db.Datastore.Engine == datastore.PostgreSQL { + tablePrefix := s.config.Db.Datastore.TablePrefix + if len(s.config.Db.SQL.TablePrefix) > 0 { + tablePrefix = s.config.Db.SQL.TablePrefix + } + + options = append(options, datastore.WithSQL(s.config.Db.Datastore.Engine, []*datastore.SQLConfig{ + { + CommonConfig: datastore.CommonConfig{ + Debug: s.config.Db.Datastore.Debug, + MaxConnectionIdleTime: s.config.Db.SQL.MaxConnectionIdleTime, + MaxConnectionTime: s.config.Db.SQL.MaxConnectionTime, + MaxIdleConnections: s.config.Db.SQL.MaxIdleConnections, + MaxOpenConnections: s.config.Db.SQL.MaxOpenConnections, + TablePrefix: tablePrefix, + }, + Driver: s.config.Db.Datastore.Engine.String(), + Host: s.config.Db.SQL.Host, + Name: s.config.Db.SQL.Name, + Password: s.config.Db.SQL.Password, + Port: s.config.Db.SQL.Port, + TimeZone: s.config.Db.SQL.TimeZone, + TxTimeout: s.config.Db.SQL.TxTimeout, + User: s.config.Db.SQL.User, + SslMode: s.config.Db.SQL.SslMode, + }, + })) + + } else { + panic(spverrors.Newf("invalid configuration: unsupported datastore engine: %s", s.config.Db.Datastore.Engine.String())) + } + + return options +} + +func (s *Storage) migrateDatabase() error { + models := database.Models() + if err := s.db.AutoMigrate(models...); err != nil { + return spverrors.Wrapf(err, "failed to auto-migrate database") + } + return nil +} + +// DB returns the database connection. +// Deprecated: used as adapter for engine v1 +func (s *Storage) DB() *gorm.DB { + return s.db +} diff --git a/engine/v2/engine/paymail.go b/engine/v2/engine/paymail.go new file mode 100644 index 000000000..dcc026a6c --- /dev/null +++ b/engine/v2/engine/paymail.go @@ -0,0 +1,72 @@ +package engine + +import ( + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/go-paymail/server" + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/engine/v2/utils/must" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog" +) + +func setupPaymailClient(overridesToApply *overrides, httpClient *resty.Client) paymail.ClientInterface { + var paymailClient paymail.ClientInterface + if overridesToApply.paymailClient != nil { + paymailClient = overridesToApply.paymailClient + } else { + var err error + paymailClient, err = paymail.NewClient() + must.HaveNoErrorf(err, "failed to setup paymail client") + paymailClient.WithCustomHTTPClient(httpClient) + } + return paymailClient +} + +func setupPaymailServer(cfg *config.AppConfig, logger zerolog.Logger, serviceProvider server.PaymailServiceProvider) *server.Configuration { + pmCfg := cfg.Paymail + + logger = logger.With().Str("service", "paymail-server").Logger() + + if !pmCfg.Beef.Enabled() { + logger.Warn().Msg("In V2, BEEF capability cannot be disabled") + } + options := []server.ConfigOps{ + server.WithP2PCapabilities(), + server.WithBeefCapabilities(), + } + + for _, domain := range pmCfg.Domains { + options = append(options, server.WithDomain(domain)) + } + + if pmCfg.SenderValidationEnabled { + options = append(options, server.WithSenderValidation()) + } + + if !pmCfg.DomainValidationEnabled { + options = append(options, server.WithDomainValidationDisabled()) + } + + if cfg.ExperimentalFeatures.PikeContactsEnabled { + logger.Warn().Msg("In V2, Pike Payment is not yet supported") + } + + if cfg.ExperimentalFeatures.PikePaymentEnabled { + logger.Warn().Msg("In V2, Pike Payment is not yet supported") + } + + paymailLogger := logger.With().Str("subservice", "go-paymail").Logger() + options = append(options, server.WithLogger(&paymailLogger)) + + paymailLocator := &server.PaymailServiceLocator{} + + paymailLocator.RegisterPaymailService(serviceProvider) + + configuration, err := server.NewConfig( + paymailLocator, + options..., + ) + must.HaveNoErrorf(err, "failed to setup paymail server") + + return configuration +} diff --git a/engine/v2/fee/fee_service.go b/engine/v2/fee/fee_service.go new file mode 100644 index 000000000..bb688b6ea --- /dev/null +++ b/engine/v2/fee/fee_service.go @@ -0,0 +1,75 @@ +package fee + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/config" + "github.com/bitcoin-sv/spv-wallet/conv" + "github.com/bitcoin-sv/spv-wallet/engine/spverrors" + "github.com/bitcoin-sv/spv-wallet/engine/v2/utils/must" + "github.com/bitcoin-sv/spv-wallet/models/bsv" + "github.com/bitcoin-sv/spv-wallet/models/optional" + "github.com/rs/zerolog" + "github.com/samber/lo" +) + +// Provider is an interface that provides fee units from miners. +type Provider interface { + GetFeeUnit(ctx context.Context) (*bsv.FeeUnit, error) +} + +// Service is a fee service that provides fee units for transactions. +type Service struct { + logger zerolog.Logger + feeUnit optional.Param[bsv.FeeUnit] + feeProvider Provider +} + +// NewService creates a new fee service. +func NewService(cfg *config.AppConfig, feeProvider Provider, logger zerolog.Logger) *Service { + must.BeTrue(cfg != nil, "config is required") + must.BeTrue(feeProvider != nil, "feeProvider is required") + logger = logger.With().Str("service", "fee").Logger() + + return &Service{ + feeUnit: lo.IfF(cfg.CustomFeeUnit != nil, + func() optional.Param[bsv.FeeUnit] { + satoshis, err := conv.IntToUint64(cfg.CustomFeeUnit.Satoshis) + must.HaveNoErrorf(err, "error converting custom fee unit %d satoshis", cfg.CustomFeeUnit.Satoshis) + logger.Log(). + Int("satoshis", cfg.CustomFeeUnit.Satoshis). + Int("bytes", cfg.CustomFeeUnit.Bytes). + Msg("Fee unit found in configuration, using custom fee unit") + + return &bsv.FeeUnit{ + Satoshis: bsv.Satoshis(satoshis), + Bytes: cfg.CustomFeeUnit.Bytes, + } + }). + Else(nil), + feeProvider: feeProvider, + } +} + +// GetFeeUnit returns the fee unit that should be used for transactions. +func (s *Service) GetFeeUnit(ctx context.Context) (bsv.FeeUnit, error) { + if s.feeUnit != nil { + return *s.feeUnit, nil + } + + s.logger.Debug().Msg("Fee unit not found in config, will try to receive it from miners") + + feeUnit, err := s.feeProvider.GetFeeUnit(ctx) + if err != nil { + return bsv.FeeUnit{}, spverrors.Wrapf(err, "failed to get fee unit from miners") + } + if feeUnit == nil { + return bsv.FeeUnit{}, spverrors.Newf("received empty fee unit from miners") + } + s.feeUnit = feeUnit + + s.logger.Log().Any("satoshis", feeUnit.Satoshis).Int("bytes", feeUnit.Bytes). + Msg("Received fee unit from miners, will use it from now on") + + return *feeUnit, nil +} diff --git a/engine/v2/paymailserver/paymail_service_provider.go b/engine/v2/paymailserver/paymail_service_provider.go index 6d55a4dfb..3998f6c05 100644 --- a/engine/v2/paymailserver/paymail_service_provider.go +++ b/engine/v2/paymailserver/paymail_service_provider.go @@ -22,15 +22,16 @@ import ( // NewServiceProvider create a new paymail service server which handlers incoming paymail requests func NewServiceProvider( - logger *zerolog.Logger, + logger zerolog.Logger, paymails paymail.PaymailsService, users paymail.UsersService, addresses paymail.AddressesService, spv paymail.MerkleRootsVerifier, recorder paymail.TxRecorder, ) server.PaymailServiceProvider { + logger = logger.With().Str("subservice", "paymail-service-provider").Logger() return &serviceProvider{ - logger: logger, + logger: &logger, paymails: paymails, users: users, addresses: addresses, diff --git a/engine/v2/utils/must/must.go b/engine/v2/utils/must/must.go new file mode 100644 index 000000000..7b28c7c27 --- /dev/null +++ b/engine/v2/utils/must/must.go @@ -0,0 +1,30 @@ +// Package must provides a simple way to panic if an error is not nil. +// It should be used only in initialisation phase, +// in places where error is most probably problem with the code itself. +package must + +import ( + "fmt" +) + +// HaveNoError panics if the error is not nil. +func HaveNoError(err error) { + if err != nil { + panic(err) + } +} + +// HaveNoErrorf panics if the error is not nil, wrapping error with a message. +func HaveNoErrorf(err error, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + if err != nil { + panic(fmt.Errorf("%s, caused by: %w", msg, err)) + } +} + +// BeTrue is a simple way to panic if the condition is false. +func BeTrue(condition bool, format string, args ...interface{}) { + if !condition { + panic(fmt.Sprintf(format, args...)) + } +} diff --git a/initializer/config_to_options.go b/initializer/config_to_options.go index 28b60e150..f116315b8 100644 --- a/initializer/config_to_options.go +++ b/initializer/config_to_options.go @@ -249,9 +249,6 @@ func addPaymailOpts(c *config.AppConfig, options []engine.ClientOps) []engine.Cl if c.ExperimentalFeatures.PikePaymentEnabled { options = append(options, engine.WithPaymailPikePaymentSupport()) } - if c.ExperimentalFeatures.V2 { - options = append(options, engine.WithPaymailExperimentalNewTransactionFlow()) - } return options } diff --git a/server/server.go b/server/server.go index 7df5996c4..00eaa739f 100644 --- a/server/server.go +++ b/server/server.go @@ -109,11 +109,12 @@ func (s *Server) Handlers() *gin.Engine { func setupServerRoutes(appConfig *config.AppConfig, spvWalletEngine engine.ClientInterface, ginEngine *gin.Engine, log *zerolog.Logger) { handlersManager := handlers.NewManager(ginEngine, appConfig) - actions.Register(handlersManager) - paymailserver.Register(spvWalletEngine.GetPaymailConfig().Configuration, ginEngine) - if appConfig.ExperimentalFeatures.V2 { - v2.RegisterNonOpenAPIRoutes(handlersManager) + if !appConfig.ExperimentalFeatures.V2 { + paymailserver.Register(spvWalletEngine.GetPaymailConfig().Configuration, ginEngine) + actions.Register(handlersManager) + } else { + v2.RegisterNonOpenAPIRoutes(ginEngine, appConfig, spvWalletEngine) api.RegisterHandlersWithOptions(ginEngine, v2.NewV2API(appConfig, spvWalletEngine, log), api.GinServerOptions{ BaseURL: "", Middlewares: []api.MiddlewareFunc{