diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index de73bfd5..83a2b0b5 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -5,12 +5,10 @@ env: GO111MODULE: on on: - pull_request: - branches: - - "*" push: - branches: - - "*" + branches-ignore: + - main + - master jobs: yamllint: diff --git a/action_destination.go b/action_destination.go index 7e21bf22..ee2d14a3 100644 --- a/action_destination.go +++ b/action_destination.go @@ -78,7 +78,7 @@ func (c *Client) NewDestinationForLockingScript(ctx context.Context, xPubID, loc } // set the monitoring, passed down from the initiating function - // this will be set when calling NewDestination from http / graphql, but not for instance paymail + // this will be set when calling NewDestination from http, but not for instance paymail if monitor { destination.Monitor = customTypes.NullTime{NullTime: sql.NullTime{ Valid: true, diff --git a/chainstate/client.go b/chainstate/client.go index 911d822e..db6f81bb 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -40,7 +40,7 @@ type ( network Network // Current network (mainnet, testnet, stn) queryTimeout time.Duration // Timeout for transaction query broadcastClient broadcast.Client // Broadcast client - pulseClient *PulseClient // Pulse client + pulseClient *pulseClientProvider // 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 } @@ -53,12 +53,6 @@ type ( apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) minerAPIs []*minercraft.MinerAPIs // List of miners APIs } - - // PulseClient is the internal chainstate pulse client - PulseClient struct { - url string - authToken string - } ) // NewClient creates a new client for all on-chain functionality diff --git a/chainstate/client_options.go b/chainstate/client_options.go index 2497091d..b9dd62e0 100644 --- a/chainstate/client_options.go +++ b/chainstate/client_options.go @@ -181,6 +181,6 @@ func WithBroadcastClient(client broadcast.Client) ClientOps { // WithConnectionToPulse will set pulse API settings. func WithConnectionToPulse(url, authToken string) ClientOps { return func(c *clientOptions) { - c.config.pulseClient = &PulseClient{url, authToken} + c.config.pulseClient = newPulseClientProvider(url, authToken) } } diff --git a/chainstate/merkle_root.go b/chainstate/merkle_root.go index 462ad4a4..819064a2 100644 --- a/chainstate/merkle_root.go +++ b/chainstate/merkle_root.go @@ -3,19 +3,18 @@ package chainstate import ( "context" "errors" - "fmt" ) // VerifyMerkleRoots will try to verify merkle roots with all available providers +// When no error is returned, it means that the pulse client responded with state: Confirmed or UnableToVerify func (c *Client) VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRootConfirmationRequestItem) error { - if c.options.config.pulseClient == nil { + pc := c.options.config.pulseClient + if pc == nil { c.options.logger.Warn().Msg("VerifyMerkleRoots is called even though no pulse client is configured; this likely indicates that the paymail capabilities have been cached.") return errors.New("no pulse client found") } - pulseProvider := createPulseProvider(c) - merkleRootsRes, err := pulseProvider.verifyMerkleRoots(ctx, c, merkleRoots) + merkleRootsRes, err := pc.verifyMerkleRoots(ctx, c.options.logger, merkleRoots) if err != nil { - debugLog(c, "", fmt.Sprintf("verify merkle root error: %s from Pulse", err)) return err } @@ -30,10 +29,3 @@ func (c *Client) VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRoot return nil } - -func createPulseProvider(c *Client) pulseClientProvider { - return pulseClientProvider{ - url: c.options.config.pulseClient.url, - authToken: c.options.config.pulseClient.authToken, - } -} diff --git a/chainstate/merkle_root_provider.go b/chainstate/merkle_root_provider.go index 19b024ee..08ae2690 100644 --- a/chainstate/merkle_root_provider.go +++ b/chainstate/merkle_root_provider.go @@ -5,8 +5,9 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" + + "github.com/rs/zerolog" ) // MerkleRootConfirmationState represents the state of each Merkle Root verification @@ -46,47 +47,63 @@ type MerkleRootsConfirmationsResponse struct { } type pulseClientProvider struct { - url string - authToken string + url string + authToken string + httpClient *http.Client +} + +func newPulseClientProvider(url, authToken string) *pulseClientProvider { + return &pulseClientProvider{url: url, authToken: authToken, httpClient: &http.Client{}} } -// verifyMerkleProof using Pulse -func (p pulseClientProvider) verifyMerkleRoots( +func (p *pulseClientProvider) verifyMerkleRoots( ctx context.Context, - c *Client, merkleRoots []MerkleRootConfirmationRequestItem, + logger *zerolog.Logger, + merkleRoots []MerkleRootConfirmationRequestItem, ) (*MerkleRootsConfirmationsResponse, error) { jsonData, err := json.Marshal(merkleRoots) if err != nil { - return nil, err + return nil, _fmtAndLogError(err, logger, "Error occurred while marshaling merkle roots.") } - client := &http.Client{} req, err := http.NewRequestWithContext(ctx, "POST", p.url, bytes.NewBuffer(jsonData)) if err != nil { - return nil, err + return nil, _fmtAndLogError(err, logger, "Error occurred while creating request for the pulse client.") } if p.authToken != "" { req.Header.Set("Authorization", "Bearer "+p.authToken) } - res, err := client.Do(req) + res, err := p.httpClient.Do(req) + if res != nil { + defer func() { + _ = res.Body.Close() + }() + } if err != nil { - c.options.logger.Error().Msgf("Error during creating connection to pulse client: %s", err.Error()) - return nil, err + return nil, _fmtAndLogError(err, logger, "Error occurred while sending request to the Pulse service.") } - defer res.Body.Close() //nolint: all // Close the body - // Parse response body. - var merkleRootsRes MerkleRootsConfirmationsResponse - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("error during reading response body: %s", err.Error()) + if res.StatusCode != 200 { + return nil, _fmtAndLogError(_statusError(res.StatusCode), logger, "Received unexpected status code from Pulse service.") } - err = json.Unmarshal(bodyBytes, &merkleRootsRes) + // Parse response body. + var merkleRootsRes MerkleRootsConfirmationsResponse + err = json.NewDecoder(res.Body).Decode(&merkleRootsRes) if err != nil { - return nil, fmt.Errorf("error during unmarshalling response body: %s", err.Error()) + return nil, _fmtAndLogError(err, logger, "Error occurred while parsing response from the Pulse service.") } return &merkleRootsRes, nil } + +// _fmtAndLogError returns brief error for http response message and logs detailed information with original error +func _fmtAndLogError(err error, logger *zerolog.Logger, message string) error { + logger.Error().Err(err).Msg("[verifyMerkleRoots] " + message) + return fmt.Errorf("cannot verify transaction - %s", message) +} + +func _statusError(statusCode int) error { + return fmt.Errorf("pulse client returned status code %d - check Pulse configuration and service status", statusCode) +} diff --git a/chainstate/merkle_root_test.go b/chainstate/merkle_root_test.go new file mode 100644 index 00000000..e05a8157 --- /dev/null +++ b/chainstate/merkle_root_test.go @@ -0,0 +1,127 @@ +package chainstate + +import ( + "bytes" + "context" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func initMockClient(ops ...ClientOps) (*Client, *buffLogger) { + bLogger := newBuffLogger() + ops = append(ops, WithLogger(bLogger.logger)) + c, _ := NewClient( + context.Background(), + ops..., + ) + return c.(*Client), bLogger +} + +func TestVerifyMerkleRoots(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + mockURL := "http://pulse.test/api/v1/chain/merkleroot/verify" + + t.Run("no pulse client", func(t *testing.T) { + c, _ := initMockClient() + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + }) + + t.Run("pulse is not online", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewStringResponder(500, `{"error":"Internal Server Error"}`), + ) + c, bLogger := initMockClient(WithConnectionToPulse(mockURL, "")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, bLogger.contains("pulse client returned status code 500")) + }) + + t.Run("pulse wrong auth", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewStringResponder(401, `Unauthorized`), + ) + c, bLogger := initMockClient(WithConnectionToPulse(mockURL, "some-token")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, bLogger.contains("401")) + }) + + t.Run("pulse invalid state", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewJsonResponderOrPanic(200, MerkleRootsConfirmationsResponse{ + ConfirmationState: Invalid, + Confirmations: []MerkleRootConfirmation{}, + }), + ) + c, bLogger := initMockClient(WithConnectionToPulse(mockURL, "some-token")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, bLogger.contains("Not all merkle roots confirmed")) + }) + + t.Run("pulse confirmedState", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewJsonResponderOrPanic(200, MerkleRootsConfirmationsResponse{ + ConfirmationState: Confirmed, + Confirmations: []MerkleRootConfirmation{ + { + Hash: "some-hash", + BlockHeight: 1, + MerkleRoot: "some-merkle-root", + Confirmation: Confirmed, + }, + }, + }), + ) + c, bLogger := initMockClient(WithConnectionToPulse(mockURL, "some-token")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{ + { + MerkleRoot: "some-merkle-root", + BlockHeight: 1, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.False(t, bLogger.contains("ERR")) + assert.False(t, bLogger.contains("WARN")) + }) +} + +// buffLogger allows to check if a certain string was logged +type buffLogger struct { + logger *zerolog.Logger + buf *bytes.Buffer +} + +func newBuffLogger() *buffLogger { + var buf bytes.Buffer + logger := zerolog.New(&buf).Level(zerolog.DebugLevel).With().Logger() + return &buffLogger{logger: &logger, buf: &buf} +} + +func (l *buffLogger) contains(expected string) bool { + return bytes.Contains(l.buf.Bytes(), []byte(expected)) +} diff --git a/errors.go b/errors.go index 17562034..6beabed5 100644 --- a/errors.go +++ b/errors.go @@ -104,7 +104,7 @@ var ErrUtxoNotReserved = errors.New("transaction utxo has not been reserved for var ErrDraftIDMismatch = errors.New("transaction draft id does not match utxo draft reservation id") // ErrMissingTxHex is when the hex is missing or invalid and creates an empty id -var ErrMissingTxHex = errors.New("transaction hex is empty or id is missing") +var ErrMissingTxHex = errors.New("transaction hex is invalid or id is missing") // ErrMissingBlockHeaderHash is when the hash is missing or invalid and creates an empty id var ErrMissingBlockHeaderHash = errors.New("block header hash is empty or id is missing") diff --git a/go.mod b/go.mod index e76d4128..1a91ebd1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/BuxOrg/bux go 1.21.5 require ( - github.com/99designs/gqlgen v0.17.42 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/bitcoin-sv/go-broadcast-client v0.16.0 github.com/bitcoin-sv/go-paymail v0.12.1 @@ -42,6 +41,7 @@ require ( ) require ( + github.com/99designs/gqlgen v0.17.42 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/bitcoinschema/go-bpu v0.1.3 // indirect github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 // indirect diff --git a/model_ids.go b/model_ids.go index db47f2b8..22180364 100644 --- a/model_ids.go +++ b/model_ids.go @@ -4,7 +4,6 @@ import ( "database/sql/driver" "encoding/json" - "github.com/99designs/gqlgen/graphql" "github.com/BuxOrg/bux/utils" "github.com/mrz1836/go-datastore" "gorm.io/gorm" @@ -53,23 +52,3 @@ func (IDs) GormDBDataType(db *gorm.DB, _ *schema.Field) string { } return datastore.JSON } - -// UnmarshalIDs will marshal the custom type -func UnmarshalIDs(v interface{}) (IDs, error) { - if v == nil { - return nil, nil - } - - // Try to unmarshal - ids, err := graphql.UnmarshalAny(v) - if err != nil { - return nil, err - } - - // Cast interface back to IDs (safely) - if idCast, ok := ids.(IDs); ok { - return idCast, nil - } - - return nil, ErrCannotConvertToIDs -} diff --git a/model_ids_test.go b/model_ids_test.go index 13490cd8..fafd7c01 100644 --- a/model_ids_test.go +++ b/model_ids_test.go @@ -75,32 +75,3 @@ func TestIDs_Value(t *testing.T) { assert.Equal(t, "[\"test1\"]", value) }) } - -// TestUnmarshalIDs will test the method UnmarshalIDs() -func TestUnmarshalIDs(t *testing.T) { - - t.Parallel() - - t.Run("nil value", func(t *testing.T) { - i, err := UnmarshalIDs(nil) - require.NoError(t, err) - assert.Equal(t, 0, len(i)) - assert.IsType(t, IDs{}, i) - }) - - t.Run("empty string", func(t *testing.T) { - i, err := UnmarshalIDs("\"\"") - assert.Error(t, err) - assert.Equal(t, 0, len(i)) - assert.IsType(t, IDs{}, i) - }) - - t.Run("valid set of ids", func(t *testing.T) { - i, err := UnmarshalIDs(IDs{"test1", "test2"}) - require.NoError(t, err) - assert.Equal(t, 2, len(i)) - assert.IsType(t, IDs{}, i) - assert.Equal(t, "test1", i[0]) - assert.Equal(t, "test2", i[1]) - }) -} diff --git a/model_metadata.go b/model_metadata.go index af2091ee..16fe44de 100644 --- a/model_metadata.go +++ b/model_metadata.go @@ -5,7 +5,6 @@ import ( "database/sql/driver" "encoding/json" - "github.com/99designs/gqlgen/graphql" "github.com/BuxOrg/bux/utils" "github.com/mrz1836/go-datastore" "go.mongodb.org/mongo-driver/bson" @@ -186,19 +185,3 @@ func (x *XpubMetadata) UnmarshalBSONValue(t bsontype.Type, data []byte) error { return nil } - -// MarshalMetadata will marshal the custom type -func MarshalMetadata(m Metadata) graphql.Marshaler { - if m == nil { - return graphql.Null - } - return graphql.MarshalMap(m) -} - -// UnmarshalMetadata will unmarshal the custom type -func UnmarshalMetadata(v interface{}) (Metadata, error) { - if v == nil { - return nil, nil - } - return graphql.UnmarshalMap(v) -} diff --git a/model_metadata_test.go b/model_metadata_test.go index 45523cff..256d0a56 100644 --- a/model_metadata_test.go +++ b/model_metadata_test.go @@ -1,7 +1,6 @@ package bux import ( - "bytes" "encoding/hex" "encoding/json" "testing" @@ -124,64 +123,6 @@ func TestXpubMetadata_Value(t *testing.T) { }) } -// TestMetaDataScan will test the db Scanner of the Metadata model -func TestMetadata_UnmarshalMetadata(t *testing.T) { - t.Parallel() - - t.Run("nil value", func(t *testing.T) { - m, err := UnmarshalMetadata(nil) - require.NoError(t, err) - assert.Equal(t, 0, len(m)) - assert.IsType(t, Metadata{}, m) - }) - - t.Run("empty string", func(t *testing.T) { - m, err := UnmarshalMetadata("\"\"") - assert.Error(t, err) - assert.Equal(t, 0, len(m)) - assert.IsType(t, Metadata{}, m) - }) - - t.Run("empty string - incorrectly coded", func(t *testing.T) { - m, err := UnmarshalMetadata("") - assert.Error(t, err) - assert.Equal(t, 0, len(m)) - assert.IsType(t, Metadata{}, m) - }) - - t.Run("object", func(t *testing.T) { - m, err := UnmarshalMetadata(map[string]interface{}{"test": "test2"}) - require.NoError(t, err) - assert.Equal(t, 1, len(m)) - assert.IsType(t, Metadata{}, m) - assert.Equal(t, "test2", m["test"]) - }) -} - -// TestMetadata_MarshalMetadata will test the db Valuer of the Metadata model -func TestMetadata_MarshalMetadata(t *testing.T) { - t.Parallel() - - t.Run("empty object", func(t *testing.T) { - m := Metadata{} - writer := MarshalMetadata(m) - require.NotNil(t, writer) - b := bytes.NewBufferString("") - writer.MarshalGQL(b) - assert.Equal(t, "{}\n", b.String()) - }) - - t.Run("map present", func(t *testing.T) { - m := Metadata{} - m["test"] = "test2" - writer := MarshalMetadata(m) - require.NotNil(t, writer) - b := bytes.NewBufferString("") - writer.MarshalGQL(b) - assert.Equal(t, "{\"test\":\"test2\"}\n", b.String()) - }) -} - // TestMetadata_MarshalBSONValue will test the method MarshalBSONValue() func TestMetadata_MarshalBSONValue(t *testing.T) { t.Parallel()