Skip to content

Commit

Permalink
feat(SPV-846): refactor how the errors are returned (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
pawellewandowski98 authored Jul 4, 2024
1 parent 2ff6071 commit 869de71
Show file tree
Hide file tree
Showing 21 changed files with 348 additions and 213 deletions.
137 changes: 137 additions & 0 deletions errors/definitions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package errors

// How the Codes are generated?
// 1. "error" - like mandatory prefix for all error codes
// 2. (optional) {error group} - e.g. "configuration", "capabilities", "spv"
// 3. (optional) {subject} - name of model (with or without specific field) or some noun e.g. "body", "auth-header", "transaction", "paymail-address"
// 4. (optional) {reason} - what happened, e.g. "not-found", "missing", "invalid"

// CONFIG ERRORS
var (
// ErrDomainMissing is the error for missing domain
ErrDomainMissing = SPVError{Message: "domain is missing", StatusCode: 500, Code: "error-configuration-domain-missing"}

// ErrPortMissing is when the port is not found
ErrPortMissing = SPVError{Message: "missing a port", StatusCode: 500, Code: "error-configuration-port-missing"}

// ErrServiceNameMissing is when the service name is not found
ErrServiceNameMissing = SPVError{Message: "missing service name", StatusCode: 500, Code: "error-configuration-service-name-missing"}

// ErrCapabilitiesMissing is when the capabilities struct is nil or not set
ErrCapabilitiesMissing = SPVError{Message: "missing capabilities struct", StatusCode: 500, Code: "error-configuration-capabilities-missing"}

// ErrBsvAliasMissing is when the bsv alias version is missing
ErrBsvAliasMissing = SPVError{Message: "missing bsv alias version", StatusCode: 500, Code: "error-configuration-bsv-alias-missing"}

// ErrServiceProviderNil is the error for having a nil service provider
ErrServiceProviderNil = SPVError{Message: "service provider is nil", StatusCode: 500, Code: "error-configuration-service-provider-nil"}
)

// CAPABILITY ERRORS
var (
//ErrPrefixOrDomainMissing is when the prefix or domain is missing
ErrPrefixOrDomainMissing = SPVError{Message: "prefix or domain is missing", StatusCode: 400, Code: "error-capabilities-prefix-or-domain-missing"}

//ErrDomainUnknown is when the domain is not in the list of allowed domains
ErrDomainUnknown = SPVError{Message: "paymail domain is unknown", StatusCode: 400, Code: "error-capabilities-domain-unknown"}

//ErrCastingNestedCapabilities is when the nested capabilities cannot be cast
ErrCastingNestedCapabilities = SPVError{Message: "failed to cast nested capabilities", StatusCode: 500, Code: "error-capabilities-nested-capabilities-failed-to-cast"}
)

// PARSING ERRORS
var (
// ErrCannotBindRequest is when request body cannot be bind into struct
ErrCannotBindRequest = SPVError{Message: "cannot bind request body", StatusCode: 400, Code: "error-bind-body-invalid"}

// ErrProcessingHex is when error occurred during processing hex
ErrProcessingHex = SPVError{Message: "cannot process hex", StatusCode: 400, Code: "error-processing-hex"}

// ErrProcessingBEEF is when error occurred during processing beef
ErrProcessingBEEF = SPVError{Message: "cannot process beef", StatusCode: 400, Code: "error-processing-beef"}
)

// PAYMAIL ERRORS
var (
// ErrCouldNotFindPaymail is when could not find paymail
ErrCouldNotFindPaymail = SPVError{Message: "invalid paymail", StatusCode: 400, Code: "error-paymail-not-found"}
)

// INVALID FIELD ERRORS
var (
// ErrInvalidPaymail is when the paymail is invalid
ErrInvalidPaymail = SPVError{Message: "invalid paymail", StatusCode: 400, Code: "error-paymail-invalid"}

// ErrInvalidPubKey is when the pubkey is invalid
ErrInvalidPubKey = SPVError{Message: "invalid pubkey", StatusCode: 400, Code: "error-pubkey-invalid"}

// ErrInvalidSignature is when the signature is invalid
ErrInvalidSignature = SPVError{Message: "invalid signature", StatusCode: 400, Code: "error-signature-invalid"}

// ErrInvalidScript is when the script is invalid
ErrInvalidScript = SPVError{Message: "invalid script", StatusCode: 400, Code: "error-script-invalid"}

// ErrInvalidTimestamp is when the timestamp is invalid
ErrInvalidTimestamp = SPVError{Message: "invalid timestamp", StatusCode: 400, Code: "error-timestamp-invalid"}

// ErrInvalidSenderHandle is when the sender handle is invalid
ErrInvalidSenderHandle = SPVError{Message: "invalid sender handle", StatusCode: 400, Code: "error-sender-handle-invalid"}
)

// MISSING FIELD ERRORS

var (
// ErrMissingFieldReference is when the reference field is required but missing
ErrMissingFieldReference = SPVError{Message: "missing required field: reference", StatusCode: 400, Code: "error-missing-field-reference"}

// ErrMissingFieldHex is when the hex field is required but missing
ErrMissingFieldHex = SPVError{Message: "missing required field: hex", StatusCode: 400, Code: "error-missing-field-hex"}

// ErrMissingFieldBEEF is when the beef field is required but missing
ErrMissingFieldBEEF = SPVError{Message: "missing required field: beef", StatusCode: 400, Code: "error-missing-field-beef"}

// ErrMissingFieldSignature is when the signature field is required but missing
ErrMissingFieldSignature = SPVError{Message: "missing required field: signature", StatusCode: 400, Code: "error-missing-field-signature"}

// ErrMissingFieldPubKey is when the pubkey field is required but missing
ErrMissingFieldPubKey = SPVError{Message: "missing required field: pubkey", StatusCode: 400, Code: "error-missing-field-pubkey"}

// ErrMissingFieldSatoshis is when the satoshis field is required but missing
ErrMissingFieldSatoshis = SPVError{Message: "missing required field: satoshis", StatusCode: 400, Code: "error-missing-field-satoshis"}
)

// EMPTY FIELDS ERRORS
var (
// ErrSenderHandleEmpty is when the server handle is empty
ErrSenderHandleEmpty = SPVError{Message: "empty sender handle", StatusCode: 400, Code: "error-sender-handle-empty"}

// ErrDtEmpty is when the dt field is empty
ErrDtEmpty = SPVError{Message: "empty dt", StatusCode: 400, Code: "error-dt-empty"}
)

// SPV ERRORS
var (
// ErrNoOutputs is when there are no outputs
ErrNoOutputs = SPVError{Message: "invalid output, no outputs", StatusCode: 417, Code: "error-spv-no-outputs"}

// ErrNoInputs is when there are no inputs
ErrNoInputs = SPVError{Message: "invalid input, no inputs", StatusCode: 417, Code: "error-spv-no-inputs"}

// ErrInvalidParentTransactions is when the parent transactions are invalid
ErrInvalidParentTransactions = SPVError{Message: "invalid parent transactions, no matching transactions for input", StatusCode: 417, Code: "error-spv-parent-tx-invalid"}

// ErrLockTimeAndSequence is when the locktime and sequence are invalid
ErrLockTimeAndSequence = SPVError{Message: "nLocktime is set and nSequence is not max, therefore this could be a non-final tx which is not currently supported", StatusCode: 417, Code: "error-spv-locktime-sequence-invalid"}

// ErrOutputValueTooHigh is when the satoshis output is too high on a transaction
ErrOutputValueTooHigh = SPVError{Message: "invalid input and output sum, outputs can not be larger than inputs", StatusCode: 417, Code: "error-spv-output-value-too-high"}

// ErrBUMPAncestorNotPresent is when the input mined ancestor is not present in BUMPs
ErrBUMPAncestorNotPresent = SPVError{Message: "invalid BUMP - input mined ancestor is not present in BUMPs", StatusCode: 417, Code: "error-spv-bump-ancestor-not-present"}

// ErrBUMPCouldNotFindMinedParent is when the mined parent for input could not be found
ErrBUMPCouldNotFindMinedParent = SPVError{Message: "invalid BUMP - cannot find mined parent for input", StatusCode: 417, Code: "error-spv-bump-mined-parent-not-found"}

// ErrNoMatchingTransactionsForInput is when no matching transaction for input can be found
ErrNoMatchingTransactionsForInput = SPVError{Message: "invalid parent transactions, no matching transactions for input", StatusCode: 417, Code: "error-spv-bump-ancestor-not-present"}
)
43 changes: 43 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package errors

type ExtendedError interface {
error
GetCode() string
GetMessage() string
GetStatusCode() int
}

// SPVError is extended error which holds information about http status and code that should be returned
type SPVError struct {
Code string
Message string
StatusCode int
}

// ResponseError is an error which will be returned in HTTP response
type ResponseError struct {
Code string `json:"code"`
Message string `json:"message"`
}

const UnknownErrorCode = "error-unknown"

// Error returns the error message string for SPVError, satisfying the error interface
func (e SPVError) Error() string {
return e.Message
}

// GetCode returns the error code string for SPVError
func (e SPVError) GetCode() string {
return e.Code
}

// GetMessage returns the error message string for SPVError
func (e SPVError) GetMessage() string {
return e.Message
}

// GetStatusCode returns the error status code for SPVError
func (e SPVError) GetStatusCode() int {
return e.StatusCode
}
27 changes: 27 additions & 0 deletions errors/http_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package errors

import (
"errors"
"github.com/gin-gonic/gin"
)

// ErrorResponse is a standard way to return errors to the client
func ErrorResponse(c *gin.Context, err error) {
response, statusCode := getError(err)
c.JSON(statusCode, response)
}

func getError(err error) (ResponseError, int) {
var extendedErr ExtendedError
if errors.As(err, &extendedErr) {
return ResponseError{
Code: extendedErr.GetCode(),
Message: extendedErr.GetMessage(),
}, extendedErr.GetStatusCode()
}

return ResponseError{
Code: UnknownErrorCode,
Message: "Unable to get information about error",
}, 500
}
9 changes: 5 additions & 4 deletions server/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"fmt"
"github.com/bitcoin-sv/go-paymail/errors"
"net/http"
"strings"

Expand Down Expand Up @@ -139,13 +140,13 @@ func (c *Configuration) showCapabilities(context *gin.Context) {
}

if !c.IsAllowedDomain(host) {
ErrorResponse(context, ErrorUnknownDomain, "domain unknown: "+host, http.StatusBadRequest)
errors.ErrorResponse(context, errors.ErrDomainUnknown)
return
}

capabilities, err := c.EnrichCapabilities(host)
if err != nil {
ErrorResponse(context, ErrorEncodingResponse, err.Error(), http.StatusBadRequest)
errors.ErrorResponse(context, err)
return
}

Expand Down Expand Up @@ -173,7 +174,7 @@ func (c *Configuration) EnrichCapabilities(host string) (*paymail.CapabilitiesPa
for nestedKey, nestedCap := range cap {
nestedObj, ok := payload.Capabilities[key].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to cast nested capabilities")
return nil, errors.ErrCastingNestedCapabilities
}
nestedObj[nestedKey] = serviceUrl + nestedCap.Path
}
Expand All @@ -183,7 +184,7 @@ func (c *Configuration) EnrichCapabilities(host string) (*paymail.CapabilitiesPa

func generateServiceURL(prefix, domain, apiVersion, serviceName string) (string, error) {
if len(prefix) == 0 || len(domain) == 0 {
return "", ErrPrefixOrDomainMissing
return "", errors.ErrPrefixOrDomainMissing
}
strBuilder := new(strings.Builder)
strBuilder.WriteString(prefix)
Expand Down
15 changes: 8 additions & 7 deletions server/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package server

import (
"github.com/bitcoin-sv/go-paymail/errors"
"github.com/rs/zerolog"
"slices"
"strings"
Expand Down Expand Up @@ -47,28 +48,28 @@ func (c *Configuration) Validate() error {

// Requires domains for the server to run
if len(c.PaymailDomains) == 0 && !c.PaymailDomainsValidationDisabled {
return ErrDomainMissing
return errors.ErrDomainMissing
}

// Requires a port
if c.Port <= 0 {
return ErrPortMissing
return errors.ErrPortMissing
}

// todo: validate the []domains

// Sanitize and standardize the service name
c.ServiceName = paymail.SanitizePathName(c.ServiceName)
if len(c.ServiceName) == 0 {
return ErrServiceNameMissing
return errors.ErrServiceNameMissing
}

if c.BSVAliasVersion == "" {
return ErrBsvAliasMissing
return errors.ErrBsvAliasMissing
}

if c.callableCapabilities == nil || len(c.callableCapabilities) == 0 {
return ErrCapabilitiesMissing
return errors.ErrCapabilitiesMissing
}

return nil
Expand Down Expand Up @@ -96,7 +97,7 @@ func (c *Configuration) AddDomain(domain string) (err error) {

// Sanity check
if len(domain) == 0 {
return ErrDomainMissing
return errors.ErrDomainMissing
}

// Sanitize and standardize
Expand All @@ -121,7 +122,7 @@ func NewConfig(serviceProvider *PaymailServiceLocator, opts ...ConfigOps) (*Conf

// Check that a service provider is set
if serviceProvider == nil {
return nil, ErrServiceProviderNil
return nil, errors.ErrServiceProviderNil
}

// Create the base configuration
Expand Down
Loading

0 comments on commit 869de71

Please sign in to comment.