From 98e2f672c49ee4aae0a62ccebba39a208604f154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lewandowski?= Date: Wed, 23 Aug 2023 13:53:33 +0200 Subject: [PATCH 1/2] feat(BUX-147): integrate go-broadcast-client --- chainstate/broadcast.go | 10 ++++++++ chainstate/broadcast_providers.go | 35 ++++++++++++++++++++++++++ chainstate/client.go | 34 +++++++++++++++++-------- chainstate/client_options.go | 28 ++++++++++++++++++--- chainstate/definitions.go | 9 ++++--- chainstate/interface.go | 2 ++ chainstate/transaction.go | 42 +++++++++++++++++++++++++++++++ client_options.go | 16 ++++++++++++ go.mod | 1 + go.sum | 2 ++ model_sync_transactions.go | 4 +-- 11 files changed, 163 insertions(+), 20 deletions(-) diff --git a/chainstate/broadcast.go b/chainstate/broadcast.go index 5bc8ae69..1005cacf 100644 --- a/chainstate/broadcast.go +++ b/chainstate/broadcast.go @@ -111,6 +111,11 @@ func createActiveProviders(c *Client, txID, txHex string) []txBroadcastProvider providers = append(providers, &pvdr) } + if shouldBroadcastWithBroadcastClient(c) { + pvdr := broadcastClientProvider{txID: txID, txHex: txHex} + providers = append(providers, &pvdr) + } + return providers } @@ -128,6 +133,11 @@ func shouldBroadcastToNowNodes(c *Client) bool { c.NowNodes() != nil // Only if NowNodes is loaded (requires API key) } +func shouldBroadcastWithBroadcastClient(c *Client) bool { + return !utils.StringInSlice(ProviderBroadcastClient, c.options.config.excludedProviders) && + c.BroadcastClient() != nil // Only if NowNodes is loaded (requires API key) +} + func broadcastToProvider(ctx, fallbackCtx context.Context, provider txBroadcastProvider, txID string, c *Client, fallbackTimeout time.Duration, resultsChannel chan broadcastResult, status *broadcastStatus, diff --git a/chainstate/broadcast_providers.go b/chainstate/broadcast_providers.go index 0a5bf097..87d31252 100644 --- a/chainstate/broadcast_providers.go +++ b/chainstate/broadcast_providers.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" "strings" "github.com/mrz1836/go-nownodes" @@ -149,3 +150,37 @@ func broadcastNowNodes(ctx context.Context, client ClientInterface, uniqueID, tx func incorrectTxIDReturnedErr(actualTxID, expectedTxID string) error { return fmt.Errorf("returned tx id [%s] does not match given tx id [%s]", actualTxID, expectedTxID) } + +//// + +// BroadcastClient provider +type broadcastClientProvider struct { + txID, txHex string +} + +func (provider broadcastClientProvider) getName() string { + return ProviderBroadcastClient +} + +// Broadcast using BroadcastClient +func (provider broadcastClientProvider) broadcast(ctx context.Context, c *Client) error { + return broadcastWithBroadcastClient(ctx, c, provider.txID, provider.txHex) +} + +func broadcastWithBroadcastClient(ctx context.Context, client ClientInterface, txID, hex string) error { + debugLog(client, txID, "executing broadcast request for "+ProviderBroadcastClient) + + tx := broadcast.Transaction{ + RawTx: hex, + } + + result, err := client.BroadcastClient().SubmitTransaction(ctx, &tx) + if err != nil { + debugLog(client, txID, "error broadcast request for "+ProviderBroadcastClient+" failed: "+err.Error()) + return nil + } + + debugLog(client, txID, "result broadcast request for "+ProviderBroadcastClient+" blockhash: "+result.BlockHash+" status: "+result.TxStatus.String()) + + return nil +} diff --git a/chainstate/client.go b/chainstate/client.go index 5f21f8c8..fe040fc2 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -3,6 +3,8 @@ package chainstate import ( "context" "fmt" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + broadcastClient "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" "sync" "time" @@ -35,16 +37,18 @@ type ( // syncConfig holds all the configuration about the different sync processes syncConfig struct { - excludedProviders []string // List of provider names - httpClient HTTPInterface // Custom HTTP client (Minercraft, WOC) - minercraftConfig *minercraftConfig // minercraftConfig configuration - minercraft minercraft.ClientInterface // Minercraft client - network Network // Current network (mainnet, testnet, stn) - nowNodes nownodes.ClientInterface // NOWNodes client - nowNodesAPIKey string // If set, use this key - queryTimeout time.Duration // Timeout for transaction query - whatsOnChain whatsonchain.ClientInterface // WhatsOnChain client - whatsOnChainAPIKey string // If set, use this key + excludedProviders []string // List of provider names + httpClient HTTPInterface // Custom HTTP client (Minercraft, WOC) + minercraftConfig *minercraftConfig // minercraftConfig configuration + minercraft minercraft.ClientInterface // Minercraft client + network Network // Current network (mainnet, testnet, stn) + nowNodes nownodes.ClientInterface // NOWNodes client + nowNodesAPIKey string // If set, use this key + queryTimeout time.Duration // Timeout for transaction query + whatsOnChain whatsonchain.ClientInterface // WhatsOnChain client + whatsOnChainAPIKey string // If set, use this key + broadcastClient broadcast.Client // Broadcast client + broadcastClientConfig *broadcastClientConfig // Broadcast client config } // minercraftConfig is specific for minercraft configuration @@ -63,6 +67,11 @@ type ( FeeUnit *utils.FeeUnit `json:"fee_unit"` // The fee unit returned from Policy request Miner *minercraft.Miner `json:"miner"` // The minercraft miner } + + // broadcastClientConfig is specific for broadcast client configuration + broadcastClientConfig struct { + BroadcastClientApis []broadcastClient.ArcClientConfig `json:"broadcast_client_apis"` // List of broadcast client apis + } ) // NewClient creates a new client for all on-chain functionality @@ -184,6 +193,11 @@ func (c *Client) NowNodes() nownodes.ClientInterface { return c.options.config.nowNodes } +// BroadcastClient will return the BroadcastClient client +func (c *Client) BroadcastClient() broadcast.Client { + return c.options.config.broadcastClient +} + // QueryTimeout will return the query timeout func (c *Client) QueryTimeout() time.Duration { return c.options.config.queryTimeout diff --git a/chainstate/client_options.go b/chainstate/client_options.go index 6af26ec6..e5cec019 100644 --- a/chainstate/client_options.go +++ b/chainstate/client_options.go @@ -2,6 +2,8 @@ package chainstate import ( "context" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + broadcastClient "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" "time" zLogger "github.com/mrz1836/go-logger" @@ -35,10 +37,14 @@ func defaultClientOptions() *clientOptions { minercraftFeeQuotes: true, feeUnit: DefaultFee, }, - minercraft: nil, - network: MainNet, - queryTimeout: defaultQueryTimeOut, - whatsOnChain: nil, + minercraft: nil, + network: MainNet, + queryTimeout: defaultQueryTimeOut, + whatsOnChain: nil, + broadcastClient: nil, + broadcastClientConfig: &broadcastClientConfig{ + BroadcastClientApis: nil, + }, }, debug: false, newRelicEnabled: false, @@ -260,3 +266,17 @@ func WithMinercraftAPIs(apis []*minercraft.MinerAPIs) ClientOps { c.config.minercraftConfig.minerAPIs = apis } } + +// WithBroadcastClient will set broadcast client APIs +func WithBroadcastClient(client broadcast.Client) ClientOps { + return func(c *clientOptions) { + c.config.broadcastClient = client + } +} + +// WithBroadcastClientAPIs will set broadcast client APIs +func WithBroadcastClientAPIs(apis []broadcastClient.ArcClientConfig) ClientOps { + return func(c *clientOptions) { + c.config.broadcastClientConfig.BroadcastClientApis = apis + } +} diff --git a/chainstate/definitions.go b/chainstate/definitions.go index 5328eb6d..13d8f612 100644 --- a/chainstate/definitions.go +++ b/chainstate/definitions.go @@ -44,10 +44,11 @@ const ( // List of providers const ( - ProviderAll = "all" // All providers (used for errors etc) - ProviderMAPI = "mapi" // Query & broadcast provider for mAPI (using given miners) - ProviderNowNodes = "nownodes" // Query & broadcast provider for NowNodes - ProviderWhatsOnChain = "whatsonchain" // Query & broadcast provider for WhatsOnChain + ProviderAll = "all" // All providers (used for errors etc) + ProviderMAPI = "mapi" // Query & broadcast provider for mAPI (using given miners) + ProviderNowNodes = "nownodes" // Query & broadcast provider for NowNodes + ProviderWhatsOnChain = "whatsonchain" // Query & broadcast provider for WhatsOnChain + ProviderBroadcastClient = "broadcastclient" // Query & broadcast provider for configured miners ) // TransactionInfo is the universal information about the transaction found from a chain provider diff --git a/chainstate/interface.go b/chainstate/interface.go index d4a7de82..b74c43b7 100644 --- a/chainstate/interface.go +++ b/chainstate/interface.go @@ -2,6 +2,7 @@ package chainstate import ( "context" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" "net/http" "time" @@ -40,6 +41,7 @@ type ProviderServices interface { Minercraft() minercraft.ClientInterface NowNodes() nownodes.ClientInterface WhatsOnChain() whatsonchain.ClientInterface + BroadcastClient() broadcast.Client } // MinercraftServices is the minercraft services interface diff --git a/chainstate/transaction.go b/chainstate/transaction.go index eae188a6..3e99ecd3 100644 --- a/chainstate/transaction.go +++ b/chainstate/transaction.go @@ -54,6 +54,17 @@ func (c *Client) query(ctx context.Context, id string, requiredIn RequiredIn, } } + // Next: try with BroadcastClient (if loaded) + if !utils.StringInSlice(ProviderBroadcastClient, c.options.config.excludedProviders) { + if c.BroadcastClient() != nil { + if resp, err := queryBroadcastClient( + ctxWithCancel, c, id, + ); err == nil && checkRequirement(requiredIn, id, resp) { + return resp + } + } + } + // No transaction information found return nil } @@ -122,6 +133,20 @@ func (c *Client) fastestQuery(ctx context.Context, id string, requiredIn Require } } + if !utils.StringInSlice(ProviderBroadcastClient, c.options.config.excludedProviders) { + if c.BroadcastClient() != nil { + wg.Add(1) + go func(ctx context.Context, client *Client, id string, requiredIn RequiredIn) { + defer wg.Done() + if resp, err := queryBroadcastClient( + ctx, client, id, + ); err == nil && checkRequirement(requiredIn, id, resp) { + resultsChannel <- resp + } + }(ctxWithCancel, c, id, requiredIn) + } + } + // Waiting for all requests to finish go func() { wg.Wait() @@ -187,3 +212,20 @@ func queryNowNodes(ctx context.Context, client ClientInterface, id string) (*Tra } return nil, ErrTransactionIDMismatch } + +// queryBroadcastClient will submit a query transaction request to a go-broadcast-client +func queryBroadcastClient(ctx context.Context, client ClientInterface, id string) (*TransactionInfo, error) { + client.DebugLog("executing request in minercraft using " + ProviderBroadcastClient) + if resp, err := client.BroadcastClient().QueryTransaction(ctx, id); err != nil { + client.DebugLog("error executing request in " + ProviderBroadcastClient + " failed: " + err.Error()) + return nil, err + } else if resp != nil && strings.EqualFold(resp.TxID, id) { + return &TransactionInfo{ + BlockHash: resp.BlockHash, + BlockHeight: resp.BlockHeight, + ID: resp.TxID, + Provider: resp.Miner, + }, nil + } + return nil, ErrTransactionIDMismatch +} diff --git a/client_options.go b/client_options.go index 806253a8..c1d7c35e 100644 --- a/client_options.go +++ b/client_options.go @@ -3,6 +3,8 @@ package bux import ( "context" "database/sql" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + broadcast_client "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" "net/http" "strings" "time" @@ -744,3 +746,17 @@ func WithMinercraftAPIs(miners []*minercraft.MinerAPIs) ClientOps { c.chainstate.options = append(c.chainstate.options, chainstate.WithMinercraftAPIs(miners)) } } + +// WithBroadcastClient will set broadcast client +func WithBroadcastClient(broadcastClient broadcast.Client) ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithBroadcastClient(broadcastClient)) + } +} + +// WithBroadcastClientAPIs will set broadcast client APIs +func WithBroadcastClientAPIs(apis []broadcast_client.ArcClientConfig) ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithBroadcastClientAPIs(apis)) + } +} diff --git a/go.mod b/go.mod index 7ea2a4fe..ae145bf7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/99designs/gqlgen v0.17.36 github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/bitcoin-sv/go-broadcast-client v0.0.0-20230822135329-75a90170644a github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 github.com/bitcoinschema/go-map v0.1.0 github.com/centrifugal/centrifuge-go v0.10.1 diff --git a/go.sum b/go.sum index d45d18db..218e9643 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE= +github.com/bitcoin-sv/go-broadcast-client v0.0.0-20230822135329-75a90170644a h1:5qGe/VArjvjPq8YgdjazFxUGmvJtxADItnpo5+rOOIE= +github.com/bitcoin-sv/go-broadcast-client v0.0.0-20230822135329-75a90170644a/go.mod h1:EGTrUK2r81moZbgaNalEZJaIBYp7/kOVXy17J9AFQJQ= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fMuWYhNoZa1tck= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinschema/go-bob v0.4.0 h1:adsAEboLQCg0D6e9vwcJUJEJScszsouAYCYu35UAiGo= diff --git a/model_sync_transactions.go b/model_sync_transactions.go index ec7ed073..0f51fcf7 100644 --- a/model_sync_transactions.go +++ b/model_sync_transactions.go @@ -672,7 +672,7 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa transaction.BlockHeight = uint64(txInfo.BlockHeight) // Create status message - message := "transaction was found on-chain by " + txInfo.Provider + message := "transaction was found on-chain by " + chainstate.ProviderBroadcastClient // Save the transaction (should NOT error) if err = transaction.Save(ctx); err != nil { @@ -688,7 +688,7 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa syncTx.Results.Results = append(syncTx.Results.Results, &SyncResult{ Action: syncActionSync, ExecutedAt: time.Now().UTC(), - Provider: txInfo.Provider, + Provider: chainstate.ProviderBroadcastClient, StatusMessage: message, }) From d86329f2e9a72f48b51c56d561ddc0ad6a90c94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lewandowski?= Date: Wed, 23 Aug 2023 14:44:17 +0200 Subject: [PATCH 2/2] fix(BUX-147): fix tests --- mock_chainstate_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mock_chainstate_test.go b/mock_chainstate_test.go index 39d89c7b..b6d59c1e 100644 --- a/mock_chainstate_test.go +++ b/mock_chainstate_test.go @@ -2,6 +2,7 @@ package bux import ( "context" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" "time" "github.com/BuxOrg/bux/chainstate" @@ -125,6 +126,10 @@ func (c *chainStateEverythingOnChain) Monitor() chainstate.MonitorService { return nil } +func (c *chainStateEverythingOnChain) BroadcastClient() broadcast.Client { + return nil +} + func (c *chainStateEverythingOnChain) QueryTransaction(_ context.Context, id string, _ chainstate.RequiredIn, _ time.Duration) (*chainstate.TransactionInfo, error) {