Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' of https://github.com/BuxOrg/bux into feat/BUX-…
Browse files Browse the repository at this point in the history
…480/RecordTransaction
  • Loading branch information
Nazarii-4chain committed Feb 1, 2024
2 parents 058ff37 + fbb4bba commit 015fb28
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 174 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ env:
GO111MODULE: on

on:
pull_request:
branches:
- "*"
push:
branches:
- "*"
branches-ignore:
- main
- master

jobs:
yamllint:
Expand Down
2 changes: 1 addition & 1 deletion action_destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 1 addition & 7 deletions chainstate/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion chainstate/client_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
16 changes: 4 additions & 12 deletions chainstate/merkle_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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,
}
}
57 changes: 37 additions & 20 deletions chainstate/merkle_root_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
127 changes: 127 additions & 0 deletions chainstate/merkle_root_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
2 changes: 1 addition & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 0 additions & 21 deletions model_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 015fb28

Please sign in to comment.