From e9d4452fccff0e313d2a87f7c9f9be6d2350ed0b Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:23:47 +0100 Subject: [PATCH 01/12] fix(BUX-461): chainstate initialization for broadcast-client with feeQuotes --- chainstate/broadcast_client_init.go | 46 +++++ chainstate/client.go | 146 ++++----------- chainstate/client_internal.go | 68 ------- chainstate/client_options.go | 18 +- chainstate/client_test.go | 26 +-- chainstate/definitions.go | 8 +- chainstate/interface.go | 10 +- chainstate/minercraft_init.go | 179 +++++++++++++++++++ chainstate/mock_minercraft.go | 2 +- client_options.go | 4 +- examples/client/custom_rates/custom_rates.go | 8 - mock_chainstate_test.go | 2 +- model_draft_transactions.go | 2 +- model_draft_transactions_test.go | 6 +- model_transaction_config_test.go | 2 +- utils/fees.go | 22 +++ utils/fees_test.go | 55 ++++++ 17 files changed, 358 insertions(+), 246 deletions(-) create mode 100644 chainstate/broadcast_client_init.go delete mode 100644 chainstate/client_internal.go create mode 100644 chainstate/minercraft_init.go diff --git a/chainstate/broadcast_client_init.go b/chainstate/broadcast_client_init.go new file mode 100644 index 00000000..a6926373 --- /dev/null +++ b/chainstate/broadcast_client_init.go @@ -0,0 +1,46 @@ +package chainstate + +import ( + "context" + "errors" + + "github.com/BuxOrg/bux/utils" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/newrelic/go-agent/v3/newrelic" +) + +func (c *Client) broadcastClientInit(ctx context.Context) (feeUnit *utils.FeeUnit, err error) { + if txn := newrelic.FromContext(ctx); txn != nil { + defer txn.StartSegment("start_broadcast_client").End() + } + + bc := c.options.config.broadcastClient + if bc == nil { + err = errors.New("broadcast client is not configured") + return + } + + feeUnit = DefaultFee() + if c.isFeeQuotesEnabled() { + // get the lowest fee + var feeQuotes []*broadcast.FeeQuote + feeQuotes, err = bc.GetFeeQuote(ctx) + if err != nil { + return + } + if len(feeQuotes) == 0 { + c.options.logger.Warn().Msg("no fee quotes returned from broadcast client") + } + fees := make([]utils.FeeUnit, len(feeQuotes)) + for index, fee := range feeQuotes { + fees[index] = utils.FeeUnit{ + Satoshis: int(fee.MiningFee.Satoshis), + Bytes: int(fee.MiningFee.Bytes), + } + } + lowest := utils.LowestFee(fees, *DefaultFee()) + feeUnit = &lowest + } + + return +} diff --git a/chainstate/client.go b/chainstate/client.go index e73bdcde..d97c19ef 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -2,17 +2,14 @@ package chainstate import ( "context" - "sync" "time" "github.com/BuxOrg/bux/logging" "github.com/BuxOrg/bux/utils" "github.com/bitcoin-sv/go-broadcast-client/broadcast" - "github.com/libsv/go-bt/v2" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" "github.com/tonicpow/go-minercraft/v2" - "github.com/tonicpow/go-minercraft/v2/apis/mapi" ) type ( @@ -42,16 +39,17 @@ type ( queryTimeout time.Duration // Timeout for transaction query broadcastClient broadcast.Client // Broadcast client pulseClient *PulseClient // Pulse client + feeUnit *utils.FeeUnit // The lowest fees among all miners + feeQuotes bool // If set, feeUnit will be updated with fee quotes from miner's } // minercraftConfig is specific for minercraft configuration minercraftConfig struct { - broadcastMiners []*Miner // List of loaded miners for broadcasting - queryMiners []*Miner // List of loaded miners for querying transactions - feeUnit *utils.FeeUnit // The lowest fees among all miners - minercraftFeeQuotes bool // If set, feeUnit will be updated with fee quotes from miner's mAPI - apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) - minerAPIs []*minercraft.MinerAPIs // List of miners APIs + broadcastMiners []*Miner // List of loaded miners for broadcasting + queryMiners []*Miner // List of loaded miners for querying transactions + + apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) + minerAPIs []*minercraft.MinerAPIs // List of miners APIs } // Miner is the internal chainstate miner (wraps Minercraft miner with more information) @@ -89,12 +87,29 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) client.options.logger = logging.GetDefaultLogger() } - if client.ActiveProvider() == ProviderMinercraft { - if err := client.startMinerCraft(ctx); err != nil { - return nil, err - } + // Init active provider + var feeUnit *utils.FeeUnit + var err error + switch client.ActiveProvider() { + case ProviderMinercraft: + feeUnit, err = client.minercraftInit(ctx) + case ProviderBroadcastClient: + feeUnit, err = client.broadcastClientInit(ctx) + } + + if err != nil { + return nil, err } + // Set fee unit + if feeUnit == nil { + feeUnit = DefaultFee() + client.options.logger.Info().Msgf("no fee unit found, using default: %s", feeUnit) + } else { + client.options.logger.Info().Msgf("using fee unit: %s", feeUnit) + } + client.options.config.feeUnit = feeUnit + // Return the client return client, nil } @@ -169,112 +184,13 @@ func (c *Client) QueryTimeout() time.Duration { return c.options.config.queryTimeout } -// BroadcastMiners will return the broadcast miners -func (c *Client) BroadcastMiners() []*Miner { - return c.options.config.minercraftConfig.broadcastMiners -} - -// QueryMiners will return the query miners -func (c *Client) QueryMiners() []*Miner { - return c.options.config.minercraftConfig.queryMiners -} - // FeeUnit will return feeUnit func (c *Client) FeeUnit() *utils.FeeUnit { - return c.options.config.minercraftConfig.feeUnit + return c.options.config.feeUnit } -func (c *Client) isMinercraftFeeQuotesEnabled() bool { - return c.options.config.minercraftConfig.minercraftFeeQuotes -} - -// ValidateMiners will check if miner is reacheble by requesting its FeeQuote -// If there was on error on FeeQuote(), the miner will be deleted from miners list -// If usage of MapiFeeQuotes is enabled and miner is reacheble, miner's fee unit will be upadeted with MAPI fee quotes -// If FeeQuote returns some quote, but fee is not presented in it, it means that miner is valid but we can't use it's feequote -func (c *Client) ValidateMiners(ctx context.Context) { - ctxWithCancel, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - var wg sync.WaitGroup - // Loop all broadcast miners - for index := range c.options.config.minercraftConfig.broadcastMiners { - wg.Add(1) - go func( - ctx context.Context, client *Client, - wg *sync.WaitGroup, miner *Miner, - ) { - defer wg.Done() - // Get the fee quote using the miner - // Switched from policyQuote to feeQuote as gorillapool doesn't have such endpoint - var fee *bt.Fee - if c.Minercraft().APIType() == minercraft.MAPI { - quote, err := c.Minercraft().FeeQuote(ctx, miner.Miner) - if err != nil { - client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) - miner.FeeUnit = nil - return - } - - fee = quote.Quote.GetFee(mapi.FeeTypeData) - if fee == nil { - client.options.logger.Error().Msgf("Fee is missing in %s's FeeQuote response", miner.Miner.Name) - return - } - // Arc doesn't support FeeQuote right now(2023.07.21), that's why PolicyQuote is used - } else if c.Minercraft().APIType() == minercraft.Arc { - quote, err := c.Minercraft().PolicyQuote(ctx, miner.Miner) - if err != nil { - client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) - miner.FeeUnit = nil - return - } - - fee = quote.Quote.Fees[0] - } - if c.isMinercraftFeeQuotesEnabled() { - miner.FeeUnit = &utils.FeeUnit{ - Satoshis: fee.MiningFee.Satoshis, - Bytes: fee.MiningFee.Bytes, - } - miner.FeeLastChecked = time.Now().UTC() - } - }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.broadcastMiners[index]) - } - wg.Wait() - - c.DeleteUnreacheableMiners() - - if c.isMinercraftFeeQuotesEnabled() { - c.SetLowestFees() - } -} - -// SetLowestFees takes the lowest fees among all miners and sets them as the feeUnit for future transactions -func (c *Client) SetLowestFees() { - minFees := DefaultFee - for _, m := range c.options.config.minercraftConfig.broadcastMiners { - if float64(minFees.Satoshis)/float64(minFees.Bytes) > float64(m.FeeUnit.Satoshis)/float64(m.FeeUnit.Bytes) { - minFees = m.FeeUnit - } - } - c.options.config.minercraftConfig.feeUnit = minFees -} - -// DeleteUnreacheableMiners deletes miners which can't be reacheable from config -func (c *Client) DeleteUnreacheableMiners() { - validMinerIndex := 0 - for _, miner := range c.options.config.minercraftConfig.broadcastMiners { - if miner.FeeUnit != nil { - c.options.config.minercraftConfig.broadcastMiners[validMinerIndex] = miner - validMinerIndex++ - } - } - // Prevent memory leak by erasing truncated miners - for i := validMinerIndex; i < len(c.options.config.minercraftConfig.broadcastMiners); i++ { - c.options.config.minercraftConfig.broadcastMiners[i] = nil - } - c.options.config.minercraftConfig.broadcastMiners = c.options.config.minercraftConfig.broadcastMiners[:validMinerIndex] +func (c *Client) isFeeQuotesEnabled() bool { + return c.options.config.feeQuotes } // ActiveProvider returns a name of a provider based on config. diff --git a/chainstate/client_internal.go b/chainstate/client_internal.go deleted file mode 100644 index b6863794..00000000 --- a/chainstate/client_internal.go +++ /dev/null @@ -1,68 +0,0 @@ -package chainstate - -import ( - "context" - - "github.com/BuxOrg/bux/utils" - "github.com/newrelic/go-agent/v3/newrelic" - "github.com/tonicpow/go-minercraft/v2" -) - -// defaultMinercraftOptions will create the defaults -func (c *Client) defaultMinercraftOptions() (opts *minercraft.ClientOptions) { - opts = minercraft.DefaultClientOptions() - if len(c.options.userAgent) > 0 { - opts.UserAgent = c.options.userAgent - } - return -} - -// startMinerCraft will start Minercraft (if no custom client is found) -func (c *Client) startMinerCraft(ctx context.Context) (err error) { - if txn := newrelic.FromContext(ctx); txn != nil { - defer txn.StartSegment("start_minercraft").End() - } - - // No client? - if c.Minercraft() == nil { - var optionalMiners []*minercraft.Miner - var loadedMiners []string - - // Loop all broadcast miners and append to the list of miners - for i := range c.options.config.minercraftConfig.broadcastMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID) - } - } - - // Loop all query miners and append to the list of miners - for i := range c.options.config.minercraftConfig.queryMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.queryMiners[i].Miner) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID) - } - } - c.options.config.minercraft, err = minercraft.NewClient( - c.defaultMinercraftOptions(), - c.HTTPClient(), - c.options.config.minercraftConfig.apiType, - optionalMiners, - c.options.config.minercraftConfig.minerAPIs, - ) - } - - c.ValidateMiners(ctx) - - // Check for broadcast miners - if len(c.BroadcastMiners()) == 0 { - return ErrMissingBroadcastMiners - } - - // Check for query miners - if len(c.QueryMiners()) == 0 { - return ErrMissingQueryMiners - } - - return nil -} diff --git a/chainstate/client_options.go b/chainstate/client_options.go index 551cca2f..06d95a38 100644 --- a/chainstate/client_options.go +++ b/chainstate/client_options.go @@ -27,16 +27,16 @@ func defaultClientOptions() *clientOptions { config: &syncConfig{ httpClient: nil, minercraftConfig: &minercraftConfig{ - broadcastMiners: bm, - queryMiners: qm, - minerAPIs: apis, - minercraftFeeQuotes: true, - feeUnit: DefaultFee, + broadcastMiners: bm, + queryMiners: qm, + minerAPIs: apis, }, minercraft: nil, network: MainNet, queryTimeout: defaultQueryTimeOut, broadcastClient: nil, + feeQuotes: true, + feeUnit: DefaultFee(), }, debug: false, newRelicEnabled: false, @@ -52,7 +52,7 @@ func defaultMiners() (broadcastMiners []*Miner, queryMiners []*Miner) { for index, miner := range miners { broadcastMiners = append(broadcastMiners, &Miner{ FeeLastChecked: time.Now().UTC(), - FeeUnit: DefaultFee, + FeeUnit: DefaultFee(), Miner: miners[index], }) @@ -209,10 +209,10 @@ func WithExcludedProviders(providers []string) ClientOps { } } -// WithMinercraftFeeQuotes will set minercraftFeeQuotes flag as true -func WithMinercraftFeeQuotes() ClientOps { +// WithFeeQuotes will set minercraftFeeQuotes flag as true +func WithFeeQuotes() ClientOps { return func(c *clientOptions) { - c.config.minercraftConfig.minercraftFeeQuotes = true + c.config.feeQuotes = true } } diff --git a/chainstate/client_test.go b/chainstate/client_test.go index 5f1cbbe1..e766b9ad 100644 --- a/chainstate/client_test.go +++ b/chainstate/client_test.go @@ -46,7 +46,7 @@ func TestNewClient(t *testing.T) { t.Run("custom broadcast client", func(t *testing.T) { arcConfig := broadcast_client.ArcClientConfig{ Token: "", - APIUrl: "https://tapi.taal.com/arc", + APIUrl: "https://arc.gorillapool.io", } logger := zerolog.Nop() customClient := broadcast_client.Builder().WithArc(arcConfig, &logger).Build() @@ -80,30 +80,6 @@ func TestNewClient(t *testing.T) { assert.Equal(t, customClient, c.Minercraft()) }) - t.Run("custom list of broadcast miners", func(t *testing.T) { - miners, _ := defaultMiners() - c, err := NewClient( - context.Background(), - WithBroadcastMiners(miners), - WithMinercraft(&MinerCraftBase{}), - ) - require.NoError(t, err) - require.NotNil(t, c) - assert.Equal(t, miners, c.BroadcastMiners()) - }) - - t.Run("custom list of query miners", func(t *testing.T) { - miners, _ := defaultMiners() - c, err := NewClient( - context.Background(), - WithQueryMiners(miners), - WithMinercraft(&MinerCraftBase{}), - ) - require.NoError(t, err) - require.NotNil(t, c) - assert.Equal(t, miners, c.QueryMiners()) - }) - t.Run("custom query timeout", func(t *testing.T) { timeout := 55 * time.Second c, err := NewClient( diff --git a/chainstate/definitions.go b/chainstate/definitions.go index c2a6f2d4..072016d0 100644 --- a/chainstate/definitions.go +++ b/chainstate/definitions.go @@ -53,9 +53,11 @@ const ( // DefaultFee is used when a fee has not been set by the user // This default is currently accepted by all BitcoinSV miners (50/1000) (7.27.23) // Actual TAAL FeeUnit - 1/1000, GorillaPool - 50/1000 (7.27.23) -var DefaultFee = &utils.FeeUnit{ - Satoshis: 1, - Bytes: 20, +func DefaultFee() *utils.FeeUnit { + return &utils.FeeUnit{ + Satoshis: 1, + Bytes: 20, + } } // BlockInfo is the response info about a returned block diff --git a/chainstate/interface.go b/chainstate/interface.go index 3828f2cd..555fec4a 100644 --- a/chainstate/interface.go +++ b/chainstate/interface.go @@ -35,14 +35,6 @@ type ProviderServices interface { BroadcastClient() broadcast.Client } -// MinercraftServices is the minercraft services interface -type MinercraftServices interface { - BroadcastMiners() []*Miner - QueryMiners() []*Miner - ValidateMiners(ctx context.Context) - FeeUnit() *utils.FeeUnit -} - // HeaderService is header services interface type HeaderService interface { VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRootConfirmationRequestItem) error @@ -52,7 +44,6 @@ type HeaderService interface { type ClientInterface interface { ChainService ProviderServices - MinercraftServices HeaderService Close(ctx context.Context) Debug(on bool) @@ -63,6 +54,7 @@ type ClientInterface interface { Monitor() MonitorService Network() Network QueryTimeout() time.Duration + FeeUnit() *utils.FeeUnit } // MonitorClient interface diff --git a/chainstate/minercraft_init.go b/chainstate/minercraft_init.go new file mode 100644 index 00000000..d5a45019 --- /dev/null +++ b/chainstate/minercraft_init.go @@ -0,0 +1,179 @@ +package chainstate + +import ( + "context" + "sync" + "time" + + "github.com/BuxOrg/bux/utils" + "github.com/libsv/go-bt/v2" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/tonicpow/go-minercraft/v2" + "github.com/tonicpow/go-minercraft/v2/apis/mapi" +) + +func (c *Client) minercraftInit(ctx context.Context) (feeUnit *utils.FeeUnit, err error) { + if txn := newrelic.FromContext(ctx); txn != nil { + defer txn.StartSegment("start_minercraft").End() + } + mi := &minercraftInitializer{client: c, ctx: ctx} + + if err = mi.newClient(); err != nil { + return + } + + if err = mi.validateMiners(); err != nil { + return + } + + if c.isFeeQuotesEnabled() { + feeUnit = mi.lowestFee() + } else { + feeUnit = DefaultFee() + } + + return +} + +type minercraftInitializer struct { + client *Client + ctx context.Context +} + +func (i *minercraftInitializer) defaultMinercraftOptions() (opts *minercraft.ClientOptions) { + c := i.client + opts = minercraft.DefaultClientOptions() + if len(c.options.userAgent) > 0 { + opts.UserAgent = c.options.userAgent + } + return +} + +func (i *minercraftInitializer) newClient() (err error) { + c := i.client + // No client? + if c.Minercraft() == nil { + var optionalMiners []*minercraft.Miner + var loadedMiners []string + + // Loop all broadcast miners and append to the list of miners + for i := range c.options.config.minercraftConfig.broadcastMiners { + if !utils.StringInSlice(c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner) + loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID) + } + } + + // Loop all query miners and append to the list of miners + for i := range c.options.config.minercraftConfig.queryMiners { + if !utils.StringInSlice(c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.queryMiners[i].Miner) + loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID) + } + } + c.options.config.minercraft, err = minercraft.NewClient( + i.defaultMinercraftOptions(), + c.HTTPClient(), + c.options.config.minercraftConfig.apiType, + optionalMiners, + c.options.config.minercraftConfig.minerAPIs, + ) + } + return +} + +// validateMiners will check if miner is reacheble by requesting its FeeQuote +// If there was on error on FeeQuote(), the miner will be deleted from miners list +// If usage of MapiFeeQuotes is enabled and miner is reacheble, miner's fee unit will be upadeted with MAPI fee quotes +// If FeeQuote returns some quote, but fee is not presented in it, it means that miner is valid but we can't use it's feequote +func (i *minercraftInitializer) validateMiners() error { + ctxWithCancel, cancel := context.WithTimeout(i.ctx, 5*time.Second) + defer cancel() + + c := i.client + var wg sync.WaitGroup + // Loop all broadcast miners + for index := range c.options.config.minercraftConfig.broadcastMiners { + wg.Add(1) + go func( + ctx context.Context, client *Client, + wg *sync.WaitGroup, miner *Miner, + ) { + defer wg.Done() + // Get the fee quote using the miner + // Switched from policyQuote to feeQuote as gorillapool doesn't have such endpoint + var fee *bt.Fee + if c.Minercraft().APIType() == minercraft.MAPI { + quote, err := c.Minercraft().FeeQuote(ctx, miner.Miner) + if err != nil { + client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) + miner.FeeUnit = nil + return + } + + fee = quote.Quote.GetFee(mapi.FeeTypeData) + if fee == nil { + client.options.logger.Error().Msgf("Fee is missing in %s's FeeQuote response", miner.Miner.Name) + return + } + // Arc doesn't support FeeQuote right now(2023.07.21), that's why PolicyQuote is used + } else if c.Minercraft().APIType() == minercraft.Arc { + quote, err := c.Minercraft().PolicyQuote(ctx, miner.Miner) + if err != nil { + client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) + miner.FeeUnit = nil + return + } + + fee = quote.Quote.Fees[0] + } + if c.isFeeQuotesEnabled() { + miner.FeeUnit = &utils.FeeUnit{ + Satoshis: fee.MiningFee.Satoshis, + Bytes: fee.MiningFee.Bytes, + } + miner.FeeLastChecked = time.Now().UTC() + } + }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.broadcastMiners[index]) + } + wg.Wait() + + i.deleteUnreacheableMiners() + + switch { + case len(c.options.config.minercraftConfig.broadcastMiners) == 0: + return ErrMissingBroadcastMiners + case len(c.options.config.minercraftConfig.queryMiners) == 0: + return ErrMissingQueryMiners + default: + return nil + } +} + +// deleteUnreacheableMiners deletes miners which can't be reacheable from config +func (i *minercraftInitializer) deleteUnreacheableMiners() { + c := i.client + validMinerIndex := 0 + for _, miner := range c.options.config.minercraftConfig.broadcastMiners { + if miner.FeeUnit != nil { + c.options.config.minercraftConfig.broadcastMiners[validMinerIndex] = miner + validMinerIndex++ + } + } + // Prevent memory leak by erasing truncated miners + for i := validMinerIndex; i < len(c.options.config.minercraftConfig.broadcastMiners); i++ { + c.options.config.minercraftConfig.broadcastMiners[i] = nil + } + c.options.config.minercraftConfig.broadcastMiners = c.options.config.minercraftConfig.broadcastMiners[:validMinerIndex] +} + +// lowestFees takes the lowest fees among all miners and sets them as the feeUnit for future transactions +func (i *minercraftInitializer) lowestFee() *utils.FeeUnit { + miners := i.client.options.config.minercraftConfig.broadcastMiners + fees := make([]utils.FeeUnit, len(miners)) + for index, miner := range miners { + fees[index] = *miner.FeeUnit + } + lowest := utils.LowestFee(fees, *DefaultFee()) + return &lowest +} diff --git a/chainstate/mock_minercraft.go b/chainstate/mock_minercraft.go index c9f86684..3e1aa033 100644 --- a/chainstate/mock_minercraft.go +++ b/chainstate/mock_minercraft.go @@ -128,7 +128,7 @@ func (m *MinerCraftBase) FeeQuote(context.Context, *minercraft.Miner) (*minercra Fees: []*bt.Fee{ { FeeType: bt.FeeTypeData, - MiningFee: bt.FeeUnit(*DefaultFee), + MiningFee: bt.FeeUnit(*DefaultFee()), }, }, }, diff --git a/client_options.go b/client_options.go index c9c0f826..591d7d53 100644 --- a/client_options.go +++ b/client_options.go @@ -664,9 +664,9 @@ func WithCustomNotifications(customNotifications notifications.ClientInterface) } // WithMinercraftFeeQuotes will set usage of minercraft's fee quotes instead of default fees -func WithMinercraftFeeQuotes() ClientOps { +func WithFeeQuotes() ClientOps { return func(c *clientOptions) { - c.chainstate.options = append(c.chainstate.options, chainstate.WithMinercraftFeeQuotes()) + c.chainstate.options = append(c.chainstate.options, chainstate.WithFeeQuotes()) } } diff --git a/examples/client/custom_rates/custom_rates.go b/examples/client/custom_rates/custom_rates.go index 9593fc3d..65f9a46f 100644 --- a/examples/client/custom_rates/custom_rates.go +++ b/examples/client/custom_rates/custom_rates.go @@ -46,14 +46,6 @@ func main() { _ = client.Close(context.Background()) }() - // Get the miners - broadcastMiners := client.Chainstate().BroadcastMiners() - for _, miner := range broadcastMiners { - log.Println("miner", miner.Miner) - log.Println("fee", miner.FeeUnit) - log.Println("last_checked", miner.FeeLastChecked.String()) - } - // Create an xPub var xpub *bux.Xpub if xpub, err = client.NewXpub( diff --git a/mock_chainstate_test.go b/mock_chainstate_test.go index 603fe9af..aad1d641 100644 --- a/mock_chainstate_test.go +++ b/mock_chainstate_test.go @@ -161,7 +161,7 @@ func (c *chainStateEverythingOnChain) QueryTransactionFastest(_ context.Context, } func (c *chainStateEverythingOnChain) FeeUnit() *utils.FeeUnit { - return chainstate.DefaultFee + return chainstate.DefaultFee() } func (c *chainStateEverythingOnChain) VerifyMerkleRoots(_ context.Context, _ []chainstate.MerkleRootConfirmationRequestItem) error { diff --git a/model_draft_transactions.go b/model_draft_transactions.go index 026fce6b..cf4c249c 100644 --- a/model_draft_transactions.go +++ b/model_draft_transactions.go @@ -68,7 +68,7 @@ func newDraftTransaction(rawXpubKey string, config *TransactionConfig, opts ...M if c := draft.Client(); c != nil { draft.Configuration.FeeUnit = c.Chainstate().FeeUnit() } else { - draft.Configuration.FeeUnit = chainstate.DefaultFee + draft.Configuration.FeeUnit = chainstate.DefaultFee() } } return draft diff --git a/model_draft_transactions_test.go b/model_draft_transactions_test.go index 2f64f914..7a4521a9 100644 --- a/model_draft_transactions_test.go +++ b/model_draft_transactions_test.go @@ -53,7 +53,7 @@ func TestDraftTransaction_newDraftTransaction(t *testing.T) { assert.WithinDurationf(t, expires, draftTx.ExpiresAt, 1*time.Second, "within 1 second") assert.Equal(t, DraftStatusDraft, draftTx.Status) assert.Equal(t, testXPubID, draftTx.XpubID) - assert.Equal(t, chainstate.DefaultFee, draftTx.Configuration.FeeUnit) + assert.Equal(t, *chainstate.DefaultFee(), *draftTx.Configuration.FeeUnit) }) } @@ -272,7 +272,7 @@ func TestDraftTransaction_createTransaction(t *testing.T) { assert.Equal(t, uint64(98988), draftTransaction.Configuration.ChangeSatoshis) assert.Equal(t, uint64(12), draftTransaction.Configuration.Fee) - assert.Equal(t, chainstate.DefaultFee, draftTransaction.Configuration.FeeUnit) + assert.Equal(t, *chainstate.DefaultFee(), *draftTransaction.Configuration.FeeUnit) assert.Equal(t, 1, len(draftTransaction.Configuration.Inputs)) assert.Equal(t, testLockingScript, draftTransaction.Configuration.Inputs[0].ScriptPubKey) @@ -1398,7 +1398,7 @@ func TestDraftTransaction_estimateFees(t *testing.T) { b, _ := json.Marshal(in["feeUnit"]) _ = json.Unmarshal(b, &feeUnit) } else { - feeUnit = chainstate.DefaultFee + feeUnit = chainstate.DefaultFee() } draftTransaction, tx, err2 := createDraftTransactionFromHex(in["hex"].(string), in["inputs"].([]interface{}), feeUnit) require.NoError(t, err2) diff --git a/model_transaction_config_test.go b/model_transaction_config_test.go index 07b48d67..a4b5eba1 100644 --- a/model_transaction_config_test.go +++ b/model_transaction_config_test.go @@ -47,7 +47,7 @@ var ( ChangeSatoshis: 124, ExpiresIn: defaultDraftTxExpiresIn, Fee: 12, - FeeUnit: chainstate.DefaultFee, + FeeUnit: chainstate.DefaultFee(), Inputs: nil, Outputs: nil, } diff --git a/utils/fees.go b/utils/fees.go index 0d941eb7..ab0aa5f6 100644 --- a/utils/fees.go +++ b/utils/fees.go @@ -2,6 +2,7 @@ package utils import ( "encoding/hex" + "fmt" "github.com/libsv/go-bt/v2" ) @@ -9,6 +10,27 @@ import ( // FeeUnit fee unit imported from go-bt/v2 type FeeUnit bt.FeeUnit +// IsLowerThan compare two fee units +func (f *FeeUnit) IsLowerThan(other *FeeUnit) bool { + return float64(f.Satoshis)/float64(f.Bytes) < float64(other.Satoshis)/float64(other.Bytes) +} + +// String returns the fee unit as a string +func (f *FeeUnit) String() string { + return fmt.Sprintf("FeeUnit(%d satoshis / %d bytes)", f.Satoshis, f.Bytes) +} + +// LowestFee get the lowest fee from a list of fee units AND a default value +func LowestFee(feeUnits []FeeUnit, defaultValue FeeUnit) FeeUnit { + minFee := defaultValue + for _, feeUnit := range feeUnits { + if feeUnit.IsLowerThan(&minFee) { + minFee = feeUnit + } + } + return minFee +} + // GetInputSizeForType get an estimated size for the input based on the type func GetInputSizeForType(inputType string) uint64 { switch inputType { diff --git a/utils/fees_test.go b/utils/fees_test.go index 78958c28..56cd88c0 100644 --- a/utils/fees_test.go +++ b/utils/fees_test.go @@ -31,3 +31,58 @@ func TestGetOutputSizeForType(t *testing.T) { assert.Equal(t, uint64(500), GetOutputSize("")) }) } + +func TestIsLowerThan(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 20, + } + two := FeeUnit{ + Satoshis: 2, + Bytes: 20, + } + assert.True(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) +} + +func TestLowestFee(t *testing.T) { + initTest := func() (feeList []FeeUnit, defaultFee FeeUnit) { + feeList = []FeeUnit{ + { + Satoshis: 1, + Bytes: 20, + }, + { + Satoshis: 2, + Bytes: 20, + }, + { + Satoshis: 3, + Bytes: 20, + }, + } + defaultFee = FeeUnit{ + Satoshis: 4, + Bytes: 20, + } + return + } + + t.Run("lowest fee as default value", func(t *testing.T) { + feeList, defaultFee := initTest() + defaultFee.Satoshis = 1 + defaultFee.Bytes = 50 + assert.Equal(t, defaultFee, LowestFee(feeList, defaultFee)) + }) + + t.Run("lowest fee as first value", func(t *testing.T) { + feeList, defaultFee := initTest() + assert.Equal(t, feeList[0], LowestFee(feeList, defaultFee)) + }) + + t.Run("lowest fee as middle value", func(t *testing.T) { + feeList, defaultFee := initTest() + feeList[1].Bytes = 50 + assert.Equal(t, feeList[1], LowestFee(feeList, defaultFee)) + }) +} From 4f681e1ff0b4c0298875b16cd251b82f1d2693af Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 11 Jan 2024 09:10:48 +0100 Subject: [PATCH 02/12] fix(BUX-461): configurable fee unit --- chainstate/broadcast_client_init.go | 20 +++++++++---------- chainstate/client.go | 22 +++++++++++---------- chainstate/client_options.go | 14 +++++++++++--- chainstate/minercraft_init.go | 30 ++++++++++++++--------------- client_options.go | 14 +++++++++++--- utils/fees.go | 22 ++++++++++++++------- utils/fees_test.go | 14 ++++++++++---- 7 files changed, 84 insertions(+), 52 deletions(-) diff --git a/chainstate/broadcast_client_init.go b/chainstate/broadcast_client_init.go index a6926373..454f2ece 100644 --- a/chainstate/broadcast_client_init.go +++ b/chainstate/broadcast_client_init.go @@ -9,28 +9,28 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" ) -func (c *Client) broadcastClientInit(ctx context.Context) (feeUnit *utils.FeeUnit, err error) { +func (c *Client) broadcastClientInit(ctx context.Context) error { if txn := newrelic.FromContext(ctx); txn != nil { defer txn.StartSegment("start_broadcast_client").End() } bc := c.options.config.broadcastClient if bc == nil { - err = errors.New("broadcast client is not configured") - return + err := errors.New("broadcast client is not configured") + return err } - feeUnit = DefaultFee() if c.isFeeQuotesEnabled() { // get the lowest fee var feeQuotes []*broadcast.FeeQuote - feeQuotes, err = bc.GetFeeQuote(ctx) + feeQuotes, err := bc.GetFeeQuote(ctx) if err != nil { - return + return err } if len(feeQuotes) == 0 { - c.options.logger.Warn().Msg("no fee quotes returned from broadcast client") + return errors.New("no fee quotes returned from broadcast client") } + c.options.logger.Info().Msgf("got %d fee quote(s) from broadcast client", len(feeQuotes)) fees := make([]utils.FeeUnit, len(feeQuotes)) for index, fee := range feeQuotes { fees[index] = utils.FeeUnit{ @@ -38,9 +38,9 @@ func (c *Client) broadcastClientInit(ctx context.Context) (feeUnit *utils.FeeUni Bytes: int(fee.MiningFee.Bytes), } } - lowest := utils.LowestFee(fees, *DefaultFee()) - feeUnit = &lowest + lowest := utils.LowestFee(fees, c.options.config.feeUnit) + c.options.config.feeUnit = lowest } - return + return nil } diff --git a/chainstate/client.go b/chainstate/client.go index d97c19ef..cd19882d 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -2,6 +2,7 @@ package chainstate import ( "context" + "errors" "time" "github.com/BuxOrg/bux/logging" @@ -88,27 +89,28 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) } // Init active provider - var feeUnit *utils.FeeUnit var err error switch client.ActiveProvider() { case ProviderMinercraft: - feeUnit, err = client.minercraftInit(ctx) + err = client.minercraftInit(ctx) case ProviderBroadcastClient: - feeUnit, err = client.broadcastClientInit(ctx) + err = client.broadcastClientInit(ctx) } if err != nil { return nil, err } - // Set fee unit - if feeUnit == nil { - feeUnit = DefaultFee() - client.options.logger.Info().Msgf("no fee unit found, using default: %s", feeUnit) - } else { - client.options.logger.Info().Msgf("using fee unit: %s", feeUnit) + // Check the fee unit + finalFeeUnit := client.options.config.feeUnit + switch { + case finalFeeUnit == nil: + return nil, errors.New("no fee unit found") + case finalFeeUnit.IsZero(): + return nil, errors.New("fee unit suggests no fees (free)") + default: + client.options.logger.Info().Msgf("using fee unit: %s", finalFeeUnit) } - client.options.config.feeUnit = feeUnit // Return the client return client, nil diff --git a/chainstate/client_options.go b/chainstate/client_options.go index 06d95a38..7aa887d1 100644 --- a/chainstate/client_options.go +++ b/chainstate/client_options.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/BuxOrg/bux/utils" "github.com/bitcoin-sv/go-broadcast-client/broadcast" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" @@ -36,7 +37,7 @@ func defaultClientOptions() *clientOptions { queryTimeout: defaultQueryTimeOut, broadcastClient: nil, feeQuotes: true, - feeUnit: DefaultFee(), + feeUnit: nil, // fee has to be set explicitly or via fee quotes }, debug: false, newRelicEnabled: false, @@ -210,9 +211,16 @@ func WithExcludedProviders(providers []string) ClientOps { } // WithFeeQuotes will set minercraftFeeQuotes flag as true -func WithFeeQuotes() ClientOps { +func WithFeeQuotes(enabled bool) ClientOps { return func(c *clientOptions) { - c.config.feeQuotes = true + c.config.feeQuotes = enabled + } +} + +// WithFeeUnit will set the fee unit +func WithFeeUnit(feeUnit *utils.FeeUnit) ClientOps { + return func(c *clientOptions) { + c.config.feeUnit = feeUnit } } diff --git a/chainstate/minercraft_init.go b/chainstate/minercraft_init.go index d5a45019..674a3b8e 100644 --- a/chainstate/minercraft_init.go +++ b/chainstate/minercraft_init.go @@ -12,27 +12,25 @@ import ( "github.com/tonicpow/go-minercraft/v2/apis/mapi" ) -func (c *Client) minercraftInit(ctx context.Context) (feeUnit *utils.FeeUnit, err error) { +func (c *Client) minercraftInit(ctx context.Context) error { if txn := newrelic.FromContext(ctx); txn != nil { defer txn.StartSegment("start_minercraft").End() } mi := &minercraftInitializer{client: c, ctx: ctx} - if err = mi.newClient(); err != nil { - return + if err := mi.newClient(); err != nil { + return err } - if err = mi.validateMiners(); err != nil { - return + if err := mi.validateMiners(); err != nil { + return err } if c.isFeeQuotesEnabled() { - feeUnit = mi.lowestFee() - } else { - feeUnit = DefaultFee() + c.options.config.feeUnit = mi.lowestFee() } - return + return nil } type minercraftInitializer struct { @@ -127,13 +125,15 @@ func (i *minercraftInitializer) validateMiners() error { fee = quote.Quote.Fees[0] } + feeUnit := &utils.FeeUnit{ + Satoshis: fee.MiningFee.Satoshis, + Bytes: fee.MiningFee.Bytes, + } if c.isFeeQuotesEnabled() { - miner.FeeUnit = &utils.FeeUnit{ - Satoshis: fee.MiningFee.Satoshis, - Bytes: fee.MiningFee.Bytes, - } + miner.FeeUnit = feeUnit miner.FeeLastChecked = time.Now().UTC() } + client.options.logger.Info().Msgf("Got FeeQuote response from miner %s. Fee: %s", miner.Miner.Name, feeUnit) }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.broadcastMiners[index]) } wg.Wait() @@ -174,6 +174,6 @@ func (i *minercraftInitializer) lowestFee() *utils.FeeUnit { for index, miner := range miners { fees[index] = *miner.FeeUnit } - lowest := utils.LowestFee(fees, *DefaultFee()) - return &lowest + lowest := utils.LowestFee(fees, i.client.options.config.feeUnit) + return lowest } diff --git a/client_options.go b/client_options.go index 591d7d53..731b2c95 100644 --- a/client_options.go +++ b/client_options.go @@ -12,6 +12,7 @@ import ( "github.com/BuxOrg/bux/logging" "github.com/BuxOrg/bux/notifications" "github.com/BuxOrg/bux/taskmanager" + "github.com/BuxOrg/bux/utils" "github.com/bitcoin-sv/go-broadcast-client/broadcast" "github.com/bitcoin-sv/go-paymail" "github.com/bitcoin-sv/go-paymail/server" @@ -663,10 +664,17 @@ func WithCustomNotifications(customNotifications notifications.ClientInterface) } } -// WithMinercraftFeeQuotes will set usage of minercraft's fee quotes instead of default fees -func WithFeeQuotes() ClientOps { +// WithFeeQuotes will find the lowest fee instead of using the fee set by the WithFeeUnit function +func WithFeeQuotes(enabled bool) ClientOps { return func(c *clientOptions) { - c.chainstate.options = append(c.chainstate.options, chainstate.WithFeeQuotes()) + c.chainstate.options = append(c.chainstate.options, chainstate.WithFeeQuotes(enabled)) + } +} + +// WithFeeUnit will set the fee unit to use for broadcasting +func WithFeeUnit(feeUnit *utils.FeeUnit) ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithFeeUnit(feeUnit)) } } diff --git a/utils/fees.go b/utils/fees.go index ab0aa5f6..31729ed3 100644 --- a/utils/fees.go +++ b/utils/fees.go @@ -20,15 +20,23 @@ func (f *FeeUnit) String() string { return fmt.Sprintf("FeeUnit(%d satoshis / %d bytes)", f.Satoshis, f.Bytes) } -// LowestFee get the lowest fee from a list of fee units AND a default value -func LowestFee(feeUnits []FeeUnit, defaultValue FeeUnit) FeeUnit { - minFee := defaultValue - for _, feeUnit := range feeUnits { - if feeUnit.IsLowerThan(&minFee) { - minFee = feeUnit +// IsZero returns true if the fee unit suggets no fees (free) +func (f *FeeUnit) IsZero() bool { + return f.Satoshis == 0 +} + +// LowestFee get the lowest fee from a list of fee units, if defaultValue exists and none is found, return defaultValue +func LowestFee(feeUnits []FeeUnit, defaultValue *FeeUnit) *FeeUnit { + if len(feeUnits) == 0 { + return defaultValue + } + minFee := feeUnits[0] + for i := 1; i < len(feeUnits); i++ { + if feeUnits[i].IsLowerThan(&minFee) { + minFee = feeUnits[i] } } - return minFee + return &minFee } // GetInputSizeForType get an estimated size for the input based on the type diff --git a/utils/fees_test.go b/utils/fees_test.go index 56cd88c0..4bad5063 100644 --- a/utils/fees_test.go +++ b/utils/fees_test.go @@ -68,21 +68,27 @@ func TestLowestFee(t *testing.T) { return } - t.Run("lowest fee as default value", func(t *testing.T) { + t.Run("lowest fee among feeList elements, despite defaultValue", func(t *testing.T) { feeList, defaultFee := initTest() defaultFee.Satoshis = 1 defaultFee.Bytes = 50 - assert.Equal(t, defaultFee, LowestFee(feeList, defaultFee)) + assert.Equal(t, feeList[0], *LowestFee(feeList, &defaultFee)) }) t.Run("lowest fee as first value", func(t *testing.T) { feeList, defaultFee := initTest() - assert.Equal(t, feeList[0], LowestFee(feeList, defaultFee)) + assert.Equal(t, feeList[0], *LowestFee(feeList, &defaultFee)) }) t.Run("lowest fee as middle value", func(t *testing.T) { feeList, defaultFee := initTest() feeList[1].Bytes = 50 - assert.Equal(t, feeList[1], LowestFee(feeList, defaultFee)) + assert.Equal(t, feeList[1], *LowestFee(feeList, &defaultFee)) + }) + + t.Run("lowest fee as defaultValue", func(t *testing.T) { + feeList, defaultFee := initTest() + feeList = []FeeUnit{} + assert.Equal(t, defaultFee, *LowestFee(feeList, &defaultFee)) }) } From 2ebc639426f6346fdc39e30d6b1834915c3659b2 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 11 Jan 2024 09:47:59 +0100 Subject: [PATCH 03/12] fix(BUX-461): default fee --- chainstate/definitions.go | 2 +- go.mod | 2 ++ utils/fees_test.go | 38 ++++++++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/chainstate/definitions.go b/chainstate/definitions.go index 072016d0..970dff97 100644 --- a/chainstate/definitions.go +++ b/chainstate/definitions.go @@ -56,7 +56,7 @@ const ( func DefaultFee() *utils.FeeUnit { return &utils.FeeUnit{ Satoshis: 1, - Bytes: 20, + Bytes: 1000, } } diff --git a/go.mod b/go.mod index a23a30ad..cb6c78fa 100644 --- a/go.mod +++ b/go.mod @@ -153,3 +153,5 @@ replace github.com/centrifugal/protocol => github.com/centrifugal/protocol v0.9. // Issue: go.mongodb.org/mongo-driver/x/bsonx: cannot find module providing package go.mongodb.org/mongo-driver/x/bsonx // Need to convert bsonx to bsoncore replace go.mongodb.org/mongo-driver => go.mongodb.org/mongo-driver v1.11.7 + +replace github.com/bitcoin-sv/go-broadcast-client => E:\Data\Source\4chain\go-broadcast-client diff --git a/utils/fees_test.go b/utils/fees_test.go index 4bad5063..721a7ccd 100644 --- a/utils/fees_test.go +++ b/utils/fees_test.go @@ -33,16 +33,30 @@ func TestGetOutputSizeForType(t *testing.T) { } func TestIsLowerThan(t *testing.T) { - one := FeeUnit{ - Satoshis: 1, - Bytes: 20, - } - two := FeeUnit{ - Satoshis: 2, - Bytes: 20, - } - assert.True(t, one.IsLowerThan(&two)) - assert.False(t, two.IsLowerThan(&one)) + t.Run("same satoshis, different bytes", func(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 1000, + } + two := FeeUnit{ + Satoshis: 1, + Bytes: 20, + } + assert.True(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) + }) + t.Run("same bytes, different satoshis", func(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 20, + } + two := FeeUnit{ + Satoshis: 2, + Bytes: 20, + } + assert.True(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) + }) } func TestLowestFee(t *testing.T) { @@ -87,8 +101,8 @@ func TestLowestFee(t *testing.T) { }) t.Run("lowest fee as defaultValue", func(t *testing.T) { - feeList, defaultFee := initTest() - feeList = []FeeUnit{} + _, defaultFee := initTest() + feeList := []FeeUnit{} assert.Equal(t, defaultFee, *LowestFee(feeList, &defaultFee)) }) } From be37acec8eec601afe387d273ccb76c7e4ddb603 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:29:35 +0100 Subject: [PATCH 04/12] fix(BUX-461): problem with defaultFee used by the tests --- chainstate/client_test.go | 3 ++- chainstate/mock_const.go | 8 ++++++++ chainstate/mock_minercraft.go | 2 +- mock_chainstate_test.go | 2 +- model_draft_transactions_test.go | 4 ++-- model_transaction_config_test.go | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/chainstate/client_test.go b/chainstate/client_test.go index e766b9ad..cf9efe4a 100644 --- a/chainstate/client_test.go +++ b/chainstate/client_test.go @@ -64,7 +64,7 @@ func TestNewClient(t *testing.T) { t.Run("custom minercraft client", func(t *testing.T) { customClient, err := minercraft.NewClient( - minercraft.DefaultClientOptions(), &http.Client{}, minercraft.Arc, nil, nil, + minercraft.DefaultClientOptions(), &http.Client{}, minercraft.MAPI, nil, nil, ) require.NoError(t, err) require.NotNil(t, customClient) @@ -108,6 +108,7 @@ func TestNewClient(t *testing.T) { context.Background(), WithNetwork(StressTestNet), WithMinercraft(&MinerCraftBase{}), + WithFeeUnit(DefaultFee()), ) require.NoError(t, err) require.NotNil(t, c) diff --git a/chainstate/mock_const.go b/chainstate/mock_const.go index 256664f8..c5b380d1 100644 --- a/chainstate/mock_const.go +++ b/chainstate/mock_const.go @@ -1,5 +1,7 @@ package chainstate +import "github.com/BuxOrg/bux/utils" + const ( // Dummy transaction data broadcastExample1TxID = "15d31d00ed7533a83d7ab206115d7642812ec04a2cbae4248365febb82576ff3" @@ -23,3 +25,9 @@ const ( utf8Type = "UTF-8" applicationJSONType = "application/json" ) + +// MockDefaultFee is a mock default fee used for assertions +var MockDefaultFee = &utils.FeeUnit{ + Satoshis: 1, + Bytes: 20, +} diff --git a/chainstate/mock_minercraft.go b/chainstate/mock_minercraft.go index 3e1aa033..caa9678d 100644 --- a/chainstate/mock_minercraft.go +++ b/chainstate/mock_minercraft.go @@ -128,7 +128,7 @@ func (m *MinerCraftBase) FeeQuote(context.Context, *minercraft.Miner) (*minercra Fees: []*bt.Fee{ { FeeType: bt.FeeTypeData, - MiningFee: bt.FeeUnit(*DefaultFee()), + MiningFee: bt.FeeUnit(*MockDefaultFee), }, }, }, diff --git a/mock_chainstate_test.go b/mock_chainstate_test.go index aad1d641..ae1b97dd 100644 --- a/mock_chainstate_test.go +++ b/mock_chainstate_test.go @@ -161,7 +161,7 @@ func (c *chainStateEverythingOnChain) QueryTransactionFastest(_ context.Context, } func (c *chainStateEverythingOnChain) FeeUnit() *utils.FeeUnit { - return chainstate.DefaultFee() + return chainstate.MockDefaultFee } func (c *chainStateEverythingOnChain) VerifyMerkleRoots(_ context.Context, _ []chainstate.MerkleRootConfirmationRequestItem) error { diff --git a/model_draft_transactions_test.go b/model_draft_transactions_test.go index 7a4521a9..19e526a6 100644 --- a/model_draft_transactions_test.go +++ b/model_draft_transactions_test.go @@ -272,7 +272,7 @@ func TestDraftTransaction_createTransaction(t *testing.T) { assert.Equal(t, uint64(98988), draftTransaction.Configuration.ChangeSatoshis) assert.Equal(t, uint64(12), draftTransaction.Configuration.Fee) - assert.Equal(t, *chainstate.DefaultFee(), *draftTransaction.Configuration.FeeUnit) + assert.Equal(t, *chainstate.MockDefaultFee, *draftTransaction.Configuration.FeeUnit) assert.Equal(t, 1, len(draftTransaction.Configuration.Inputs)) assert.Equal(t, testLockingScript, draftTransaction.Configuration.Inputs[0].ScriptPubKey) @@ -1398,7 +1398,7 @@ func TestDraftTransaction_estimateFees(t *testing.T) { b, _ := json.Marshal(in["feeUnit"]) _ = json.Unmarshal(b, &feeUnit) } else { - feeUnit = chainstate.DefaultFee() + feeUnit = chainstate.MockDefaultFee } draftTransaction, tx, err2 := createDraftTransactionFromHex(in["hex"].(string), in["inputs"].([]interface{}), feeUnit) require.NoError(t, err2) diff --git a/model_transaction_config_test.go b/model_transaction_config_test.go index a4b5eba1..684ec62f 100644 --- a/model_transaction_config_test.go +++ b/model_transaction_config_test.go @@ -47,7 +47,7 @@ var ( ChangeSatoshis: 124, ExpiresIn: defaultDraftTxExpiresIn, Fee: 12, - FeeUnit: chainstate.DefaultFee(), + FeeUnit: chainstate.MockDefaultFee, Inputs: nil, Outputs: nil, } From b84eeafa4edfa1e95aa0c8c0d4e45474177adce3 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:32:56 +0100 Subject: [PATCH 05/12] reverse(BUX-461): accidently commited replace --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index cb6c78fa..a23a30ad 100644 --- a/go.mod +++ b/go.mod @@ -153,5 +153,3 @@ replace github.com/centrifugal/protocol => github.com/centrifugal/protocol v0.9. // Issue: go.mongodb.org/mongo-driver/x/bsonx: cannot find module providing package go.mongodb.org/mongo-driver/x/bsonx // Need to convert bsonx to bsoncore replace go.mongodb.org/mongo-driver => go.mongodb.org/mongo-driver v1.11.7 - -replace github.com/bitcoin-sv/go-broadcast-client => E:\Data\Source\4chain\go-broadcast-client From 5d8820b50e1dd8ad1e9a43d4d6bef1a48494bb61 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:25:37 +0100 Subject: [PATCH 06/12] refactor(BUX-461): minercraft initialization process --- chainstate/broadcast_providers.go | 6 +- chainstate/client.go | 11 +- chainstate/client_options.go | 69 ++------- chainstate/client_options_test.go | 58 -------- chainstate/minercraft_default.go | 29 ++++ chainstate/minercraft_init.go | 132 +++++++++--------- chainstate/transaction.go | 4 +- client_options.go | 18 --- .../broadcast_miners/broadcast_miners.go | 3 - examples/client/custom_rates/custom_rates.go | 6 +- mock_chainstate_test.go | 12 -- 11 files changed, 113 insertions(+), 235 deletions(-) create mode 100644 chainstate/minercraft_default.go diff --git a/chainstate/broadcast_providers.go b/chainstate/broadcast_providers.go index b66d24fb..6b3c064a 100644 --- a/chainstate/broadcast_providers.go +++ b/chainstate/broadcast_providers.go @@ -18,16 +18,16 @@ type txBroadcastProvider interface { // mAPI provider type mapiBroadcastProvider struct { - miner *Miner + miner *minercraft.Miner txID, txHex string } func (provider mapiBroadcastProvider) getName() string { - return provider.miner.Miner.Name + return provider.miner.Name } func (provider mapiBroadcastProvider) broadcast(ctx context.Context, c *Client) error { - return broadcastMAPI(ctx, c, provider.miner.Miner, provider.txID, provider.txHex) + return broadcastMAPI(ctx, c, provider.miner, provider.txID, provider.txHex) } // broadcastMAPI will broadcast a transaction to a miner using mAPI diff --git a/chainstate/client.go b/chainstate/client.go index cd19882d..e096c79c 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -46,20 +46,13 @@ type ( // minercraftConfig is specific for minercraft configuration minercraftConfig struct { - broadcastMiners []*Miner // List of loaded miners for broadcasting - queryMiners []*Miner // List of loaded miners for querying transactions + broadcastMiners []*minercraft.Miner // List of loaded miners for broadcasting + queryMiners []*minercraft.Miner // List of loaded miners for querying transactions apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) minerAPIs []*minercraft.MinerAPIs // List of miners APIs } - // Miner is the internal chainstate miner (wraps Minercraft miner with more information) - Miner struct { - FeeLastChecked time.Time `json:"fee_last_checked"` // Last time the fee was checked via mAPI - FeeUnit *utils.FeeUnit `json:"fee_unit"` // The fee unit returned from Policy request - Miner *minercraft.Miner `json:"miner"` // The minercraft miner - } - // PulseClient is the internal chainstate pulse client PulseClient struct { url string diff --git a/chainstate/client_options.go b/chainstate/client_options.go index 7aa887d1..6ede68bf 100644 --- a/chainstate/client_options.go +++ b/chainstate/client_options.go @@ -19,58 +19,23 @@ type ClientOps func(c *clientOptions) // // Useful for starting with the default and then modifying as needed func defaultClientOptions() *clientOptions { - // Create the default miners - bm, qm := defaultMiners() - apis, _ := minercraft.DefaultMinersAPIs() - // Set the default options return &clientOptions{ config: &syncConfig{ - httpClient: nil, - minercraftConfig: &minercraftConfig{ - broadcastMiners: bm, - queryMiners: qm, - minerAPIs: apis, - }, - minercraft: nil, - network: MainNet, - queryTimeout: defaultQueryTimeOut, - broadcastClient: nil, - feeQuotes: true, - feeUnit: nil, // fee has to be set explicitly or via fee quotes + httpClient: nil, + minercraftConfig: defaultMinecraftConfig(), + minercraft: nil, + network: MainNet, + queryTimeout: defaultQueryTimeOut, + broadcastClient: nil, + feeQuotes: true, + feeUnit: nil, // fee has to be set explicitly or via fee quotes }, debug: false, newRelicEnabled: false, } } -// defaultMiners will return the miners for default configuration -func defaultMiners() (broadcastMiners []*Miner, queryMiners []*Miner) { - // Set the broadcast miners - miners, _ := minercraft.DefaultMiners() - - // Loop and add (only miners that support ALL TX QUERY) - for index, miner := range miners { - broadcastMiners = append(broadcastMiners, &Miner{ - FeeLastChecked: time.Now().UTC(), - FeeUnit: DefaultFee(), - Miner: miners[index], - }) - - // Only miners that support querying - if miner.Name == minercraft.MinerTaal || miner.Name == minercraft.MinerMempool { - // minercraft.MinerGorillaPool, (does not have -t index enabled - 4.25.22) - // minercraft.MinerMatterpool, (does not have -t index enabled - 4.25.22) - queryMiners = append(queryMiners, &Miner{ - // FeeLastChecked: time.Now().UTC(), - // FeeUnit: DefaultFee, - Miner: miners[index], - }) - } - } - return -} - // getTxnCtx will check for an existing transaction func (c *clientOptions) getTxnCtx(ctx context.Context) context.Context { if c.newRelicEnabled { @@ -128,24 +93,6 @@ func WithArc() ClientOps { } } -// WithBroadcastMiners will set a list of miners for broadcasting -func WithBroadcastMiners(miners []*Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.config.minercraftConfig.broadcastMiners = miners - } - } -} - -// WithQueryMiners will set a list of miners for querying transactions -func WithQueryMiners(miners []*Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.config.minercraftConfig.queryMiners = miners - } - } -} - // WithQueryTimeout will set a different timeout for transaction querying func WithQueryTimeout(timeout time.Duration) ClientOps { return func(c *clientOptions) { diff --git a/chainstate/client_options_test.go b/chainstate/client_options_test.go index 17c986c7..65c9d113 100644 --- a/chainstate/client_options_test.go +++ b/chainstate/client_options_test.go @@ -139,64 +139,6 @@ func TestWithBroadcastClient(t *testing.T) { }) } -// TestWithBroadcastMiners will test the method WithBroadcastMiners() -func TestWithBroadcastMiners(t *testing.T) { - t.Parallel() - - t.Run("check type", func(t *testing.T) { - opt := WithBroadcastMiners(nil) - assert.IsType(t, *new(ClientOps), opt) - }) - - t.Run("test applying nil", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - opt := WithBroadcastMiners(nil) - opt(options) - assert.Nil(t, options.config.minercraftConfig.broadcastMiners) - }) - - t.Run("test applying option", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - miners := []*Miner{{Miner: minerTaal}} - opt := WithBroadcastMiners(miners) - opt(options) - assert.Equal(t, miners, options.config.minercraftConfig.broadcastMiners) - }) -} - -// TestWithQueryMiners will test the method WithQueryMiners() -func TestWithQueryMiners(t *testing.T) { - t.Parallel() - - t.Run("check type", func(t *testing.T) { - opt := WithQueryMiners(nil) - assert.IsType(t, *new(ClientOps), opt) - }) - - t.Run("test applying nil", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - opt := WithQueryMiners(nil) - opt(options) - assert.Nil(t, options.config.minercraftConfig.queryMiners) - }) - - t.Run("test applying option", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - miners := []*Miner{{Miner: minerTaal}} - opt := WithQueryMiners(miners) - opt(options) - assert.Equal(t, miners, options.config.minercraftConfig.queryMiners) - }) -} - // TestWithQueryTimeout will test the method WithQueryTimeout() func TestWithQueryTimeout(t *testing.T) { t.Parallel() diff --git a/chainstate/minercraft_default.go b/chainstate/minercraft_default.go new file mode 100644 index 00000000..3fc6a578 --- /dev/null +++ b/chainstate/minercraft_default.go @@ -0,0 +1,29 @@ +package chainstate + +import "github.com/tonicpow/go-minercraft/v2" + +func defaultMinecraftConfig() *minercraftConfig { + miners, _ := minercraft.DefaultMiners() + apis, _ := minercraft.DefaultMinersAPIs() + + broadcastMiners := []*minercraft.Miner{} + queryMiners := []*minercraft.Miner{} + for _, miner := range miners { + currentMiner := *miner + broadcastMiners = append(broadcastMiners, ¤tMiner) + + if supportsQuerying(¤tMiner) { + queryMiners = append(queryMiners, ¤tMiner) + } + } + + return &minercraftConfig{ + broadcastMiners: broadcastMiners, + queryMiners: queryMiners, + minerAPIs: apis, + } +} + +func supportsQuerying(mm *minercraft.Miner) bool { + return mm.Name == minercraft.MinerTaal || mm.Name == minercraft.MinerMempool +} diff --git a/chainstate/minercraft_init.go b/chainstate/minercraft_init.go index 674a3b8e..cf64528a 100644 --- a/chainstate/minercraft_init.go +++ b/chainstate/minercraft_init.go @@ -2,11 +2,11 @@ package chainstate import ( "context" + "fmt" "sync" "time" "github.com/BuxOrg/bux/utils" - "github.com/libsv/go-bt/v2" "github.com/newrelic/go-agent/v3/newrelic" "github.com/tonicpow/go-minercraft/v2" "github.com/tonicpow/go-minercraft/v2/apis/mapi" @@ -16,7 +16,7 @@ func (c *Client) minercraftInit(ctx context.Context) error { if txn := newrelic.FromContext(ctx); txn != nil { defer txn.StartSegment("start_minercraft").End() } - mi := &minercraftInitializer{client: c, ctx: ctx} + mi := &minercraftInitializer{client: c, ctx: ctx, minersWithFee: make(minerToFeeMap)} if err := mi.newClient(); err != nil { return err @@ -34,10 +34,17 @@ func (c *Client) minercraftInit(ctx context.Context) error { } type minercraftInitializer struct { - client *Client - ctx context.Context + client *Client + ctx context.Context + minersWithFee minerToFeeMap + lock sync.Mutex } +type ( + minerId string + minerToFeeMap map[minerId]utils.FeeUnit +) + func (i *minercraftInitializer) defaultMinercraftOptions() (opts *minercraft.ClientOptions) { c := i.client opts = minercraft.DefaultClientOptions() @@ -56,17 +63,17 @@ func (i *minercraftInitializer) newClient() (err error) { // Loop all broadcast miners and append to the list of miners for i := range c.options.config.minercraftConfig.broadcastMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID) + if !utils.StringInSlice(c.options.config.minercraftConfig.broadcastMiners[i].MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.broadcastMiners[i]) + loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.broadcastMiners[i].MinerID) } } // Loop all query miners and append to the list of miners for i := range c.options.config.minercraftConfig.queryMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.queryMiners[i].Miner) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID) + if !utils.StringInSlice(c.options.config.minercraftConfig.queryMiners[i].MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.queryMiners[i]) + loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.queryMiners[i].MinerID) } } c.options.config.minercraft, err = minercraft.NewClient( @@ -90,51 +97,19 @@ func (i *minercraftInitializer) validateMiners() error { c := i.client var wg sync.WaitGroup - // Loop all broadcast miners - for index := range c.options.config.minercraftConfig.broadcastMiners { + + for _, miner := range c.options.config.minercraftConfig.broadcastMiners { wg.Add(1) - go func( - ctx context.Context, client *Client, - wg *sync.WaitGroup, miner *Miner, - ) { + currentMiner := miner + go func() { defer wg.Done() - // Get the fee quote using the miner - // Switched from policyQuote to feeQuote as gorillapool doesn't have such endpoint - var fee *bt.Fee - if c.Minercraft().APIType() == minercraft.MAPI { - quote, err := c.Minercraft().FeeQuote(ctx, miner.Miner) - if err != nil { - client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) - miner.FeeUnit = nil - return - } - - fee = quote.Quote.GetFee(mapi.FeeTypeData) - if fee == nil { - client.options.logger.Error().Msgf("Fee is missing in %s's FeeQuote response", miner.Miner.Name) - return - } - // Arc doesn't support FeeQuote right now(2023.07.21), that's why PolicyQuote is used - } else if c.Minercraft().APIType() == minercraft.Arc { - quote, err := c.Minercraft().PolicyQuote(ctx, miner.Miner) - if err != nil { - client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) - miner.FeeUnit = nil - return - } - - fee = quote.Quote.Fees[0] - } - feeUnit := &utils.FeeUnit{ - Satoshis: fee.MiningFee.Satoshis, - Bytes: fee.MiningFee.Bytes, - } - if c.isFeeQuotesEnabled() { - miner.FeeUnit = feeUnit - miner.FeeLastChecked = time.Now().UTC() + feeUnit, err := i.getFeeQuote(ctxWithCancel, currentMiner) + if err != nil { + c.options.logger.Warn().Msgf("No FeeQuote response from miner %s. Reason: %s", currentMiner.Name, err) + return } - client.options.logger.Info().Msgf("Got FeeQuote response from miner %s. Fee: %s", miner.Miner.Name, feeUnit) - }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.broadcastMiners[index]) + i.addToMinersWithFee(currentMiner, feeUnit) + }() } wg.Wait() @@ -150,29 +125,56 @@ func (i *minercraftInitializer) validateMiners() error { } } +func (i *minercraftInitializer) getFeeQuote(ctx context.Context, miner *minercraft.Miner) (*utils.FeeUnit, error) { + c := i.client + + apiType := c.Minercraft().APIType() + + if apiType == minercraft.Arc { + return nil, fmt.Errorf("we no longer support ARC with Minercraft. (%s)", miner.Name) + } + + quote, err := c.Minercraft().FeeQuote(ctx, miner) + if err != nil { + return nil, fmt.Errorf("no FeeQuote response from miner %s. Reason: %s", miner.Name, err) + } + + btFee := quote.Quote.GetFee(mapi.FeeTypeData) + if btFee == nil { + return nil, fmt.Errorf("Fee is missing in %s's FeeQuote response", miner.Name) + } + + feeUnit := &utils.FeeUnit{ + Satoshis: btFee.MiningFee.Satoshis, + Bytes: btFee.MiningFee.Bytes, + } + return feeUnit, nil +} + +func (i *minercraftInitializer) addToMinersWithFee(miner *minercraft.Miner, feeUnit *utils.FeeUnit) { + i.lock.Lock() + defer i.lock.Unlock() + i.minersWithFee[minerId(miner.MinerID)] = *feeUnit +} + // deleteUnreacheableMiners deletes miners which can't be reacheable from config func (i *minercraftInitializer) deleteUnreacheableMiners() { c := i.client - validMinerIndex := 0 + validMiners := []*minercraft.Miner{} for _, miner := range c.options.config.minercraftConfig.broadcastMiners { - if miner.FeeUnit != nil { - c.options.config.minercraftConfig.broadcastMiners[validMinerIndex] = miner - validMinerIndex++ + _, ok := i.minersWithFee[minerId(miner.MinerID)] + if ok { + validMiners = append(validMiners, miner) } } - // Prevent memory leak by erasing truncated miners - for i := validMinerIndex; i < len(c.options.config.minercraftConfig.broadcastMiners); i++ { - c.options.config.minercraftConfig.broadcastMiners[i] = nil - } - c.options.config.minercraftConfig.broadcastMiners = c.options.config.minercraftConfig.broadcastMiners[:validMinerIndex] + c.options.config.minercraftConfig.broadcastMiners = validMiners } // lowestFees takes the lowest fees among all miners and sets them as the feeUnit for future transactions func (i *minercraftInitializer) lowestFee() *utils.FeeUnit { - miners := i.client.options.config.minercraftConfig.broadcastMiners - fees := make([]utils.FeeUnit, len(miners)) - for index, miner := range miners { - fees[index] = *miner.FeeUnit + fees := make([]utils.FeeUnit, 0) + for _, fee := range i.minersWithFee { + fees = append(fees, fee) } lowest := utils.LowestFee(fees, i.client.options.config.feeUnit) return lowest diff --git a/chainstate/transaction.go b/chainstate/transaction.go index 724023af..4f018ed1 100644 --- a/chainstate/transaction.go +++ b/chainstate/transaction.go @@ -23,7 +23,7 @@ func (c *Client) query(ctx context.Context, id string, requiredIn RequiredIn, for index := range c.options.config.minercraftConfig.queryMiners { if c.options.config.minercraftConfig.queryMiners[index] != nil { if res, err := queryMinercraft( - ctxWithCancel, c, c.options.config.minercraftConfig.queryMiners[index].Miner, id, + ctxWithCancel, c, c.options.config.minercraftConfig.queryMiners[index], id, ); err == nil && checkRequirementMapi(requiredIn, id, res) { return res } @@ -74,7 +74,7 @@ func (c *Client) fastestQuery(ctx context.Context, id string, requiredIn Require ); err == nil && checkRequirementMapi(requiredIn, id, res) { resultsChannel <- res } - }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.queryMiners[index].Miner, id, requiredIn) + }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.queryMiners[index], id, requiredIn) } case ProviderBroadcastClient: wg.Add(1) diff --git a/client_options.go b/client_options.go index 731b2c95..1d747427 100644 --- a/client_options.go +++ b/client_options.go @@ -615,24 +615,6 @@ func WithChainstateOptions(broadcasting, broadcastInstant, paymailP2P, syncOnCha } } -// WithBroadcastMiners will set a list of miners for broadcasting -func WithBroadcastMiners(miners []*chainstate.Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.chainstate.options = append(c.chainstate.options, chainstate.WithBroadcastMiners(miners)) - } - } -} - -// WithQueryMiners will set a list of miners for querying transactions -func WithQueryMiners(miners []*chainstate.Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.chainstate.options = append(c.chainstate.options, chainstate.WithQueryMiners(miners)) - } - } -} - // WithExcludedProviders will set a list of excluded providers func WithExcludedProviders(providers []string) ClientOps { return func(c *clientOptions) { diff --git a/examples/client/broadcast_miners/broadcast_miners.go b/examples/client/broadcast_miners/broadcast_miners.go index bcb09187..d8753ffa 100644 --- a/examples/client/broadcast_miners/broadcast_miners.go +++ b/examples/client/broadcast_miners/broadcast_miners.go @@ -6,7 +6,6 @@ import ( "os" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux/chainstate" "github.com/tonicpow/go-minercraft/v2" ) @@ -30,8 +29,6 @@ func main() { // Create the client client, err := bux.NewClient( context.Background(), // Set context - bux.WithBroadcastMiners([]*chainstate.Miner{{Miner: minerTaal}}), // This will auto-fetch a policy using the token (api key) - bux.WithQueryMiners([]*chainstate.Miner{{Miner: minerTaal}}), // This will only use this as a query provider bux.WithMinercraftAPIs(minerCraftApis), bux.WithArc(), ) diff --git a/examples/client/custom_rates/custom_rates.go b/examples/client/custom_rates/custom_rates.go index 65f9a46f..c7a5835d 100644 --- a/examples/client/custom_rates/custom_rates.go +++ b/examples/client/custom_rates/custom_rates.go @@ -7,7 +7,6 @@ import ( "time" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux/chainstate" "github.com/tonicpow/go-minercraft/v2" ) @@ -32,9 +31,8 @@ func main() { // Create the client client, err := bux.NewClient( - context.Background(), // Set context - bux.WithAutoMigrate(bux.BaseModels...), // All models - bux.WithBroadcastMiners([]*chainstate.Miner{{Miner: minerTaal}}), // This will auto-fetch a policy using the token (api key) + context.Background(), // Set context + bux.WithAutoMigrate(bux.BaseModels...), // All models bux.WithMinercraftAPIs(minerCraftApis), bux.WithArc(), ) diff --git a/mock_chainstate_test.go b/mock_chainstate_test.go index ae1b97dd..38d4b16c 100644 --- a/mock_chainstate_test.go +++ b/mock_chainstate_test.go @@ -30,10 +30,6 @@ func (c *chainStateBase) QueryTransactionFastest(context.Context, string, chains return nil, nil } -func (c *chainStateBase) BroadcastMiners() []*chainstate.Miner { - return nil -} - func (c *chainStateBase) Close(context.Context) {} func (c *chainStateBase) Debug(bool) {} @@ -56,18 +52,10 @@ func (c *chainStateBase) Minercraft() minercraft.ClientInterface { return nil } -func (c *chainStateBase) Miners() []*chainstate.Miner { - return nil -} - func (c *chainStateBase) Network() chainstate.Network { return chainstate.MainNet } -func (c *chainStateBase) QueryMiners() []*chainstate.Miner { - return nil -} - func (c *chainStateBase) QueryTimeout() time.Duration { return 10 * time.Second } From b166a276f5e8fca9fbff84dd83c22ecb36ffb914 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:20:00 +0100 Subject: [PATCH 07/12] refactor(BUX-461): small changes after self-review --- chainstate/broadcast_client_init.go | 3 +-- chainstate/client.go | 2 +- utils/fees_test.go | 13 +++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/chainstate/broadcast_client_init.go b/chainstate/broadcast_client_init.go index 454f2ece..68152141 100644 --- a/chainstate/broadcast_client_init.go +++ b/chainstate/broadcast_client_init.go @@ -38,8 +38,7 @@ func (c *Client) broadcastClientInit(ctx context.Context) error { Bytes: int(fee.MiningFee.Bytes), } } - lowest := utils.LowestFee(fees, c.options.config.feeUnit) - c.options.config.feeUnit = lowest + c.options.config.feeUnit = utils.LowestFee(fees, c.options.config.feeUnit) } return nil diff --git a/chainstate/client.go b/chainstate/client.go index e096c79c..00016a83 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -100,7 +100,7 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) case finalFeeUnit == nil: return nil, errors.New("no fee unit found") case finalFeeUnit.IsZero(): - return nil, errors.New("fee unit suggests no fees (free)") + client.options.logger.Info().Msg("fee unit suggests no fees (free)") default: client.options.logger.Info().Msgf("using fee unit: %s", finalFeeUnit) } diff --git a/utils/fees_test.go b/utils/fees_test.go index 721a7ccd..2f6bf8d7 100644 --- a/utils/fees_test.go +++ b/utils/fees_test.go @@ -57,6 +57,19 @@ func TestIsLowerThan(t *testing.T) { assert.True(t, one.IsLowerThan(&two)) assert.False(t, two.IsLowerThan(&one)) }) + + t.Run("zero as bytes in denominator", func(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 0, + } + two := FeeUnit{ + Satoshis: 2, + Bytes: 0, + } + assert.False(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) + }) } func TestLowestFee(t *testing.T) { From eea46e5add00839e01c7dc82202fdc4fa2443556 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 12 Jan 2024 07:02:42 +0100 Subject: [PATCH 08/12] refactor(BUX-461): chainstate initialisation process --- chainstate/client.go | 60 +++++++++++++++++++++-------------- chainstate/minercraft_init.go | 8 ++--- model_draft_transactions.go | 2 -- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/chainstate/client.go b/chainstate/client.go index 00016a83..f0426a29 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -81,28 +81,12 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) client.options.logger = logging.GetDefaultLogger() } - // Init active provider - var err error - switch client.ActiveProvider() { - case ProviderMinercraft: - err = client.minercraftInit(ctx) - case ProviderBroadcastClient: - err = client.broadcastClientInit(ctx) - } - - if err != nil { + if err := client.initActiveProvider(ctx); err != nil { return nil, err } - // Check the fee unit - finalFeeUnit := client.options.config.feeUnit - switch { - case finalFeeUnit == nil: - return nil, errors.New("no fee unit found") - case finalFeeUnit.IsZero(): - client.options.logger.Info().Msg("fee unit suggests no fees (free)") - default: - client.options.logger.Info().Msgf("using fee unit: %s", finalFeeUnit) + if err := client.checkFeeUnit(); err != nil { + return nil, err } // Return the client @@ -184,10 +168,6 @@ func (c *Client) FeeUnit() *utils.FeeUnit { return c.options.config.feeUnit } -func (c *Client) isFeeQuotesEnabled() bool { - return c.options.config.feeQuotes -} - // ActiveProvider returns a name of a provider based on config. func (c *Client) ActiveProvider() string { excluded := c.options.config.excludedProviders @@ -199,3 +179,37 @@ func (c *Client) ActiveProvider() string { } return ProviderNone } + +func (c *Client) isFeeQuotesEnabled() bool { + return c.options.config.feeQuotes +} + +func (c *Client) initActiveProvider(ctx context.Context) error { + switch c.ActiveProvider() { + case ProviderMinercraft: + return c.minercraftInit(ctx) + case ProviderBroadcastClient: + return c.broadcastClientInit(ctx) + default: + return errors.New("no active provider found") + } +} + +func (c *Client) checkFeeUnit() error { + feeUnit := c.options.config.feeUnit + switch { + case feeUnit == nil: + return errors.New("no fee unit found") + case feeUnit.IsZero(): + c.options.logger.Warn().Msg("fee unit suggests no fees (free)") + default: + var feeUnitSource string + if c.isFeeQuotesEnabled() { + feeUnitSource = "fee quotes" + } else { + feeUnitSource = "configured fee_unit" + } + c.options.logger.Info().Msgf("using fee unit: %s from %s", feeUnit, feeUnitSource) + } + return nil +} diff --git a/chainstate/minercraft_init.go b/chainstate/minercraft_init.go index cf64528a..ae9519e8 100644 --- a/chainstate/minercraft_init.go +++ b/chainstate/minercraft_init.go @@ -41,8 +41,8 @@ type minercraftInitializer struct { } type ( - minerId string - minerToFeeMap map[minerId]utils.FeeUnit + minerID string + minerToFeeMap map[minerID]utils.FeeUnit ) func (i *minercraftInitializer) defaultMinercraftOptions() (opts *minercraft.ClientOptions) { @@ -154,7 +154,7 @@ func (i *minercraftInitializer) getFeeQuote(ctx context.Context, miner *minercra func (i *minercraftInitializer) addToMinersWithFee(miner *minercraft.Miner, feeUnit *utils.FeeUnit) { i.lock.Lock() defer i.lock.Unlock() - i.minersWithFee[minerId(miner.MinerID)] = *feeUnit + i.minersWithFee[minerID(miner.MinerID)] = *feeUnit } // deleteUnreacheableMiners deletes miners which can't be reacheable from config @@ -162,7 +162,7 @@ func (i *minercraftInitializer) deleteUnreacheableMiners() { c := i.client validMiners := []*minercraft.Miner{} for _, miner := range c.options.config.minercraftConfig.broadcastMiners { - _, ok := i.minersWithFee[minerId(miner.MinerID)] + _, ok := i.minersWithFee[minerID(miner.MinerID)] if ok { validMiners = append(validMiners, miner) } diff --git a/model_draft_transactions.go b/model_draft_transactions.go index cf4c249c..b4f5ff21 100644 --- a/model_draft_transactions.go +++ b/model_draft_transactions.go @@ -62,8 +62,6 @@ func newDraftTransaction(rawXpubKey string, config *TransactionConfig, opts ...M ), } - // Set the fee (if not found) (if chainstate is loaded, use the first miner) - // todo: make this more intelligent or allow the config to dictate the miner selection if config.FeeUnit == nil { if c := draft.Client(); c != nil { draft.Configuration.FeeUnit = c.Chainstate().FeeUnit() From 6c672a31847f4059779dd76a976607ed6f62eaf9 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 12 Jan 2024 07:31:41 +0100 Subject: [PATCH 09/12] chore(BUX-461): get rid of hardcoded DefaultFee --- chainstate/client_test.go | 10 ++++------ chainstate/definitions.go | 12 ------------ model_draft_transactions.go | 7 +------ model_draft_transactions_test.go | 14 +++++++++++--- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/chainstate/client_test.go b/chainstate/client_test.go index cf9efe4a..fff6e1ed 100644 --- a/chainstate/client_test.go +++ b/chainstate/client_test.go @@ -103,16 +103,14 @@ func TestNewClient(t *testing.T) { assert.Equal(t, TestNet, c.Network()) }) - t.Run("custom network - stn", func(t *testing.T) { - c, err := NewClient( + t.Run("no provider when using minercraft with customNet", func(t *testing.T) { + _, err := NewClient( context.Background(), WithNetwork(StressTestNet), WithMinercraft(&MinerCraftBase{}), - WithFeeUnit(DefaultFee()), + WithFeeUnit(MockDefaultFee), ) - require.NoError(t, err) - require.NotNil(t, c) - assert.Equal(t, StressTestNet, c.Network()) + require.Error(t, err) }) t.Run("unreacheble miners", func(t *testing.T) { diff --git a/chainstate/definitions.go b/chainstate/definitions.go index 970dff97..25d180eb 100644 --- a/chainstate/definitions.go +++ b/chainstate/definitions.go @@ -2,8 +2,6 @@ package chainstate import ( "time" - - "github.com/BuxOrg/bux/utils" ) // Chainstate configuration defaults @@ -50,16 +48,6 @@ const ( ProviderNone = "none" // No providers (used to indicate no providers) ) -// DefaultFee is used when a fee has not been set by the user -// This default is currently accepted by all BitcoinSV miners (50/1000) (7.27.23) -// Actual TAAL FeeUnit - 1/1000, GorillaPool - 50/1000 (7.27.23) -func DefaultFee() *utils.FeeUnit { - return &utils.FeeUnit{ - Satoshis: 1, - Bytes: 1000, - } -} - // BlockInfo is the response info about a returned block type BlockInfo struct { Bits string `json:"bits"` diff --git a/model_draft_transactions.go b/model_draft_transactions.go index b4f5ff21..ada89589 100644 --- a/model_draft_transactions.go +++ b/model_draft_transactions.go @@ -9,7 +9,6 @@ import ( "math/big" "time" - "github.com/BuxOrg/bux/chainstate" "github.com/BuxOrg/bux/utils" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" @@ -63,11 +62,7 @@ func newDraftTransaction(rawXpubKey string, config *TransactionConfig, opts ...M } if config.FeeUnit == nil { - if c := draft.Client(); c != nil { - draft.Configuration.FeeUnit = c.Chainstate().FeeUnit() - } else { - draft.Configuration.FeeUnit = chainstate.DefaultFee() - } + draft.Configuration.FeeUnit = draft.Client().Chainstate().FeeUnit() } return draft } diff --git a/model_draft_transactions_test.go b/model_draft_transactions_test.go index 19e526a6..e4e1e802 100644 --- a/model_draft_transactions_test.go +++ b/model_draft_transactions_test.go @@ -45,7 +45,9 @@ func TestDraftTransaction_newDraftTransaction(t *testing.T) { t.Run("valid config", func(t *testing.T) { expires := time.Now().UTC().Add(defaultDraftTxExpiresIn) draftTx := newDraftTransaction( - testXPub, &TransactionConfig{}, New(), + testXPub, &TransactionConfig{ + FeeUnit: chainstate.MockDefaultFee, + }, New(), ) require.NotNil(t, draftTx) assert.NotEqual(t, "", draftTx.ID) @@ -53,7 +55,7 @@ func TestDraftTransaction_newDraftTransaction(t *testing.T) { assert.WithinDurationf(t, expires, draftTx.ExpiresAt, 1*time.Second, "within 1 second") assert.Equal(t, DraftStatusDraft, draftTx.Status) assert.Equal(t, testXPubID, draftTx.XpubID) - assert.Equal(t, *chainstate.DefaultFee(), *draftTx.Configuration.FeeUnit) + assert.Equal(t, *chainstate.MockDefaultFee, *draftTx.Configuration.FeeUnit) }) } @@ -62,7 +64,9 @@ func TestDraftTransaction_GetModelName(t *testing.T) { t.Parallel() t.Run("model name", func(t *testing.T) { - draftTx := newDraftTransaction(testXPub, &TransactionConfig{}, New()) + draftTx := newDraftTransaction(testXPub, &TransactionConfig{ + FeeUnit: chainstate.MockDefaultFee, + }, New()) require.NotNil(t, draftTx) assert.Equal(t, ModelDraftTransaction.String(), draftTx.GetModelName()) }) @@ -76,6 +80,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { ChangeDestinations: []*Destination{{ LockingScript: testLockingScript, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) changSatoshis, err := draftTx.getChangeSatoshis(1000000) @@ -91,6 +96,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { ChangeDestinations: []*Destination{{ LockingScript: testLockingScript, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) changSatoshis, err := draftTx.getChangeSatoshis(1000000) @@ -107,6 +113,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { }, { LockingScript: testTxInScriptPubKey, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) changSatoshis, err := draftTx.getChangeSatoshis(1000001) @@ -127,6 +134,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { }, { LockingScript: testTxScriptPubKey1, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) satoshis := uint64(1000001) From aa52005436e01b143cb3875ea9abbc4e9cfdbc6b Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 12 Jan 2024 07:40:42 +0100 Subject: [PATCH 10/12] chore(BUX-461) bump version to 0.12.0 --- definitions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/definitions.go b/definitions.go index 0980c321..1fe08827 100644 --- a/definitions.go +++ b/definitions.go @@ -23,7 +23,7 @@ const ( dustLimit = uint64(1) // Dust limit mongoTestVersion = "6.0.4" // Mongo Testing Version sqliteTestVersion = "3.37.0" // SQLite Testing Version (dummy version for now) - version = "v0.11.0" // bux version + version = "v0.12.0" // bux version ) // All the base models From 9a22645c9d05e69788ec612acd1aab1269bb1c6f Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:05:04 +0100 Subject: [PATCH 11/12] refactor(BUX-461): suggestions from PR comments --- chainstate/minercraft_default.go | 7 +++---- chainstate/minercraft_init.go | 18 +++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/chainstate/minercraft_default.go b/chainstate/minercraft_default.go index 3fc6a578..1e6e28a0 100644 --- a/chainstate/minercraft_default.go +++ b/chainstate/minercraft_default.go @@ -9,11 +9,10 @@ func defaultMinecraftConfig() *minercraftConfig { broadcastMiners := []*minercraft.Miner{} queryMiners := []*minercraft.Miner{} for _, miner := range miners { - currentMiner := *miner - broadcastMiners = append(broadcastMiners, ¤tMiner) + broadcastMiners = append(broadcastMiners, miner) - if supportsQuerying(¤tMiner) { - queryMiners = append(queryMiners, ¤tMiner) + if supportsQuerying(miner) { + queryMiners = append(queryMiners, miner) } } diff --git a/chainstate/minercraft_init.go b/chainstate/minercraft_init.go index ae9519e8..297b55d6 100644 --- a/chainstate/minercraft_init.go +++ b/chainstate/minercraft_init.go @@ -56,24 +56,24 @@ func (i *minercraftInitializer) defaultMinercraftOptions() (opts *minercraft.Cli func (i *minercraftInitializer) newClient() (err error) { c := i.client - // No client? + if c.Minercraft() == nil { var optionalMiners []*minercraft.Miner var loadedMiners []string // Loop all broadcast miners and append to the list of miners - for i := range c.options.config.minercraftConfig.broadcastMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.broadcastMiners[i].MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.broadcastMiners[i]) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.broadcastMiners[i].MinerID) + for _, broadcastMiner := range c.options.config.minercraftConfig.broadcastMiners { + if !utils.StringInSlice(broadcastMiner.MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, broadcastMiner) + loadedMiners = append(loadedMiners, broadcastMiner.MinerID) } } // Loop all query miners and append to the list of miners - for i := range c.options.config.minercraftConfig.queryMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.queryMiners[i].MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.queryMiners[i]) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.queryMiners[i].MinerID) + for _, queryMiner := range c.options.config.minercraftConfig.queryMiners { + if !utils.StringInSlice(queryMiner.MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, queryMiner) + loadedMiners = append(loadedMiners, queryMiner.MinerID) } } c.options.config.minercraft, err = minercraft.NewClient( From 2410e8ef1906b13a48f128d259444ac021c96726 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:00:13 +0100 Subject: [PATCH 12/12] chore(BUX-461): filter out invalid fee units --- chainstate/client.go | 3 +++ utils/fees.go | 27 ++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/chainstate/client.go b/chainstate/client.go index f0426a29..911d822e 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -3,6 +3,7 @@ package chainstate import ( "context" "errors" + "fmt" "time" "github.com/BuxOrg/bux/logging" @@ -200,6 +201,8 @@ func (c *Client) checkFeeUnit() error { switch { case feeUnit == nil: return errors.New("no fee unit found") + case !feeUnit.IsValid(): + return fmt.Errorf("invalid fee unit found: %s", feeUnit) case feeUnit.IsZero(): c.options.logger.Warn().Msg("fee unit suggests no fees (free)") default: diff --git a/utils/fees.go b/utils/fees.go index 31729ed3..6a4e0089 100644 --- a/utils/fees.go +++ b/utils/fees.go @@ -25,15 +25,32 @@ func (f *FeeUnit) IsZero() bool { return f.Satoshis == 0 } +// IsValid returns true if the Bytes in fee are greater than 0 +func (f *FeeUnit) IsValid() bool { + return f.Bytes > 0 +} + +// ValidFees filters out invalid fees from a list of fee units +func ValidFees(feeUnits []FeeUnit) []FeeUnit { + validFees := []FeeUnit{} + for _, fee := range feeUnits { + if fee.IsValid() { + validFees = append(validFees, fee) + } + } + return validFees +} + // LowestFee get the lowest fee from a list of fee units, if defaultValue exists and none is found, return defaultValue func LowestFee(feeUnits []FeeUnit, defaultValue *FeeUnit) *FeeUnit { - if len(feeUnits) == 0 { + validFees := ValidFees(feeUnits) + if len(validFees) == 0 { return defaultValue } - minFee := feeUnits[0] - for i := 1; i < len(feeUnits); i++ { - if feeUnits[i].IsLowerThan(&minFee) { - minFee = feeUnits[i] + minFee := validFees[0] + for i := 1; i < len(validFees); i++ { + if validFees[i].IsLowerThan(&minFee) { + minFee = validFees[i] } } return &minFee