diff --git a/bhserrors/definitions.go b/bhserrors/definitions.go new file mode 100644 index 00000000..81b0bad0 --- /dev/null +++ b/bhserrors/definitions.go @@ -0,0 +1,105 @@ +package bhserrors + +// ////////////////////////////////// GENERIC ERRORS + +// ErrGeneric is a generic error that something went wrong +var ErrGeneric = BHSError{Message: "internal server error", StatusCode: 500, Code: "ErrGeneric"} + +// ErrBindBody is an error when it fails to bind JSON body +var ErrBindBody = BHSError{Message: "error during bind JSON body", StatusCode: 400, Code: "ErrBindBody"} + +// ////////////////////////////////// AUTH ERRORS + +// ErrMissingAuthHeader is when request does not have auth header +var ErrMissingAuthHeader = BHSError{Message: "empty auth header", StatusCode: 401, Code: "ErrMissingAuthHeader"} + +// ErrInvalidAuthHeader is when request does not have a valid auth header +var ErrInvalidAuthHeader = BHSError{Message: "invalid auth header", StatusCode: 401, Code: "ErrInvalidAuthHeader"} + +// ErrInvalidAccessToken is when access token is invalid +var ErrInvalidAccessToken = BHSError{Message: "invalid access token", StatusCode: 401, Code: "ErrInvalidAccessToken"} + +// ErrUnauthorized is a generic error when user is unauthorized to make a request +var ErrUnauthorized = BHSError{Message: "not authorized", StatusCode: 401, Code: "ErrUnauthorized"} + +// ErrAdminTokenNotFound is when admin token was not found in Block Header Service +var ErrAdminTokenNotFound = BHSError{Message: "admin token not found", StatusCode: 401, Code: "ErrAdminTokenNotFound"} + +// ////////////////////////////////// MERKLE ROOTS ERRORS + +// ErrMerklerootNotFound is when provided merkleroot from user was not found in Block Header Service's database +var ErrMerklerootNotFound = BHSError{Message: "no block with provided merkleroot was found", Code: "ErrMerkleRootNotFound", StatusCode: 404} + +// ErrMerklerootNotInLongestChain is when provided merkleroot from user was found in Block Header Service's database but is not in Longest Chain state +var ErrMerklerootNotInLongestChain = BHSError{Message: "provided merkleroot is not part of the longest chain", Code: "ErrMerkleRootNotInLongestChain", StatusCode: 409} + +// ErrInvalidBatchSize is when user provided incorrect batchSize +var ErrInvalidBatchSize = BHSError{Message: "batchSize must be 0 or a positive integer", Code: "ErrInvalidBatchSize", StatusCode: 400} + +// ErrGetChainTipHeight is when it fails to get a chain tip height +var ErrGetChainTipHeight = BHSError{Message: "failed to get chain tip height", Code: "ErrGetChainTipHeight", StatusCode: 400} + +// ErrVerifyMerklerootsBadBody is when request for verify merkleroots has wrong body +var ErrVerifyMerklerootsBadBody = BHSError{Message: "at least one merkleroot is required", Code: "ErrVerifyMerklerootsBadBody", StatusCode: 400} + +// ////////////////////////////////// ACCESS ERRORS + +// ErrTokenNotFound is when token was not found in Block Header Service +var ErrTokenNotFound = BHSError{Message: "token not found", StatusCode: 404, Code: "ErrTokenNotFound"} + +// ErrCreateToken is when create token fails +var ErrCreateToken = BHSError{Message: "failed to create new token", StatusCode: 400, Code: "ErrCreateToken"} + +// ErrDeleteToken is when delete token fails +var ErrDeleteToken = BHSError{Message: "failed to delete token", StatusCode: 400, Code: "ErrDeleteToken"} + +// ////////////////////////////////// HEADERS ERRORS + +// ErrAncestorHashHigher is when ancestor hash height is higher than requested header +var ErrAncestorHashHigher = BHSError{Message: "ancestor header height can not be higher than requested header height", StatusCode: 400, Code: "ErrAncestorHashHigher"} + +// ErrAncestorNotFound is when ancestor for a given hash was not found +var ErrAncestorNotFound = BHSError{Message: "failed to get ancestor with given hash ", StatusCode: 400, Code: "ErrAncestorNotFound"} + +// ErrHeadersNotPartOfTheSameChain is when provided headers are not part of the same chain +var ErrHeadersNotPartOfTheSameChain = BHSError{Message: "the headers provided are not part of the same chain", StatusCode: 400, Code: "ErrHeadersNotPartOfTheSameChain"} + +// ErrHeaderWithGivenHashes is when getting header with given hashes fails +var ErrHeaderWithGivenHashes = BHSError{Message: "error during getting headers with given hashes", StatusCode: 400, Code: "ErrHeaderWithGivenHashes"} + +// ErrHeaderNotFound is when hash could not be found +var ErrHeaderNotFound = BHSError{Message: "header not found", StatusCode: 404, Code: "ErrHeaderNotFound"} + +// ErrHeadersForGivenRangeNotFound is when hash could not be found for given range +var ErrHeadersForGivenRangeNotFound = BHSError{Message: "could not find headers in given range", StatusCode: 404, Code: "ErrHeadersForGivenRangeNotFound"} + +// ErrHeaderStopHeightNotFound is when stop height for given heade was not found +var ErrHeaderStopHeightNotFound = BHSError{Message: "could not find stop height for given header", StatusCode: 404, Code: "ErrHeaderStopHeightNotFound"} + +// ////////////////////////////////// TIPS ERRORS + +// ErrGetTips is when it fails to get tips +var ErrGetTips = BHSError{Message: "failed to get tips", StatusCode: 400, Code: "ErrGetTips"} + +// ////////////////////////////////// WEBHOOK ERRORS + +// ErrURLBodyRequired is when url is not provided in body +var ErrURLBodyRequired = BHSError{Message: "url is required", StatusCode: 400, Code: "ErrURLBodyRequired"} + +// ErrURLParamRequired is when url is not provided in param +var ErrURLParamRequired = BHSError{Message: "url is required", StatusCode: 400, Code: "ErrURLParamRequired"} + +// ErrCreateWebhook is when it fails to create a webhook +var ErrCreateWebhook = BHSError{Message: "failed to create a webhook", StatusCode: 400, Code: "ErrCreateWebhook"} + +// ErrWebhookNotFound is when webhook was not found +var ErrWebhookNotFound = BHSError{Message: "webhook not found", StatusCode: 404, Code: "ErrWebhookNotFound"} + +// ErrRefreshWebhook is when a webhook already exists and is active and we tried to refresh it +var ErrRefreshWebhook = BHSError{Message: "webhook already exists and is active", StatusCode: 400, Code: "ErrRefreshWebhook"} + +// ErrGetAllWebhooks is when it failed to get all webhooks +var ErrGetAllWebhooks = BHSError{Message: "failed to get all webhooks", StatusCode: 400, Code: "ErrGetAllWebhooks"} + +// ErrDeleteWebhook is when it failed to delete a webhook +var ErrDeleteWebhook = BHSError{Message: "failed to delete webhook", StatusCode: 400, Code: "ErrDeleteWebhook"} diff --git a/bhserrors/errors.go b/bhserrors/errors.go new file mode 100644 index 00000000..bce4da45 --- /dev/null +++ b/bhserrors/errors.go @@ -0,0 +1,90 @@ +package bhserrors + +import ( + "github.com/pkg/errors" +) + +type stackTracer interface { + StackTrace() errors.StackTrace +} + +// ExtendedError is an interface for errors that hold information about http status and code that should be returned +type ExtendedError interface { + error + GetCode() string + GetMessage() string + GetStatusCode() int + StackTrace() (trace errors.StackTrace) +} + +// BHSError is extended error which holds information about http status and code that should be returned from Block Header Service +type BHSError struct { + Code string + Message string + StatusCode int + cause error +} + +// ResponseError is an error which will be returned in HTTP response +type ResponseError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// Error returns the error message string for BHSError, satisfying the error interface +func (e BHSError) Error() string { + return e.Message +} + +// GetCode returns the error code string for BHSError +func (e BHSError) GetCode() string { + return e.Code +} + +// GetMessage returns the error message string for BHSError +func (e BHSError) GetMessage() string { + return e.Message +} + +// GetStatusCode returns the error status code for BHSError +func (e BHSError) GetStatusCode() int { + return e.StatusCode +} + +// StackTrace returns the error's stack trace. +func (e BHSError) StackTrace() errors.StackTrace { + err, ok := e.cause.(stackTracer) + if !ok { + return nil + } + + return err.StackTrace() +} + +// Unwrap returns the "cause" error +func (e BHSError) Unwrap() error { + return e.cause +} + +// Wrap sets the "cause" error +func (e BHSError) Wrap(err error) BHSError { + e.cause = err + return e +} + +// WithTrace save the stack trace of the error +func (e BHSError) WithTrace(err error) BHSError { + if st := stackTracer(nil); !errors.As(e.cause, &st) { + return e.Wrap(errors.WithStack(err)) + } + return e.Wrap(err) +} + +// Is checks if the target error is the same as the current error +func (e BHSError) Is(target error) bool { + t, ok := target.(BHSError) + if !ok { + return false + } + return e.Code == t.Code +} diff --git a/bhserrors/http_response.go b/bhserrors/http_response.go new file mode 100644 index 00000000..fc073f46 --- /dev/null +++ b/bhserrors/http_response.go @@ -0,0 +1,53 @@ +package bhserrors + +import ( + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// UnknownErrorCode is a constant for unknown error code +const UnknownErrorCode = "error-unknown" + +// ErrorResponse is searching for error and setting it up in gin context +func ErrorResponse(c *gin.Context, err error, log *zerolog.Logger) { + response, statusCode := mapAndLog(err, log) + c.JSON(statusCode, response) +} + +// AbortWithErrorResponse is searching for error and abort with error set +func AbortWithErrorResponse(c *gin.Context, err error, log *zerolog.Logger) { + response, statusCode := mapAndLog(err, log) + c.AbortWithStatusJSON(statusCode, response) +} + +func mapAndLog(err error, log *zerolog.Logger) (model ResponseError, statusCode int) { + model.Code = UnknownErrorCode + model.Message = "Internal server error" + statusCode = 500 + + logLevel := zerolog.WarnLevel + exposedInternalError := false + var extendedErr ExtendedError + if errors.As(err, &extendedErr) { + model.Code = extendedErr.GetCode() + model.Message = extendedErr.GetMessage() + statusCode = extendedErr.GetStatusCode() + if statusCode >= 500 { + logLevel = zerolog.ErrorLevel + } + } else { + // we should wrap all internal errors into BHSError (with proper code, message and status code) + // if you find out that some endpoint produces this warning, feel free to fix it + exposedInternalError = true + } + + if log != nil { + logInstance := log.WithLevel(logLevel).Str("module", "block-header-error") + if exposedInternalError { + logInstance.Str("warning", "internal error returned as HTTP response") + } + logInstance.Err(err).Msgf("Error HTTP response, returning %d", statusCode) + } + return +} diff --git a/database/sql/headers.go b/database/sql/headers.go index 26dd345a..822316b4 100644 --- a/database/sql/headers.go +++ b/database/sql/headers.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/repository/dto" "github.com/jmoiron/sqlx" @@ -278,10 +279,7 @@ func (h *HeadersDb) Count(ctx context.Context) (int, error) { func (h *HeadersDb) GetHeaderByHash(ctx context.Context, hash string) (*dto.DbBlockHeader, error) { var bh dto.DbBlockHeader if err := h.db.GetContext(ctx, &bh, h.db.Rebind(sqlHeader), hash); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find hash") - } - return nil, errors.Wrapf(err, "failed to get blockhash using hash %s", hash) + return nil, bhserrors.ErrHeaderNotFound.Wrap(err) } return &bh, nil } @@ -302,10 +300,7 @@ func (h *HeadersDb) GetHeaderByHeight(ctx context.Context, height int32, state s func (h *HeadersDb) GetHeaderByHeightRange(from int, to int) ([]*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, h.db.Rebind(sqlHeaderByHeightRange), from, to); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find headers in given range") - } - return nil, errors.Wrapf(err, "failed to get headers using given range from: %d to: %d", from, to) + return nil, bhserrors.ErrHeadersForGivenRangeNotFound.Wrap(err) } return bh, nil } @@ -344,10 +339,7 @@ func (h *HeadersDb) GenesisExists(_ context.Context) bool { func (h *HeadersDb) GetPreviousHeader(ctx context.Context, hash string) (*dto.DbBlockHeader, error) { var bh dto.DbBlockHeader if err := h.db.GetContext(ctx, &bh, h.db.Rebind(sqlSelectPreviousBlock), hash); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find header") - } - return nil, errors.Wrapf(err, "failed to get prev header using hash %s", hash) + return nil, bhserrors.ErrHeaderNotFound.Wrap(err) } return &bh, nil } @@ -370,10 +362,10 @@ func (h *HeadersDb) GetTip(_ context.Context) (*dto.DbBlockHeader, error) { func (h *HeadersDb) GetAncestorOnHeight(hash string, height int32) (*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, h.db.Rebind(sqlSelectAncestorOnHeight), hash, int(height), int(height)); err != nil { - return nil, errors.Wrapf(err, "failed to get ancestors using given hash: %s ", hash) + return nil, bhserrors.ErrAncestorNotFound.Wrap(err) } if len(bh) == 0 { - return nil, errors.New("could not find ancestors for a providen hash") + return nil, bhserrors.ErrAncestorNotFound } return bh[0], nil } @@ -382,7 +374,7 @@ func (h *HeadersDb) GetAncestorOnHeight(hash string, height int32) (*dto.DbBlock func (h *HeadersDb) GetAllTips() ([]*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, sqlSelectTips); err != nil { - return nil, errors.Wrapf(err, "failed to get tips") + return nil, bhserrors.ErrGetTips.Wrap(err) } return bh, nil } @@ -391,10 +383,10 @@ func (h *HeadersDb) GetAllTips() ([]*dto.DbBlockHeader, error) { func (h *HeadersDb) GetChainBetweenTwoHashes(low string, high string) ([]*dto.DbBlockHeader, error) { var bh []*dto.DbBlockHeader if err := h.db.Select(&bh, h.db.Rebind(sqlChainBetweenTwoHashes), high, low, low); err != nil { - return nil, errors.Wrapf(err, "failed to get headers using given range from: %s to: %s", low, high) + return nil, bhserrors.ErrHeadersForGivenRangeNotFound.Wrap(err) } if len(bh) == 0 { - return nil, errors.New("could not find headers in given range") + return nil, bhserrors.ErrHeadersForGivenRangeNotFound } return bh, nil } @@ -406,7 +398,7 @@ func (h *HeadersDb) GetMerkleRootsConfirmations( confirmations := make([]*dto.DbMerkleRootConfirmation, 0) tipHeight, err := h.getChainTipHeight() if err != nil { - return nil, errors.Wrap(err, "failed to get chain tip height") + return nil, bhserrors.ErrGetChainTipHeight.Wrap(err) } for _, item := range request { @@ -511,7 +503,7 @@ func (h *HeadersDb) getLastEvaluatedMerklerootHeight(lastEvaluatedKey string) (i err := h.db.Get(&lastEvaluatedMerkleroot, h.db.Rebind(sqlGetSingleMerkleroot), lastEvaluatedKey) if errors.Is(err, sql.ErrNoRows) { - return 0, domains.ErrMerklerootNotFound + return 0, bhserrors.ErrMerklerootNotFound } if err != nil { @@ -519,7 +511,7 @@ func (h *HeadersDb) getLastEvaluatedMerklerootHeight(lastEvaluatedKey string) (i } if lastEvaluatedMerkleroot.ToBlockHeader().State != domains.LongestChain { - return 0, domains.ErrMerklerootNotInLongestChain + return 0, bhserrors.ErrMerklerootNotInLongestChain } lastEvaluatedHeight := lastEvaluatedMerkleroot.Height diff --git a/database/sql/tokens.go b/database/sql/tokens.go index 32d4079f..19fcab52 100644 --- a/database/sql/tokens.go +++ b/database/sql/tokens.go @@ -2,10 +2,9 @@ package sql import ( "context" - "database/sql" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/repository/dto" - "github.com/pkg/errors" ) const ( @@ -39,19 +38,21 @@ func (h *HeadersDb) CreateToken(ctx context.Context, token *dto.DbToken) error { }() if _, err := tx.NamedExecContext(ctx, h.db.Rebind(sqlInsertToken), *token); err != nil { - return errors.Wrap(err, "failed to insert token") + return bhserrors.ErrCreateToken.Wrap(err) } - return errors.Wrap(tx.Commit(), "failed to commit tx") + + if err = tx.Commit(); err != nil { + return bhserrors.ErrCreateToken.Wrap(err) + } + + return nil } // GetTokenByValue method will search and return token by value. func (h *HeadersDb) GetTokenByValue(ctx context.Context, token string) (*dto.DbToken, error) { var dbToken dto.DbToken if err := h.db.GetContext(ctx, &dbToken, h.db.Rebind(sqlGetToken), token); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find token") - } - return nil, errors.Wrapf(err, "failed to get token using value %s", token) + return nil, bhserrors.ErrTokenNotFound.Wrap(err) } return &dbToken, nil } @@ -67,8 +68,13 @@ func (h *HeadersDb) DeleteToken(ctx context.Context, token string) error { }() if _, err = tx.NamedExecContext(ctx, h.db.Rebind(sqlDeleteToken), map[string]interface{}{"token": token}); err != nil { - return errors.Wrap(err, "failed to delete token") + return bhserrors.ErrDeleteToken.Wrap(err) + } + + if err = tx.Commit(); err != nil { + return bhserrors.ErrDeleteToken.Wrap(err) + } - return errors.Wrap(tx.Commit(), "failed to commit tx") + return nil } diff --git a/database/sql/webhooks.go b/database/sql/webhooks.go index a8c28f4d..b4747c5d 100644 --- a/database/sql/webhooks.go +++ b/database/sql/webhooks.go @@ -2,9 +2,9 @@ package sql import ( "context" - "database/sql" "time" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/repository/dto" "github.com/jmoiron/sqlx" "github.com/pkg/errors" @@ -50,20 +50,23 @@ func (h *HeadersDb) CreateWebhook(ctx context.Context, rWebhook *dto.DbWebhook) }() if _, err := tx.NamedExecContext(ctx, h.db.Rebind(sqlInsertWebhook), *rWebhook); err != nil { - return errors.Wrap(err, "failed to insert webhook") + return bhserrors.ErrCreateWebhook.Wrap(err) } - return errors.Wrap(tx.Commit(), "failed to commit tx") + + if err = tx.Commit(); err != nil { + return bhserrors.ErrCreateWebhook.Wrap(err) + } + + return nil } // GetWebhookByURL method will search and return webhook by url. func (h *HeadersDb) GetWebhookByURL(ctx context.Context, url string) (*dto.DbWebhook, error) { var rWebhook dto.DbWebhook if err := h.db.GetContext(ctx, &rWebhook, h.db.Rebind(sqlGetWebhookByURL), url); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, errors.New("could not find webhook") - } - return nil, errors.Wrapf(err, "failed to get webhook using url %s", url) + return nil, bhserrors.ErrWebhookNotFound.Wrap(err) } + return &rWebhook, nil } @@ -71,8 +74,9 @@ func (h *HeadersDb) GetWebhookByURL(ctx context.Context, url string) (*dto.DbWeb func (h *HeadersDb) GetAllWebhooks(ctx context.Context) ([]*dto.DbWebhook, error) { var rWebhooks []*dto.DbWebhook if err := h.db.SelectContext(ctx, &rWebhooks, h.db.Rebind(sqlGetAllWebhooks)); err != nil { - return nil, errors.Wrap(err, "failed to get all webhooks") + return nil, bhserrors.ErrGetAllWebhooks.Wrap(err) } + return rWebhooks, nil } @@ -89,10 +93,14 @@ func (h *HeadersDb) DeleteWebhookByURL(ctx context.Context, url string) error { params := map[string]interface{}{"url": url} if _, err = tx.NamedExecContext(ctx, h.db.Rebind(sqlDeleteWebhookByURL), params); err != nil { - return errors.Wrap(err, "failed to delete webhook") + return bhserrors.ErrDeleteWebhook.Wrap(err) } - return errors.Wrap(tx.Commit(), "failed to commit tx") + if err = tx.Commit(); err != nil { + return bhserrors.ErrDeleteWebhook.Wrap(err) + } + + return nil } // UpdateWebhook method will update webhook in db. @@ -112,6 +120,6 @@ func (h *HeadersDb) UpdateWebhook(ctx context.Context, url string, lastEmitTimes if _, err := tx.ExecContext(ctx, h.db.Rebind(query), args...); err != nil { return errors.Wrapf(err, "failed to update webhook with name %s", url) } - return errors.Wrap(tx.Commit(), "failed to commit tx") + return errors.Wrap(tx.Commit(), "failed to commit tx") } diff --git a/domains/error.go b/domains/error.go deleted file mode 100644 index 0a97c48a..00000000 --- a/domains/error.go +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright (c) 2014-2016 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package domains - -import ( - "errors" - "fmt" -) - -// DeploymentError identifies an error that indicates a deployment ID was -// specified that does not exist. -type DeploymentError uint32 - -// Error returns the assertion error as a human-readable string and satisfies -// the error interface. -func (e DeploymentError) Error() string { - return fmt.Sprintf("deployment ID %d does not exist", uint32(e)) -} - -// AssertError identifies an error that indicates an internal code consistency -// issue and should be treated as a critical and unrecoverable error. -type AssertError string - -// Error returns the assertion error as a human-readable string and satisfies -// the error interface. -func (e AssertError) Error() string { - return "assertion failed: " + string(e) -} - -// ErrorCode identifies a kind of error. -type ErrorCode int - -// These constants are used to identify errors specific to merkleroots -var ( - ErrMerklerootNotFound = errors.New("No block with provided merkleroot was found") - ErrMerklerootNotInLongestChain = errors.New("Provided merkleroot is not part of the longest chain") - ErrMerklerootInvalidBatchSize = errors.New("batchSize must be 0 or a positive integer") -) - -// These constants are used to identify a specific RuleError. -const ( - // ErrDuplicateBlock indicates a block with the same hash already - // exists. - ErrDuplicateBlock ErrorCode = iota - - // ErrBlockTooBig indicates the serialized block size exceeds the - // maximum allowed size. - ErrBlockTooBig - - // ErrBlockVersionTooOld indicates the block version is too old and is - // no longer accepted since the majority of the network has upgraded - // to a newer version. - ErrBlockVersionTooOld - - // ErrInvalidTime indicates the time in the passed block has a precision - // that is more than one second. The chain consensus rules require - // timestamps to have a maximum precision of one second. - ErrInvalidTime - - // ErrTimeTooOld indicates the time is either before the median time of - // the last several blocks per the chain consensus rules or prior to the - // most recent checkpoint. - ErrTimeTooOld - - // ErrTimeTooNew indicates the time is too far in the future as compared - // the current time. - ErrTimeTooNew - - // ErrDifficultyTooLow indicates the difficulty for the block is lower - // than the difficulty required by the most recent checkpoint. - ErrDifficultyTooLow - - // ErrUnexpectedDifficulty indicates specified bits do not align with - // the expected value either because it doesn't match the calculated - // valued based on difficulty regarted rules or it is out of the valid - // range. - ErrUnexpectedDifficulty - - // ErrHighHash indicates the block does not hash to a value which is - // lower than the required target difficultly. - ErrHighHash - - // ErrBadMerkleRoot indicates the calculated merkle root does not match - // the expected value. - ErrBadMerkleRoot - - // ErrBadCheckpoint indicates a block that is expected to be at a - // checkpoint height does not match the expected one. - ErrBadCheckpoint - - // ErrForkTooOld indicates a block is attempting to fork the block chain - // before the most recent checkpoint. - ErrForkTooOld - - // ErrCheckpointTimeTooOld indicates a block has a timestamp before the - // most recent checkpoint. - ErrCheckpointTimeTooOld - - // ErrNoTransactions indicates the block does not have a least one - // transaction. A valid block must have at least the coinbase - // transaction. - ErrNoTransactions - - // ErrNoTxInputs indicates a transaction does not have any inputs. A - // valid transaction must have at least one input. - ErrNoTxInputs - - // ErrNoTxOutputs indicates a transaction does not have any outputs. A - // valid transaction must have at least one output. - ErrNoTxOutputs - - // ErrTxTooBig indicates a transaction exceeds the maximum allowed size - // when serialized. - ErrTxTooBig - - // ErrTxTooSmall indicates a transaction is smaller than the minimum - // allowed size when serialized. - ErrTxTooSmall - - // ErrTxTooManySigOps indicates a transaction exceeds the maximum allowable - // number of signature operations. - ErrTxTooManySigOps - - // ErrBadTxOutValue indicates an output value for a transaction is - // invalid in some way such as being out of range. - ErrBadTxOutValue - - // ErrDuplicateTxInputs indicates a transaction references the same - // input more than once. - ErrDuplicateTxInputs - - // ErrBadTxInput indicates a transaction input is invalid in some way - // such as referencing a previous transaction outpoint which is out of - // range or not referencing one at all. - ErrBadTxInput - - // ErrMissingTxOut indicates a transaction output referenced by an input - // that does not exist. - ErrMissingTxOut - - // ErrSpentTxOut indicates a transaction output referenced by an input - // that has already been spent. - ErrSpentTxOut - - // ErrUnfinalizedTx indicates a transaction has not been finalized. - // A valid block may only contain finalized transactions. - ErrUnfinalizedTx - - // ErrDuplicateTx indicates a block contains an identical transaction - // (or at least two transactions which hash to the same value). A - // valid block may only contain unique transactions. - ErrDuplicateTx - - // ErrOverwriteTx indicates a block contains a transaction that has - // the same hash as a previous transaction which has not been fully - // spent. - ErrOverwriteTx - - // ErrImmatureSpend indicates a transaction is attempting to spend a - // coinbase that has not yet reached the required maturity. - ErrImmatureSpend - - // ErrSpendTooHigh indicates a transaction is attempting to spend more - // value than the sum of all of its inputs. - ErrSpendTooHigh - - // ErrBadFees indicates the total fees for a block are invalid due to - // exceeding the maximum possible value. - ErrBadFees - - // ErrTooManySigOps indicates the total number of signature operations - // for a transaction or block exceed the maximum allowed limits. - ErrTooManySigOps - - // ErrFirstTxNotCoinbase indicates the first transaction in a block - // is not a coinbase transaction. - ErrFirstTxNotCoinbase - - // ErrMultipleCoinbases indicates a block contains more than one - // coinbase transaction. - ErrMultipleCoinbases - - // ErrBadCoinbaseScriptLen indicates the length of the signature script - // for a coinbase transaction is not within the valid range. - ErrBadCoinbaseScriptLen - - // ErrBadCoinbaseValue indicates the amount of a coinbase value does - // not match the expected value of the subsidy plus the sum of all fees. - ErrBadCoinbaseValue - - // ErrMissingCoinbaseHeight indicates the coinbase transaction for a - // block does not start with the serialized block block height as - // required for version 2 and higher blocks. - ErrMissingCoinbaseHeight - - // ErrBadCoinbaseHeight indicates the serialized block height in the - // coinbase transaction for version 2 and higher blocks does not match - // the expected value. - ErrBadCoinbaseHeight - - // ErrScriptMalformed indicates a transaction script is malformed in - // some way. For example, it might be longer than the maximum allowed - // length or fail to parse. - ErrScriptMalformed - - // ErrScriptValidation indicates the result of executing transaction - // script failed. The error covers any failure when executing scripts - // such signature verification failures and execution past the end of - // the stack. - ErrScriptValidation - - // ErrPreviousBlockUnknown indicates that the previous block is not known. - ErrPreviousBlockUnknown - - // ErrInvalidAncestorBlock indicates that an ancestor of this block has - // already failed validation. - ErrInvalidAncestorBlock - - // ErrPrevBlockNotBest indicates that the block's previous block is not the - // current chain tip. This is not a block validation rule, but is required - // for block proposals submitted via getblocktemplate RPC. - ErrPrevBlockNotBest - - // ErrBlockTooSmall indicates the serialized block size is less than the - // minimum allowed size. This consensus rule currently only applies to - // the first block after the Uahf. - ErrBlockTooSmall - - // ErrInvalidTxOrder indicates the order of the transactions in the block - // does not follow the active transaction ordering consensus rule. - ErrInvalidTxOrder - - // ErrInvalidBatchSize indicates that batchsize provided to the merkleroot endpoint - // is not a positive numberic value - ErrInvalidBatchSize - - // ErrMerkleRootNotFound indicates that lastEvaluatedKey the client provided to the - // merkleroot endpoint wasn't found in our database - ErrMerkleRootNotFound - - // ErrMerkleRootNotInLC indicates that lastEvaluatedKEy the client provided to the - // merkleroot endpoint was found but is not a part of the longest chain - ErrMerkleRootNotInLC - - // ErrGeneric indicates generic error that happened on the server - ErrGeneric -) - -// String returns the ErrorCode as a human-readable name. -func (e ErrorCode) String() string { - // Map of ErrorCode values back to their constant names for pretty printing. - var errorCodeStrings = map[ErrorCode]string{ - ErrDuplicateBlock: "ErrDuplicateBlock", - ErrBlockTooBig: "ErrBlockTooBig", - ErrBlockVersionTooOld: "ErrBlockVersionTooOld", - ErrInvalidTime: "ErrInvalidTime", - ErrTimeTooOld: "ErrTimeTooOld", - ErrTimeTooNew: "ErrTimeTooNew", - ErrDifficultyTooLow: "ErrDifficultyTooLow", - ErrUnexpectedDifficulty: "ErrUnexpectedDifficulty", - ErrHighHash: "ErrHighHash", - ErrBadMerkleRoot: "ErrBadMerkleRoot", - ErrBadCheckpoint: "ErrBadCheckpoint", - ErrForkTooOld: "ErrForkTooOld", - ErrCheckpointTimeTooOld: "ErrCheckpointTimeTooOld", - ErrNoTransactions: "ErrNoTransactions", - ErrNoTxInputs: "ErrNoTxInputs", - ErrNoTxOutputs: "ErrNoTxOutputs", - ErrTxTooBig: "ErrTxTooBig", - ErrTxTooSmall: "ErrTxTooSmall", - ErrTxTooManySigOps: "ErrTxTooManySigOps", - ErrBadTxOutValue: "ErrBadTxOutValue", - ErrDuplicateTxInputs: "ErrDuplicateTxInputs", - ErrBadTxInput: "ErrBadTxInput", - ErrMissingTxOut: "ErrMissingTxOut", - ErrSpentTxOut: "ErrSpentTxOut", - ErrUnfinalizedTx: "ErrUnfinalizedTx", - ErrDuplicateTx: "ErrDuplicateTx", - ErrOverwriteTx: "ErrOverwriteTx", - ErrImmatureSpend: "ErrImmatureSpend", - ErrSpendTooHigh: "ErrSpendTooHigh", - ErrBadFees: "ErrBadFees", - ErrTooManySigOps: "ErrTooManySigOps", - ErrFirstTxNotCoinbase: "ErrFirstTxNotCoinbase", - ErrMultipleCoinbases: "ErrMultipleCoinbases", - ErrBadCoinbaseScriptLen: "ErrBadCoinbaseScriptLen", - ErrBadCoinbaseValue: "ErrBadCoinbaseValue", - ErrMissingCoinbaseHeight: "ErrMissingCoinbaseHeight", - ErrBadCoinbaseHeight: "ErrBadCoinbaseHeight", - ErrScriptMalformed: "ErrScriptMalformed", - ErrScriptValidation: "ErrScriptValidation", - ErrPreviousBlockUnknown: "ErrPreviousBlockUnknown", - ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock", - ErrPrevBlockNotBest: "ErrPrevBlockNotBest", - ErrInvalidTxOrder: "ErrInvalidTxOrder", - ErrInvalidBatchSize: "ErrInvalidBatchSize", - ErrMerkleRootNotFound: "ErrMerkleRootNotFound", - ErrMerkleRootNotInLC: "ErrMerkleRootNotInLC", - ErrGeneric: "ErrGeneric", - } - - if s := errorCodeStrings[e]; s != "" { - return s - } - return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) -} - -// RuleError identifies a rule violation. It is used to indicate that -// processing of a block or transaction failed due to one of the many validation -// rules. The caller can use type assertions to determine if a failure was -// specifically due to a rule violation and access the ErrorCode field to -// ascertain the specific reason for the rule violation. -type RuleError struct { - ErrorCode ErrorCode // Describes the kind of error - Description string // Human readable description of the issue -} - -// Error satisfies the error interface and prints human-readable errors. -func (e RuleError) Error() string { - return e.Description -} diff --git a/internal/tests/testrepository/header_testrepository.go b/internal/tests/testrepository/header_testrepository.go index 8d4eed44..713f4c61 100644 --- a/internal/tests/testrepository/header_testrepository.go +++ b/internal/tests/testrepository/header_testrepository.go @@ -5,6 +5,7 @@ import ( "slices" "sort" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/internal/chaincfg/chainhash" "github.com/bitcoin-sv/block-headers-service/internal/tests/fixtures" @@ -69,7 +70,7 @@ func (r *HeaderTestRepository) GetHeaderByHeightRange(from int, to int) ([]*doma return filteredHeaders, nil } - return nil, errors.New("could not find headers in given range") + return nil, bhserrors.ErrHeadersForGivenRangeNotFound } // GetLongestChainHeadersFromHeight returns from db the headers from "longest chain" starting from given height. @@ -132,7 +133,7 @@ func (r *HeaderTestRepository) GetHeaderByHash(hash string) (*domains.BlockHeade if header != nil { return header, nil } - return nil, errors.New("could not find hash") + return nil, bhserrors.ErrHeaderNotFound } // GenesisExists check if genesis header is in db. @@ -222,12 +223,12 @@ func (r *HeaderTestRepository) GetMerkleRoots(batchSize int, lastEvaluatedKey st }) if startIdx == -1 && lastEvaluatedKey != "" { - return nil, domains.ErrMerklerootNotFound + return nil, bhserrors.ErrMerklerootNotFound } // Check if lastEvaluatedKey is not from the longest chain if startIdx != -1 && !(*r.db)[startIdx].IsLongestChain() { - return nil, domains.ErrMerklerootNotInLongestChain + return nil, bhserrors.ErrMerklerootNotInLongestChain } // If the lastEvaluatedKey is found, we start after it; otherwise, we start from the beginning @@ -362,7 +363,7 @@ func (r *HeaderTestRepository) GetHeadersStopHeight(hashStop string) (int, error return int(header.Height), nil } } - return 0, errors.New("could not find stop height") + return 0, bhserrors.ErrHeaderStopHeightNotFound } // FillWithLongestChain fills the test header repository diff --git a/internal/tests/testrepository/webhooks_testrepository.go b/internal/tests/testrepository/webhooks_testrepository.go index bb698b38..9f0e520a 100644 --- a/internal/tests/testrepository/webhooks_testrepository.go +++ b/internal/tests/testrepository/webhooks_testrepository.go @@ -1,9 +1,9 @@ package testrepository import ( - "errors" "fmt" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/notification" ) @@ -35,7 +35,7 @@ func (r *WebhooksTestRepository) DeleteWebhookByURL(url string) error { return nil } } - return errors.New("could not find webhook") + return bhserrors.ErrWebhookNotFound } // GetWebhookByURL returns webhook from db by given url. diff --git a/notification/webhooks_service.go b/notification/webhooks_service.go index 9785c1ce..f5831c2e 100644 --- a/notification/webhooks_service.go +++ b/notification/webhooks_service.go @@ -1,9 +1,9 @@ package notification import ( - "errors" "strings" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/rs/zerolog" ) @@ -98,9 +98,9 @@ func (s *WebhooksService) refreshWebhook(url string) (*Webhook, error) { w.ErrorsCount = 0 err = s.webhooks.UpdateWebhook(w) if err != nil { - return nil, err + return nil, bhserrors.ErrRefreshWebhook.Wrap(err) } return w, nil } - return nil, errors.New("webhook already exists and is active") + return nil, bhserrors.ErrRefreshWebhook } diff --git a/service/chain_service.go b/service/chain_service.go index 5fbcfe15..e4346205 100644 --- a/service/chain_service.go +++ b/service/chain_service.go @@ -3,6 +3,7 @@ package service import ( "strings" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/internal/chaincfg" "github.com/bitcoin-sv/block-headers-service/internal/chaincfg/chainhash" @@ -186,7 +187,7 @@ func (cs *chainService) createHeader(hash *domains.BlockHash, bs *domains.BlockH func (cs *chainService) previousHeader(bs *domains.BlockHeaderSource) (*domains.BlockHeader, error) { h, err := cs.Repositories.Headers.GetHeaderByHash(bs.PrevBlock.String()) - if h == nil && err != nil && err.Error() == "could not find hash" { + if h == nil && err != nil && errors.Is(err, bhserrors.ErrHeaderNotFound) { return domains.NewOrphanPreviousBlockHeader(), nil } return h, err diff --git a/service/header_service.go b/service/header_service.go index 2ff7d939..6ad8d928 100644 --- a/service/header_service.go +++ b/service/header_service.go @@ -6,6 +6,7 @@ import ( "math" "time" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/internal/chaincfg" @@ -128,20 +129,20 @@ func (hs *HeaderService) GetHeaderAncestorsByHash(hash string, ancestorHash stri // Check possible errors if err != nil || err2 != nil { - return nil, errors.New("error during getting headers with given hashes") + return nil, bhserrors.ErrHeaderWithGivenHashes } else if ancestorHeader.Height > reqHeader.Height { - return nil, errors.New("ancestor header height can not be higher than requested header heght") + return nil, bhserrors.ErrAncestorHashHigher } else if ancestorHeader.Height == reqHeader.Height { return make([]*domains.BlockHeader, 0), nil } a, err := hs.repo.Headers.GetAncestorOnHeight(reqHeader.Hash.String(), ancestorHeader.Height) if err != nil { - return nil, errors.New("the headers provided are not part of the same chain") + return nil, bhserrors.ErrHeadersNotPartOfTheSameChain.Wrap(err) } if a.Hash != ancestorHeader.Hash { - return nil, errors.New("the headers provided are not part of the same chain") + return nil, bhserrors.ErrHeadersNotPartOfTheSameChain } // Get headers from db diff --git a/service/service.go b/service/service.go index af4ce39c..a211ae49 100644 --- a/service/service.go +++ b/service/service.go @@ -64,6 +64,7 @@ type Services struct { Tokens Tokens Notifier *notification.Notifier Webhooks *notification.WebhooksService + Logger *zerolog.Logger } // Dept is a struct used to create Services. @@ -87,6 +88,7 @@ func NewServices(d Dept) *Services { Chains: newChainService(d, notifier), Tokens: NewTokenService(d.Repositories, d.AdminToken), Webhooks: newWebhooks(d), + Logger: d.Logger, } } diff --git a/transports/http/auth/auth_token_middleware.go b/transports/http/auth/auth_token_middleware.go index dc736d38..1f8a16a2 100644 --- a/transports/http/auth/auth_token_middleware.go +++ b/transports/http/auth/auth_token_middleware.go @@ -1,10 +1,9 @@ package auth import ( - "errors" - "net/http" "strings" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/service" @@ -34,13 +33,13 @@ func (h *TokenMiddleware) ApplyToAPI(c *gin.Context) { if h.cfg.UseAuth { rawToken, err := h.parseAuthHeader(c) if err != nil { - c.AbortWithStatusJSON(http.StatusUnauthorized, err.Error()) + bhserrors.AbortWithErrorResponse(c, err, nil) return } token, err := h.getToken(rawToken) if err != nil { - c.AbortWithStatusJSON(http.StatusUnauthorized, err.Error()) + bhserrors.AbortWithErrorResponse(c, err, nil) return } @@ -51,12 +50,12 @@ func (h *TokenMiddleware) ApplyToAPI(c *gin.Context) { func (h *TokenMiddleware) parseAuthHeader(c *gin.Context) (string, error) { header := c.GetHeader(authorizationHeader) if header == "" { - return "", errors.New("empty auth header") + return "", bhserrors.ErrMissingAuthHeader } headerParts := strings.Split(header, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { - return "", errors.New("invalid auth header") + return "", bhserrors.ErrInvalidAuthHeader } return headerParts[1], nil @@ -65,7 +64,7 @@ func (h *TokenMiddleware) parseAuthHeader(c *gin.Context) (string, error) { func (h *TokenMiddleware) getToken(token string) (*domains.Token, error) { t, err := h.tokens.GetToken(token) if err != nil { - return nil, errors.New("invalid access token") + return nil, bhserrors.ErrInvalidAccessToken } return t, nil } diff --git a/transports/http/auth/require_auth.go b/transports/http/auth/require_auth.go index 20b111e7..24e0e5c6 100644 --- a/transports/http/auth/require_auth.go +++ b/transports/http/auth/require_auth.go @@ -1,9 +1,7 @@ package auth import ( - "errors" - "net/http" - + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/gin-gonic/gin" ) @@ -17,7 +15,7 @@ func RequireAdmin(handler gin.HandlerFunc, requireAdmin bool) gin.HandlerFunc { if err := validateToken(c); err == nil { handler(c) } else { - c.AbortWithStatusJSON(http.StatusUnauthorized, err.Error()) + bhserrors.AbortWithErrorResponse(c, err, nil) } } } @@ -27,14 +25,14 @@ func RequireAdmin(handler gin.HandlerFunc, requireAdmin bool) gin.HandlerFunc { func validateToken(c *gin.Context) error { token, exist := c.Get("token") if !exist { - return errors.New("token not found") + return bhserrors.ErrAdminTokenNotFound } t, ok := token.(*domains.Token) if !ok { - return errors.New("something went wrong") + return bhserrors.ErrGeneric } if !t.IsAdmin { - return errors.New("not authorized") + return bhserrors.ErrUnauthorized } return nil // the token is valid } diff --git a/transports/http/endpoints/api/access/endpoints.go b/transports/http/endpoints/api/access/endpoints.go index fd0e9c8c..4e3dea43 100644 --- a/transports/http/endpoints/api/access/endpoints.go +++ b/transports/http/endpoints/api/access/endpoints.go @@ -3,20 +3,23 @@ package access import ( "net/http" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/service" "github.com/bitcoin-sv/block-headers-service/transports/http/auth" router "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/routes" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) type handler struct { service service.Tokens + log *zerolog.Logger } // NewHandler creates new endpoint handler. func NewHandler(s *service.Services) router.APIEndpoints { - return &handler{service: s.Tokens} + return &handler{service: s.Tokens, log: s.Logger} } // RegisterAPIEndpoints registers routes that are part of service API. @@ -63,7 +66,7 @@ func (h *handler) createToken(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, bh) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -84,6 +87,6 @@ func (h *handler) revokeToken(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, "Token revoked") } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } diff --git a/transports/http/endpoints/api/headers/endpoints.go b/transports/http/endpoints/api/headers/endpoints.go index 55cbad47..a45b162e 100644 --- a/transports/http/endpoints/api/headers/endpoints.go +++ b/transports/http/endpoints/api/headers/endpoints.go @@ -4,19 +4,22 @@ import ( "net/http" "strconv" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/service" router "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/routes" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) type handler struct { service service.Headers + log *zerolog.Logger } // NewHandler creates new endpoint handler. func NewHandler(s *service.Services) router.APIEndpoints { - return &handler{service: s.Headers} + return &handler{service: s.Headers, log: s.Logger} } // RegisterAPIEndpoints registers routes that are part of service API. @@ -48,7 +51,7 @@ func (h *handler) getHeaderByHash(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, newBlockHeaderResponse(bh)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -77,10 +80,10 @@ func (h *handler) getHeaderByHeight(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, mapToBlockHeadersResponses(bh)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -103,7 +106,7 @@ func (h *handler) getHeaderAncestorsByHash(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, mapToBlockHeadersResponses(ancestors)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -120,14 +123,14 @@ func (h *handler) getHeaderAncestorsByHash(c *gin.Context) { func (h *handler) getCommonAncestor(c *gin.Context) { var body []string if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, bhserrors.ErrBindBody.Wrap(err), h.log) } else { ancestor, err := h.service.GetCommonAncestor(body) if err == nil { c.JSON(http.StatusOK, newBlockHeaderResponse(ancestor)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } } @@ -150,6 +153,6 @@ func (h *handler) getHeadersState(c *gin.Context) { headerStateResponse := newBlockHeaderStateResponse(bh) c.JSON(http.StatusOK, headerStateResponse) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } diff --git a/transports/http/endpoints/api/headers/header_endpoints_test.go b/transports/http/endpoints/api/headers/header_endpoints_test.go index c683a4ff..ae6e24a9 100644 --- a/transports/http/endpoints/api/headers/header_endpoints_test.go +++ b/transports/http/endpoints/api/headers/header_endpoints_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "strconv" "testing" @@ -17,6 +16,7 @@ import ( "github.com/bitcoin-sv/block-headers-service/internal/tests/fixtures" "github.com/bitcoin-sv/block-headers-service/internal/tests/testapp" "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/api/headers" + "github.com/stretchr/testify/require" ) var expectedObj = headers.BlockHeaderResponse{ @@ -37,10 +37,10 @@ func TestGetHeaderByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -48,8 +48,7 @@ func TestGetHeaderByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -82,10 +81,10 @@ func TestGetHeaderByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find hash\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeaderNotFound\",\"message\":\"header not found\"}", } // when @@ -93,8 +92,7 @@ func TestGetHeaderByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -105,10 +103,10 @@ func TestGetHeaderByHeight(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"code\":\"ErrMissingAuthHeader\",\"message\":\"empty auth header\"}", } // when @@ -116,8 +114,7 @@ func TestGetHeaderByHeight(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -150,10 +147,10 @@ func TestGetHeaderByHeight(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find headers in given range\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeadersForGivenRangeNotFound\",\"message\":\"could not find headers in given range\"}", } // when @@ -161,8 +158,7 @@ func TestGetHeaderByHeight(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -173,10 +169,10 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -184,8 +180,7 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -218,10 +213,10 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusBadRequest, - body: []byte("\"error during getting headers with given hashes\""), + body: "{\"code\":\"ErrHeaderWithGivenHashes\",\"message\":\"error during getting headers with given hashes\"}", } // when @@ -229,8 +224,7 @@ func TestGetHeaderAncestorsByHash(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -241,10 +235,10 @@ func TestGetCommonAncestor(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -252,8 +246,7 @@ func TestGetCommonAncestor(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -297,10 +290,10 @@ func TestGetCommonAncestor(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find hash\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeaderNotFound\",\"message\":\"header not found\"}", } // when @@ -308,8 +301,7 @@ func TestGetCommonAncestor(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } @@ -320,10 +312,10 @@ func TestGetHeadersState(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"message\": \"empty auth header\", \"code\": \"ErrMissingAuthHeader\"}", } // when @@ -331,8 +323,7 @@ func TestGetHeadersState(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -371,10 +362,10 @@ func TestGetHeadersState(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ - code: http.StatusBadRequest, - body: []byte("\"could not find hash\""), + code: http.StatusNotFound, + body: "{\"code\":\"ErrHeaderNotFound\",\"message\":\"header not found\"}", } // when @@ -382,8 +373,7 @@ func TestGetHeadersState(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) } diff --git a/transports/http/endpoints/api/merkleroots/endpoints.go b/transports/http/endpoints/api/merkleroots/endpoints.go index 2292af8a..d4a7e1f3 100644 --- a/transports/http/endpoints/api/merkleroots/endpoints.go +++ b/transports/http/endpoints/api/merkleroots/endpoints.go @@ -1,16 +1,16 @@ package merkleroots import ( - "errors" "net/http" "strconv" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/domains" "github.com/bitcoin-sv/block-headers-service/service" router "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/routes" - "github.com/bitcoin-sv/block-headers-service/transports/http/response" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) const ( @@ -20,11 +20,12 @@ const ( type handler struct { service service.Merkleroots + log *zerolog.Logger } // NewHandler creates new endpoint handler. func NewHandler(s *service.Services) router.APIEndpoints { - return &handler{service: s.Merkleroots} + return &handler{service: s.Merkleroots, log: s.Logger} } // RegisterAPIEndpoints registers routes that are part of service API. @@ -53,8 +54,7 @@ func (h *handler) merkleroots(c *gin.Context) { batchSizeInt, err := strconv.Atoi(batchSize) if err != nil || batchSizeInt < 0 { - err, statusCode := response.Error(domains.ErrMerklerootInvalidBatchSize) - c.JSON(statusCode, err) + bhserrors.ErrorResponse(c, bhserrors.ErrInvalidBatchSize.Wrap(err), h.log) return } @@ -63,8 +63,7 @@ func (h *handler) merkleroots(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, merkleroots) } else { - errResponse, statusCode := response.Error(err) - c.JSON(statusCode, errResponse) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -86,7 +85,7 @@ func (h *handler) verify(c *gin.Context) { } if len(body) == 0 { - c.JSON(http.StatusBadRequest, errors.New("at least one merkleroot is required").Error()) + bhserrors.ErrorResponse(c, bhserrors.ErrVerifyMerklerootsBadBody, h.log) return } @@ -95,6 +94,6 @@ func (h *handler) verify(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, mapToMerkleRootsConfirmationsResponses(mrcs)) } else { - c.JSON(http.StatusInternalServerError, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } diff --git a/transports/http/endpoints/api/merkleroots/merkleroots_endpoints_test.go b/transports/http/endpoints/api/merkleroots/merkleroots_endpoints_test.go index 1e85068c..8c7758ee 100644 --- a/transports/http/endpoints/api/merkleroots/merkleroots_endpoints_test.go +++ b/transports/http/endpoints/api/merkleroots/merkleroots_endpoints_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" "net/url" "testing" @@ -71,10 +70,10 @@ func TestReturnFailureFromVerifyWhenAuthorizationIsTurnedOnAndCalledWithoutToken query := []domains.MerkleRootConfirmationRequestItem{} expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"code\":\"ErrMissingAuthHeader\",\"message\":\"empty auth header\"}", } // when @@ -85,10 +84,7 @@ func TestReturnFailureFromVerifyWhenAuthorizationIsTurnedOnAndCalledWithoutToken if res.Code != expectedResult.code { t.Errorf("Expected to get status %d but instead got %d\n", expectedResult.code, res.Code) } - body, _ := io.ReadAll(res.Body) - if !bytes.Equal(body, expectedResult.body) { - t.Errorf("Expected to get body %s but insead got %s\n", expectedResult.body, body) - } + require.JSONEq(t, expectedResult.body, res.Body.String()) } func TestReturnInvalidFromVerify(t *testing.T) { @@ -222,10 +218,10 @@ func TestReturnBadRequestErrorFromVerifyWhenGivenEmtpyArray(t *testing.T) { query := []domains.MerkleRootConfirmationRequestItem{} expectedResult := struct { code int - body []byte + body string }{ code: http.StatusBadRequest, - body: []byte("\"at least one merkleroot is required\""), + body: "{\"code\":\"ErrVerifyMerklerootsBadBody\",\"message\":\"at least one merkleroot is required\"}", } // when @@ -233,11 +229,7 @@ func TestReturnBadRequestErrorFromVerifyWhenGivenEmtpyArray(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - - body, _ := io.ReadAll(res.Body) - if !bytes.Equal(body, expectedResult.body) { - t.Errorf("Expected to get body %s but insead got %s\n", expectedResult.body, body) - } + require.JSONEq(t, expectedResult.body, res.Body.String()) } func verify(request []domains.MerkleRootConfirmationRequestItem) (req *http.Request, err error) { @@ -402,7 +394,7 @@ func TestMerkleRootsFailure(t *testing.T) { expectedCode: http.StatusNotFound, expectedBody: `{ "code": "ErrMerkleRootNotFound", - "message": "No block with provided merkleroot was found" + "message": "no block with provided merkleroot was found" }`, }, "return error when evaluationKey merkleroot is from stale chain": { @@ -410,8 +402,8 @@ func TestMerkleRootsFailure(t *testing.T) { evaluationKey: "88d2a4e04a96b45e3ba04637098a92fd0786daf3fc8ff88314f8e739a9918bf3", expectedCode: http.StatusConflict, expectedBody: `{ - "code": "ErrMerkleRootNotInLC", - "message": "Provided merkleroot is not part of the longest chain" + "code": "ErrMerkleRootNotInLongestChain", + "message": "provided merkleroot is not part of the longest chain" }`, }, } diff --git a/transports/http/endpoints/api/tips/endpoints.go b/transports/http/endpoints/api/tips/endpoints.go index de31e9af..a1198ee4 100644 --- a/transports/http/endpoints/api/tips/endpoints.go +++ b/transports/http/endpoints/api/tips/endpoints.go @@ -3,19 +3,22 @@ package tips import ( "net/http" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/service" router "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/routes" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) type handler struct { service service.Headers + log *zerolog.Logger } // NewHandler creates new endpoint handler. func NewHandler(s *service.Services) router.APIEndpoints { - return &handler{service: s.Headers} + return &handler{service: s.Headers, log: s.Logger} } // RegisterAPIEndpoints registers routes that are part of service API. @@ -42,7 +45,7 @@ func (h *handler) getTips(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, mapToTipStateResponse(tips)) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } diff --git a/transports/http/endpoints/api/tips/tips_endpoints_test.go b/transports/http/endpoints/api/tips/tips_endpoints_test.go index 9d1ee3b8..00cae68e 100644 --- a/transports/http/endpoints/api/tips/tips_endpoints_test.go +++ b/transports/http/endpoints/api/tips/tips_endpoints_test.go @@ -3,7 +3,6 @@ package tips_test import ( "context" "encoding/json" - "io" "math/big" "net/http" "testing" @@ -13,6 +12,7 @@ import ( "github.com/bitcoin-sv/block-headers-service/internal/tests/fixtures" "github.com/bitcoin-sv/block-headers-service/internal/tests/testapp" "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/api/tips" + "github.com/stretchr/testify/require" ) var expectedTip = tips.TipStateResponse{ @@ -38,10 +38,10 @@ func TestGetTips(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"code\":\"ErrMissingAuthHeader\",\"message\":\"empty auth header\"}", } // when @@ -49,8 +49,7 @@ func TestGetTips(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { @@ -86,10 +85,10 @@ func TestGetTipLongest(t *testing.T) { defer cleanup() expectedResult := struct { code int - body []byte + body string }{ code: http.StatusUnauthorized, - body: []byte("\"empty auth header\""), + body: "{\"code\":\"ErrMissingAuthHeader\",\"message\":\"empty auth header\"}", } // when @@ -97,8 +96,7 @@ func TestGetTipLongest(t *testing.T) { // then assert.Equal(t, res.Code, expectedResult.code) - body, _ := io.ReadAll(res.Body) - assert.EqualBytes(t, body, expectedResult.body) + require.JSONEq(t, expectedResult.body, res.Body.String()) }) t.Run("success", func(t *testing.T) { diff --git a/transports/http/endpoints/api/webhook/endpoints.go b/transports/http/endpoints/api/webhook/endpoints.go index f9b4cc71..7e2b1c8b 100644 --- a/transports/http/endpoints/api/webhook/endpoints.go +++ b/transports/http/endpoints/api/webhook/endpoints.go @@ -3,11 +3,13 @@ package webhook import ( "net/http" + "github.com/bitcoin-sv/block-headers-service/bhserrors" "github.com/bitcoin-sv/block-headers-service/config" "github.com/bitcoin-sv/block-headers-service/notification" "github.com/bitcoin-sv/block-headers-service/service" router "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/routes" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) // Webhooks is an interface which represents methods required for Webhooks service. @@ -19,11 +21,12 @@ type Webhooks interface { type handler struct { service Webhooks + log *zerolog.Logger } // NewHandler creates new endpoint handler. func NewHandler(s *service.Services) router.APIEndpoints { - return &handler{service: s.Webhooks} + return &handler{service: s.Webhooks, log: s.Logger} } // RegisterAPIEndpoints registers routes that are part of service API. @@ -52,19 +55,19 @@ func (h *handler) registerWebhook(c *gin.Context) { err := c.Bind(&reqBody) if err != nil { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, bhserrors.ErrBindBody.Wrap(err), h.log) } if reqBody.URL == "" { - c.JSON(http.StatusBadRequest, "URL is required") + bhserrors.ErrorResponse(c, bhserrors.ErrURLBodyRequired, h.log) return } webhook, err := h.service.CreateWebhook(reqBody.RequiredAuth.Type, reqBody.RequiredAuth.Header, reqBody.RequiredAuth.Token, reqBody.URL) if err == nil { c.JSON(http.StatusOK, webhook) - } else if webhook == nil { - c.JSON(http.StatusOK, err.Error()) + } else { + bhserrors.ErrorResponse(c, err, h.log) } } @@ -82,7 +85,7 @@ func (h *handler) registerWebhook(c *gin.Context) { func (h *handler) getWebhook(c *gin.Context) { url := c.Query("url") if url == "" { - c.JSON(http.StatusBadRequest, "URL param is required") + bhserrors.ErrorResponse(c, bhserrors.ErrURLParamRequired, h.log) return } w, err := h.service.GetWebhookByURL(url) @@ -90,7 +93,7 @@ func (h *handler) getWebhook(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, w) } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } @@ -108,7 +111,7 @@ func (h *handler) getWebhook(c *gin.Context) { func (h *handler) revokeWebhook(c *gin.Context) { url := c.Query("url") if url == "" { - c.JSON(http.StatusBadRequest, "URL param is required") + bhserrors.ErrorResponse(c, bhserrors.ErrURLParamRequired, h.log) return } err := h.service.DeleteWebhook(url) @@ -116,6 +119,6 @@ func (h *handler) revokeWebhook(c *gin.Context) { if err == nil { c.JSON(http.StatusOK, "Webhook revoked") } else { - c.JSON(http.StatusBadRequest, err.Error()) + bhserrors.ErrorResponse(c, err, h.log) } } diff --git a/transports/http/endpoints/api/webhook/webhooks_handler_test.go b/transports/http/endpoints/api/webhook/webhooks_handler_test.go index a5ab96e8..12c7227d 100644 --- a/transports/http/endpoints/api/webhook/webhooks_handler_test.go +++ b/transports/http/endpoints/api/webhook/webhooks_handler_test.go @@ -11,6 +11,7 @@ import ( "github.com/bitcoin-sv/block-headers-service/internal/tests/testapp" "github.com/bitcoin-sv/block-headers-service/transports/http/endpoints/api/webhook" + "github.com/stretchr/testify/require" ) var webhookURL = "http://localhost:8080/api/v1/webhook/notify" @@ -43,6 +44,7 @@ func TestMultipleIdenticalWebhooks(t *testing.T) { // setup bhs, cleanup := testapp.NewTestBlockHeaderService(t, testapp.WithAPIAuthorizationDisabled()) defer cleanup() + expectedBodyResponse := "{\"code\":\"ErrRefreshWebhook\",\"message\":\"webhook already exists and is active\"}" // when res := bhs.API().Call(createWebhook()) @@ -55,16 +57,11 @@ func TestMultipleIdenticalWebhooks(t *testing.T) { // when res2 := bhs.API().Call(createWebhook()) - if res2.Code != http.StatusOK { + if res2.Code != http.StatusBadRequest { t.Fatalf("Expected to get status %d but instead got %d\n", http.StatusOK, res2.Code) } - body, _ := io.ReadAll(res2.Body) - bodyStr := string(body)[1 : len(string(body))-1] - - if bodyStr != "webhook already exists and is active" { - t.Fatalf("Expected message: 'webhook already exists and is active' but instead got '%s'\n", bodyStr) - } + require.JSONEq(t, expectedBodyResponse, res2.Body.String()) } // TestRevokeWebhookEndpoint tests the webhook revocation. diff --git a/transports/http/response/error_response_helpers.go b/transports/http/response/error_response_helpers.go deleted file mode 100644 index d98b1864..00000000 --- a/transports/http/response/error_response_helpers.go +++ /dev/null @@ -1,56 +0,0 @@ -package response - -import ( - "errors" - "net/http" - - "github.com/bitcoin-sv/block-headers-service/domains" -) - -// DetailedError is an object that we can return to the client if any error happens -// on the server -type DetailedError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -// Error is creating an error object to send it to the client -// it also returns http statusCode -func Error(err error) (*DetailedError, int) { - sc, cm := getCodeFromError(err) - return parseError(err, sc, cm) -} - -// ErrorFromMessage is simplified ErrorResponse when we don't want to create new error -// in code but just pass the message that will be sent to the client. -func ErrorFromMessage(errMessage string) (*DetailedError, int) { - sc, cm := getCodeFromError(errors.New(errMessage)) - return parseError(errors.New(errMessage), sc, cm) -} - -// GetCodeFromError returns error code and code message that should be returned to the client -// in a response based on the error message -func getCodeFromError(err error) (int, string) { - var errCode domains.ErrorCode = domains.ErrGeneric - - switch { - case errors.Is(err, domains.ErrMerklerootNotFound): - errCode = domains.ErrMerkleRootNotFound - return http.StatusNotFound, errCode.String() - case errors.Is(err, domains.ErrMerklerootNotInLongestChain): - errCode = domains.ErrMerkleRootNotInLC - return http.StatusConflict, errCode.String() - case errors.Is(err, domains.ErrMerklerootInvalidBatchSize): - errCode = domains.ErrInvalidBatchSize - return http.StatusBadRequest, errCode.String() - default: - return http.StatusInternalServerError, errCode.String() - } -} - -func parseError(err error, statusCode int, code string) (*DetailedError, int) { - return &DetailedError{ - Message: err.Error(), - Code: code, - }, statusCode -}