From 1922e58e22ff95de0fe35398523717a1a72ccdb1 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:52:42 +0100 Subject: [PATCH 1/9] feat(SPV-1583): errorx & problem details init --- .../assert_spvwallet_application.go | 18 ++++++ actions/v2/admin/internal/mapping/paymail.go | 6 +- actions/v2/admin/users/create.go | 14 +++-- actions/v2/admin/users/create_test.go | 22 +++++++ actions/v2/admin/users/get.go | 4 +- actions/v2/admin/users/paymail_add.go | 5 +- actions/v2/admin/users/paymail_add_test.go | 2 +- engine/tester/jsonrequire/funcs.go | 12 ++++ errdef/clienterr/builder.go | 60 +++++++++++++++++++ errdef/clienterr/client_error_definition.go | 20 +++++++ errdef/clienterr/client_errors.go | 19 ++++++ errdef/clienterr/http_error.go | 53 ++++++++++++++++ errdef/problem_details.go | 40 +++++++++++++ errdef/properties.go | 5 ++ errdef/traits.go | 10 ++++ go.mod | 1 + go.sum | 2 + 17 files changed, 281 insertions(+), 12 deletions(-) create mode 100644 errdef/clienterr/builder.go create mode 100644 errdef/clienterr/client_error_definition.go create mode 100644 errdef/clienterr/client_errors.go create mode 100644 errdef/clienterr/http_error.go create mode 100644 errdef/problem_details.go create mode 100644 errdef/properties.go create mode 100644 errdef/traits.go diff --git a/actions/testabilities/assert_spvwallet_application.go b/actions/testabilities/assert_spvwallet_application.go index b5150bd8e..53d8ffbf2 100644 --- a/actions/testabilities/assert_spvwallet_application.go +++ b/actions/testabilities/assert_spvwallet_application.go @@ -29,6 +29,7 @@ type SPVWalletResponseAssertions interface { HasStatus(status int) SPVWalletResponseAssertions WithJSONf(expectedFormat string, args ...any) WithJSONMatching(expectedTemplateFormat string, params map[string]any) + WithProblemDetails(status int, errType string, containDetails ...string) JSONValue() JsonValueGetter // IsUnauthorized asserts that the response status code is 401 and the error is about lack of authorization. IsUnauthorized() @@ -152,6 +153,23 @@ func (a *responseAssertions) JSONValue() JsonValueGetter { return jsonrequire.NewGetterWithJSON(a.t, a.response.String()) } +func (a *responseAssertions) WithProblemDetails(status int, errType string, containDetails ...string) { + a.t.Helper() + a.assert.Equal(status, a.response.StatusCode()) + + a.WithJSONMatching(`{ + "detail": "{{ containsAll .detailsParts }}", + "instance": {{ anything }}, + "status": {{ .status }}, + "title": {{ anything }}, + "type": "bad_request" + }`, map[string]any{ + "status": status, + "type": errType, + "detailsParts": containDetails, + }) +} + func (a *responseAssertions) assertJSONContentType() { a.t.Helper() contentType := a.response.Header().Get("Content-Type") diff --git a/actions/v2/admin/internal/mapping/paymail.go b/actions/v2/admin/internal/mapping/paymail.go index cfbedea69..1cecb7ef4 100644 --- a/actions/v2/admin/internal/mapping/paymail.go +++ b/actions/v2/admin/internal/mapping/paymail.go @@ -2,9 +2,9 @@ package mapping import ( "github.com/bitcoin-sv/go-paymail" - adminerrors "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/errors" "github.com/bitcoin-sv/spv-wallet/api" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "github.com/bitcoin-sv/spv-wallet/lox" ) @@ -84,14 +84,14 @@ func parsePaymail(r *api.RequestsAddPaymail) (string, string, error) { if request.HasAddress() && (request.HasAlias() || request.HasDomain()) && !request.AddressEqualsTo(request.Alias+"@"+request.Domain) { - return "", "", adminerrors.ErrPaymailInconsistent + return "", "", clienterr.BadRequest.New().WithDetailf("Alias and Domain are inconsistent with Address").Err() } if !request.HasAddress() { request.Address = request.Alias + "@" + request.Domain } alias, domain, sanitized := paymail.SanitizePaymail(request.Address) if sanitized == "" { - return "", "", adminerrors.ErrInvalidPaymail + return "", "", clienterr.BadRequest.New().WithDetailf("Invalid paymail address: %s", request.Address).Err() } return alias, domain, nil } diff --git a/actions/v2/admin/users/create.go b/actions/v2/admin/users/create.go index 33f27eec5..6c322e6ef 100644 --- a/actions/v2/admin/users/create.go +++ b/actions/v2/admin/users/create.go @@ -1,6 +1,7 @@ package users import ( + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "net/http" primitives "github.com/bitcoin-sv/go-sdk/primitives/ec" @@ -16,19 +17,23 @@ import ( // CreateUser creates a new user func (s *APIAdminUsers) CreateUser(c *gin.Context) { var request api.RequestsCreateUser + if err := c.Bind(&request); err != nil { - spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest.Wrap(err), s.logger) + // TODO: Bind does AbortWithError internally, so we should not call Response, I guess + clienterr.UnprocessableEntity. + Wrap(err, "cannot bind request"). + Response(c, s.logger) return } if err := validatePubKey(request.PublicKey); err != nil { - spverrors.ErrorResponse(c, err, s.logger) + clienterr.Response(c, err, s.logger) return } newUser, err := mapping.RequestCreateUserToNewUserModel(&request) if err != nil { - spverrors.ErrorResponse(c, err, s.logger) + clienterr.Response(c, err, s.logger) return } @@ -47,7 +52,8 @@ func (s *APIAdminUsers) CreateUser(c *gin.Context) { func validatePubKey(pubKey string) error { _, err := primitives.PublicKeyFromString(pubKey) if err != nil { - return adminerrors.ErrInvalidPublicKey.Wrap(err) + return clienterr.BadRequest. + Wrap(err, "Cannot parse public key: '%s'", pubKey).Err() } return nil } diff --git a/actions/v2/admin/users/create_test.go b/actions/v2/admin/users/create_test.go index 60a2874e1..eca3c1adb 100644 --- a/actions/v2/admin/users/create_test.go +++ b/actions/v2/admin/users/create_test.go @@ -464,5 +464,27 @@ func TestAddUserWithWrongPaymailDomain(t *testing.T) { HasStatus(400). WithJSONf(apierror.ExpectedJSON("error-invalid-domain", "invalid domain")) }) +} + +func TestTryToAddWithWrongPubKey(t *testing.T) { + // given: + given, then := testabilities.New(t) + cleanup := given.StartedSPVWalletWithConfiguration( + testengine.WithV2(), + ) + defer cleanup() + // and: + client := given.HttpClient().ForAdmin() + + // when: + res, _ := client.R(). + SetBody(map[string]any{ + "publicKey": "wrong", + }). + Post("/api/v2/admin/users") + + // then: + then.Response(res). + WithProblemDetails(400, "bad_request", "Cannot parse public key") } diff --git a/actions/v2/admin/users/get.go b/actions/v2/admin/users/get.go index c84092259..c9f469016 100644 --- a/actions/v2/admin/users/get.go +++ b/actions/v2/admin/users/get.go @@ -1,10 +1,10 @@ package users import ( + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "net/http" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/gin-gonic/gin" ) @@ -12,7 +12,7 @@ import ( func (s *APIAdminUsers) UserById(c *gin.Context, id string) { user, err := s.engine.UsersService().GetByID(c, id) if err != nil { - spverrors.ErrorResponse(c, err, s.logger) + clienterr.Response(c, err, s.logger) return } diff --git a/actions/v2/admin/users/paymail_add.go b/actions/v2/admin/users/paymail_add.go index 134946778..008aef5e5 100644 --- a/actions/v2/admin/users/paymail_add.go +++ b/actions/v2/admin/users/paymail_add.go @@ -1,6 +1,7 @@ package users import ( + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "net/http" adminerrors "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/errors" @@ -16,13 +17,13 @@ import ( func (s *APIAdminUsers) AddPaymailToUser(c *gin.Context, id string) { var request api.RequestsAddPaymail if err := c.Bind(&request); err != nil { - spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest.Wrap(err), s.logger) + clienterr.UnprocessableEntity.Wrap(err, "cannot bind request").Response(c, s.logger) return } newPaymail, err := mapping.RequestAddPaymailToNewPaymailModel(&request, id) if err != nil { - spverrors.ErrorResponse(c, err, s.logger) + clienterr.Response(c, err, s.logger) return } diff --git a/actions/v2/admin/users/paymail_add_test.go b/actions/v2/admin/users/paymail_add_test.go index 6f80ec35e..19c2a7d54 100644 --- a/actions/v2/admin/users/paymail_add_test.go +++ b/actions/v2/admin/users/paymail_add_test.go @@ -286,6 +286,6 @@ func TestAddPaymailWithBothPaymailAndAliasDomainPair(t *testing.T) { // then: then.Response(res). HasStatus(400). - WithJSONf(apierror.ExpectedJSON("error-user-inconsistent-paymail", "inconsistent paymail address and alias/domain")) + WithProblemDetails(400, "bad_request", "inconsistent") }) } diff --git a/engine/tester/jsonrequire/funcs.go b/engine/tester/jsonrequire/funcs.go index d8b2cffa8..8a9016d33 100644 --- a/engine/tester/jsonrequire/funcs.go +++ b/engine/tester/jsonrequire/funcs.go @@ -19,6 +19,7 @@ var funcsMap = template.FuncMap{ "anything": anything, "matchTxByFormat": matchTxByFormat, "matchDestination": matchDestination, + "containsAll": containsAll, } func anything() string { @@ -76,6 +77,17 @@ func matchNumber() string { return regexPlaceholder(`^\\d+$`) } +func containsAll(parts []string) string { + partsRegex := strings.Builder{} + partsRegex.WriteString("^") + for _, part := range parts { + partsRegex.WriteString(fmt.Sprintf(`(.*%s)`, part)) + } + partsRegex.WriteString(".*$") + s := partsRegex.String() + return regexPlaceholder(s) +} + // regexPlaceholder adds slashes at the beginning and end of a string // it is an indicator for placeholder matcher algorithm that it is a regex func regexPlaceholder(statement string) string { diff --git a/errdef/clienterr/builder.go b/errdef/clienterr/builder.go new file mode 100644 index 000000000..d01a0a8fa --- /dev/null +++ b/errdef/clienterr/builder.go @@ -0,0 +1,60 @@ +package clienterr + +import ( + "fmt" + "github.com/bitcoin-sv/spv-wallet/errdef" + "strings" + + "github.com/gin-gonic/gin" + "github.com/joomcode/errorx" + "github.com/rs/zerolog" +) + +var propProblemDetails = errorx.RegisterProperty("problem_details") +var clientError = errorx.NewNamespace("client").NewType("error") + +type Builder struct { + from *ClientErrorDefinition + problemDetails errdef.ProblemDetails + cause error +} + +func (b *Builder) Wrap(cause error, msg string, args ...any) *Builder { + b.cause = cause + b.problemDetails. + FromInternalError(cause). + PushDetail(fmt.Sprintf(msg, args...)) + + return b +} + +func (b *Builder) Err() *errorx.Error { + var err *errorx.Error + if b.cause != nil { + err = clientError.Wrap(b.cause, "") + } else { + err = clientError.NewWithNoMessage() + } + return err.WithProperty(propProblemDetails, b.problemDetails) +} + +func (b *Builder) Response(c *gin.Context, log *zerolog.Logger) { + Response(c, b.Err(), log) +} + +func (b *Builder) WithDetailf(detail string, args ...any) *Builder { + b.problemDetails.PushDetail(fmt.Sprintf(detail, args...)) + return b +} + +func (b *Builder) WithInstance(parts ...any) *Builder { + var sb strings.Builder + for i, p := range parts { + sb.WriteString(fmt.Sprintf("%v", p)) + if i < len(parts)-1 { + sb.WriteString("/") + } + } + b.problemDetails.Instance = sb.String() + return b +} diff --git a/errdef/clienterr/client_error_definition.go b/errdef/clienterr/client_error_definition.go new file mode 100644 index 000000000..0f73efcc6 --- /dev/null +++ b/errdef/clienterr/client_error_definition.go @@ -0,0 +1,20 @@ +package clienterr + +type ClientErrorDefinition struct { + title string + typeName string + httpCode int +} + +func (c ClientErrorDefinition) Wrap(cause error, msg string, args ...any) *Builder { + return c.New().Wrap(cause, msg, args...) +} + +func (c ClientErrorDefinition) New() *Builder { + b := &Builder{from: &c} + b.problemDetails.Status = c.httpCode + b.problemDetails.Title = c.title + + b.problemDetails.Type = c.typeName + return b +} diff --git a/errdef/clienterr/client_errors.go b/errdef/clienterr/client_errors.go new file mode 100644 index 000000000..8df437c69 --- /dev/null +++ b/errdef/clienterr/client_errors.go @@ -0,0 +1,19 @@ +package clienterr + +var BadRequest = ClientErrorDefinition{ + title: "Bad request", + typeName: "bad_request", + httpCode: 400, +} + +var UnprocessableEntity = ClientErrorDefinition{ + title: "Unprocessable entity", + typeName: "unprocessable_entity", + httpCode: 422, +} + +var Unauthorized = ClientErrorDefinition{ + title: "Unauthorized", + typeName: "unauthorized", + httpCode: 401, +} diff --git a/errdef/clienterr/http_error.go b/errdef/clienterr/http_error.go new file mode 100644 index 000000000..6fc16f7ac --- /dev/null +++ b/errdef/clienterr/http_error.go @@ -0,0 +1,53 @@ +package clienterr + +import ( + "errors" + errdef2 "github.com/bitcoin-sv/spv-wallet/errdef" + + "github.com/gin-gonic/gin" + "github.com/joomcode/errorx" + "github.com/rs/zerolog" +) + +func Response(c *gin.Context, err error, log *zerolog.Logger) { + problem, logLevel := problemDetailsFromError(err) + log.WithLevel(logLevel).Err(err).Msgf("Error HTTP response, returning %d: %s", problem.Status, problem.Detail) + c.JSON(problem.Status, problem) +} + +func problemDetailsFromError(err error) (problem errdef2.ProblemDetails, level zerolog.Level) { + var ex *errorx.Error + if errors.As(err, &ex) { + if details, ok := ex.Property(propProblemDetails); ok { + problem = details.(errdef2.ProblemDetails) + level = zerolog.InfoLevel + return + } + + // map internal error to problem details + level = zerolog.WarnLevel + problem.Type = "internal" + problem.FromInternalError(ex) + if errorx.HasTrait(ex, errdef2.TraitUnsupported) { + problem.Title = "Unsupported operation" + problem.Status = 501 + return + } + if errorx.HasTrait(ex, errdef2.TraitShouldNeverHappen) { + problem.Detail = "This should never happen" + } + + problem.Title = "Internal Server Error" + problem.Status = 500 + return + } + + level = zerolog.ErrorLevel + problem = errdef2.ProblemDetails{ + Type: "unknown_error", + Title: "Unknown error", + Status: 500, + Instance: "unknown_error", + } + return +} diff --git a/errdef/problem_details.go b/errdef/problem_details.go new file mode 100644 index 000000000..c2005efdd --- /dev/null +++ b/errdef/problem_details.go @@ -0,0 +1,40 @@ +package errdef + +import ( + "fmt" + "strings" + + "github.com/joomcode/errorx" +) + +type ProblemDetails struct { + Type string `json:"type"` + Title string `json:"title"` + Status int `json:"status"` + Detail string `json:"detail"` + Instance string `json:"instance"` +} + +func (p *ProblemDetails) PushDetail(detail string) *ProblemDetails { + separator := "" + if p.Detail != "" { + separator = "; " + } + p.Detail = fmt.Sprintf("%v%s%v", detail, separator, p.Detail) + return p +} + +func (p *ProblemDetails) FromInternalError(err error) *ProblemDetails { + ex := errorx.Cast(err) + if ex == nil { + return p + } + + if hint, ok := ex.Property(PropPublicHint); ok { + p.PushDetail(fmt.Sprintf("Hint: %v", hint)) + } + + p.Instance = strings.ReplaceAll(ex.Type().FullName(), ".", "/") + + return p +} diff --git a/errdef/properties.go b/errdef/properties.go new file mode 100644 index 000000000..16a9e4036 --- /dev/null +++ b/errdef/properties.go @@ -0,0 +1,5 @@ +package errdef + +import "github.com/joomcode/errorx" + +var PropPublicHint = errorx.RegisterPrintableProperty("public_hint") diff --git a/errdef/traits.go b/errdef/traits.go new file mode 100644 index 000000000..4f3ec6ed3 --- /dev/null +++ b/errdef/traits.go @@ -0,0 +1,10 @@ +package errdef + +import "github.com/joomcode/errorx" + +var TraitConfig = errorx.RegisterTrait("config") +var TraitIllegalArgument = errorx.RegisterTrait("illegal_argument") +var TraitAuth = errorx.RegisterTrait("auth") +var TraitARC = errorx.RegisterTrait("arc") +var TraitShouldNeverHappen = errorx.RegisterTrait("should_never_happen") +var TraitUnsupported = errorx.RegisterTrait("unsupported") diff --git a/go.mod b/go.mod index 616b12a82..8217f0e42 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/joomcode/errorx v1.2.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 9df69ecb3..1f864c1ae 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/joomcode/errorx v1.2.0 h1:7Y/fguon+9r6a/75Rv3nrUwS7nXNEcJjLShjCvz00Og= +github.com/joomcode/errorx v1.2.0/go.mod h1:Mbz68VA9hsQLT50iCQQUZ2Z1XYAKYB4EoFkFCTFyiJM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= From 77b3f74a30e4843fcd15ca5d9b20275456c51068 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:37:58 +0100 Subject: [PATCH 2/9] feat(SPV-1535): errors for create and paymail_add as errorx --- .../assert_spvwallet_application.go | 2 +- actions/v2/admin/users/create.go | 18 ++++++++----- actions/v2/admin/users/create_test.go | 10 +++---- actions/v2/admin/users/paymail_add.go | 18 ++++++++----- actions/v2/admin/users/paymail_add_test.go | 10 +++---- config/errors/errors.go | 10 ++++--- config/utils.go | 5 +++- engine/v2/database/errors/errors.go | 9 ++++--- engine/v2/database/repository/paymails.go | 9 ++++--- engine/v2/database/repository/users.go | 12 ++++----- .../paymails/paymailerrors/paymail_errors.go | 18 ++++++++----- engine/v2/paymails/paymails_service.go | 26 +++++++++---------- engine/v2/paymails/paymailsmodels/paymail.go | 10 ++++--- engine/v2/users/users_service.go | 20 +++++++------- errdef/traits.go | 1 + 15 files changed, 101 insertions(+), 77 deletions(-) diff --git a/actions/testabilities/assert_spvwallet_application.go b/actions/testabilities/assert_spvwallet_application.go index 53d8ffbf2..68cce6b9a 100644 --- a/actions/testabilities/assert_spvwallet_application.go +++ b/actions/testabilities/assert_spvwallet_application.go @@ -162,7 +162,7 @@ func (a *responseAssertions) WithProblemDetails(status int, errType string, cont "instance": {{ anything }}, "status": {{ .status }}, "title": {{ anything }}, - "type": "bad_request" + "type": "{{ .type }}" }`, map[string]any{ "status": status, "type": errType, diff --git a/actions/v2/admin/users/create.go b/actions/v2/admin/users/create.go index 6c322e6ef..b05bc7b7e 100644 --- a/actions/v2/admin/users/create.go +++ b/actions/v2/admin/users/create.go @@ -2,14 +2,13 @@ package users import ( "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" + "github.com/joomcode/errorx" "net/http" primitives "github.com/bitcoin-sv/go-sdk/primitives/ec" - adminerrors "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/errors" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" "github.com/bitcoin-sv/spv-wallet/api" configerrors "github.com/bitcoin-sv/spv-wallet/config/errors" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" "github.com/gin-gonic/gin" ) @@ -39,10 +38,17 @@ func (s *APIAdminUsers) CreateUser(c *gin.Context) { createdUser, err := s.engine.UsersService().Create(c, newUser) if err != nil { - spverrors.MapResponse(c, err, s.logger). - If(configerrors.ErrUnsupportedDomain).Then(adminerrors.ErrInvalidDomain). - If(paymailerrors.ErrInvalidAvatarURL).Then(adminerrors.ErrInvalidAvatarURL). - Else(adminerrors.ErrCreatingUser) + if errorx.IsOfType(err, configerrors.UnsupportedDomain) { + clienterr.BadRequest. + Wrap(err, "Unsupported domain"). + Response(c, s.logger) + } else if errorx.IsOfType(err, paymailerrors.InvalidAvatarURL) { + clienterr.UnprocessableEntity. + Wrap(err, "Invalid avatar url"). + Response(c, s.logger) + } else { + clienterr.Response(c, err, s.logger) + } return } diff --git a/actions/v2/admin/users/create_test.go b/actions/v2/admin/users/create_test.go index eca3c1adb..146ea6d24 100644 --- a/actions/v2/admin/users/create_test.go +++ b/actions/v2/admin/users/create_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/bitcoin-sv/spv-wallet/actions/testabilities" - "github.com/bitcoin-sv/spv-wallet/actions/testabilities/apierror" testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" ) @@ -131,8 +130,7 @@ func TestCreateUserWithBadURLAvatar(t *testing.T) { // then: then.Response(res). - HasStatus(422). - WithJSONf(apierror.ExpectedJSON("error-user-invalid-avatar-url", "invalid avatar url")) + WithProblemDetails(422, "unprocessable_entity", "Invalid avatar url") } @@ -441,8 +439,7 @@ func TestAddUserWithWrongPaymailDomain(t *testing.T) { // then: then.Response(res). - HasStatus(400). - WithJSONf(apierror.ExpectedJSON("error-invalid-domain", "invalid domain")) + WithProblemDetails(400, "bad_request", "Unsupported domain") }) t.Run("Try to add using alias and domain as address", func(t *testing.T) { @@ -461,8 +458,7 @@ func TestAddUserWithWrongPaymailDomain(t *testing.T) { // then: then.Response(res). - HasStatus(400). - WithJSONf(apierror.ExpectedJSON("error-invalid-domain", "invalid domain")) + WithProblemDetails(400, "bad_request", "Unsupported domain") }) } diff --git a/actions/v2/admin/users/paymail_add.go b/actions/v2/admin/users/paymail_add.go index 008aef5e5..608458239 100644 --- a/actions/v2/admin/users/paymail_add.go +++ b/actions/v2/admin/users/paymail_add.go @@ -2,13 +2,12 @@ package users import ( "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" + "github.com/joomcode/errorx" "net/http" - adminerrors "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/errors" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" "github.com/bitcoin-sv/spv-wallet/api" configerrors "github.com/bitcoin-sv/spv-wallet/config/errors" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" "github.com/gin-gonic/gin" ) @@ -29,10 +28,17 @@ func (s *APIAdminUsers) AddPaymailToUser(c *gin.Context, id string) { createdPaymail, err := s.engine.PaymailsService().Create(c, newPaymail) if err != nil { - spverrors.MapResponse(c, err, s.logger). - If(configerrors.ErrUnsupportedDomain).Then(adminerrors.ErrInvalidDomain). - If(paymailerrors.ErrInvalidAvatarURL).Then(adminerrors.ErrInvalidAvatarURL). - Else(adminerrors.ErrAddingPaymail) + if errorx.IsOfType(err, configerrors.UnsupportedDomain) { + clienterr.BadRequest. + Wrap(err, "Unsupported domain"). + Response(c, s.logger) + } else if errorx.IsOfType(err, paymailerrors.InvalidAvatarURL) { + clienterr.UnprocessableEntity. + Wrap(err, "Invalid avatar url"). + Response(c, s.logger) + } else { + clienterr.Response(c, err, s.logger) + } return } diff --git a/actions/v2/admin/users/paymail_add_test.go b/actions/v2/admin/users/paymail_add_test.go index 19c2a7d54..bd14e46d4 100644 --- a/actions/v2/admin/users/paymail_add_test.go +++ b/actions/v2/admin/users/paymail_add_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/bitcoin-sv/spv-wallet/actions/testabilities" - "github.com/bitcoin-sv/spv-wallet/actions/testabilities/apierror" testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" ) @@ -80,8 +79,7 @@ func TestAddPaymail(t *testing.T) { // then: then.Response(res). - HasStatus(422). - WithJSONf(apierror.ExpectedJSON("error-user-invalid-avatar-url", "invalid avatar url")) + WithProblemDetails(422, "unprocessable_entity", "Invalid avatar url") }) t.Run("Add a paymail to a user as admin using alias and domain as address", func(t *testing.T) { @@ -205,8 +203,7 @@ func TestAddPaymailWithWrongDomain(t *testing.T) { // then: then.Response(res). - HasStatus(400). - WithJSONf(apierror.ExpectedJSON("error-invalid-domain", "invalid domain")) + WithProblemDetails(400, "bad_request", "Unsupported domain") }) t.Run("Try to add using alias and domain as address", func(t *testing.T) { @@ -223,8 +220,7 @@ func TestAddPaymailWithWrongDomain(t *testing.T) { // then: then.Response(res). - HasStatus(400). - WithJSONf(apierror.ExpectedJSON("error-invalid-domain", "invalid domain")) + WithProblemDetails(400, "bad_request", "Unsupported domain") }) } diff --git a/config/errors/errors.go b/config/errors/errors.go index aad1abe4e..e215ed3f9 100644 --- a/config/errors/errors.go +++ b/config/errors/errors.go @@ -1,6 +1,10 @@ package errors -import "github.com/bitcoin-sv/spv-wallet/engine/spverrors" +import ( + "github.com/bitcoin-sv/spv-wallet/errdef" + "github.com/joomcode/errorx" +) -// ErrUnsupportedDomain is returned when the domain is not supported -var ErrUnsupportedDomain = spverrors.Newf("unsupported domain") +var Namespace = errorx.NewNamespace("config", errdef.TraitConfig) + +var UnsupportedDomain = Namespace.NewType("unsupported_domain", errdef.TraitIllegalArgument) diff --git a/config/utils.go b/config/utils.go index 48261a3ac..30703c45e 100644 --- a/config/utils.go +++ b/config/utils.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "github.com/bitcoin-sv/spv-wallet/errdef" "slices" configerrors "github.com/bitcoin-sv/spv-wallet/config/errors" @@ -11,7 +12,9 @@ import ( func (p *PaymailConfig) CheckDomain(domain string) error { if p.DomainValidationEnabled { if !slices.Contains(p.Domains, domain) { - return configerrors.ErrUnsupportedDomain + return configerrors.UnsupportedDomain. + New("domain %s is not supported", domain). + WithProperty(errdef.PropPublicHint, "Domain of provided paymail is not supported by this spv-wallet service") } } return nil diff --git a/engine/v2/database/errors/errors.go b/engine/v2/database/errors/errors.go index 07b99b61f..76b991a3b 100644 --- a/engine/v2/database/errors/errors.go +++ b/engine/v2/database/errors/errors.go @@ -1,6 +1,9 @@ package dberrors -import "github.com/bitcoin-sv/spv-wallet/models" +import ( + "github.com/joomcode/errorx" +) -// ErrDBFailed is when the database operation failed. -var ErrDBFailed = models.SPVError{Message: "database operation failed", StatusCode: 500, Code: "error-db-failed"} +var Namespace = errorx.NewNamespace("db") + +var QueryFailed = Namespace.NewType("query_failed") diff --git a/engine/v2/database/repository/paymails.go b/engine/v2/database/repository/paymails.go index bf45ad71b..1e88cf6b6 100644 --- a/engine/v2/database/repository/paymails.go +++ b/engine/v2/database/repository/paymails.go @@ -3,6 +3,7 @@ package repository import ( "context" "errors" + dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/database" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" @@ -32,7 +33,7 @@ func (p *Paymails) Create(ctx context.Context, newPaymail *paymailsmodels.NewPay } if err := p.db.WithContext(ctx).Create(&row).Error; err != nil { - return nil, err + return nil, dberrors.QueryFailed.Wrap(err, "failed to create paymail") } return p.newPaymailModel(row), nil @@ -48,7 +49,7 @@ func (p *Paymails) Find(ctx context.Context, alias, domain string) (*paymailsmod if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } - return nil, err + return nil, dberrors.QueryFailed.Wrap(err, "failed to find paymail by alias and domain") } return p.newPaymailModel(row), nil @@ -64,7 +65,7 @@ func (p *Paymails) FindForUser(ctx context.Context, alias, domain, userID string if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } - return nil, err + return nil, dberrors.QueryFailed.Wrap(err, "failed to find paymail by alias, domain and user") } return p.newPaymailModel(row), nil @@ -78,7 +79,7 @@ func (p *Paymails) GetDefault(ctx context.Context, userID string) (*paymailsmode Where("user_id = ?", userID). Order("created_at ASC"). First(&row).Error; err != nil { - return nil, err + return nil, dberrors.QueryFailed.Wrap(err, "failed to get default paymail for user") } return p.newPaymailModel(row), nil diff --git a/engine/v2/database/repository/users.go b/engine/v2/database/repository/users.go index 8f4e238b5..f8f47d501 100644 --- a/engine/v2/database/repository/users.go +++ b/engine/v2/database/repository/users.go @@ -2,8 +2,8 @@ package repository import ( "context" + dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/database" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" "github.com/bitcoin-sv/spv-wallet/engine/v2/users/usersmodels" @@ -28,7 +28,7 @@ func (u *Users) Exists(ctx context.Context, userID string) (bool, error) { var count int64 err := u.db.WithContext(ctx).Model(&database.User{}).Where("id = ?", userID).Count(&count).Error if err != nil { - return false, spverrors.Wrapf(err, "failed to check if user exists") + return false, dberrors.QueryFailed.Wrap(err, "failed to check if user exists by ID") } return count > 0, nil @@ -44,7 +44,7 @@ func (u *Users) GetIDByPubKey(ctx context.Context, pubKey string) (string, error Where("pub_key = ?", pubKey). First(&user).Error if err != nil { - return "", spverrors.Wrapf(err, "failed to get user by public key") + return "", dberrors.QueryFailed.Wrap(err, "failed to get user by public key") } return user.ID, nil @@ -58,7 +58,7 @@ func (u *Users) Get(ctx context.Context, userID string) (*usersmodels.User, erro Where("id = ?", userID). First(&user).Error if err != nil { - return nil, spverrors.Wrapf(err, "failed to get user by public key") + return nil, dberrors.QueryFailed.Wrap(err, "failed to get user by ID") } return mapToDomainUser(&user), nil @@ -81,7 +81,7 @@ func (u *Users) Create(ctx context.Context, newUser *usersmodels.NewUser) (*user } if err := query.Create(row).Error; err != nil { - return nil, spverrors.Wrapf(err, "failed to save user") + return nil, dberrors.QueryFailed.Wrap(err, "failed to create new user") } return mapToDomainUser(row), nil @@ -99,7 +99,7 @@ func (u *Users) GetBalance(ctx context.Context, userID string, bucket bucket.Nam Scan(&balance) if err != nil { - return 0, spverrors.Wrapf(err, "failed to get balance") + return 0, dberrors.QueryFailed.Wrap(err, "failed to get balance for user by ID") } return balance, nil diff --git a/engine/v2/paymails/paymailerrors/paymail_errors.go b/engine/v2/paymails/paymailerrors/paymail_errors.go index 5aab970df..6737f55cb 100644 --- a/engine/v2/paymails/paymailerrors/paymail_errors.go +++ b/engine/v2/paymails/paymailerrors/paymail_errors.go @@ -1,12 +1,16 @@ package paymailerrors -import "github.com/bitcoin-sv/spv-wallet/models" +import ( + "github.com/bitcoin-sv/spv-wallet/errdef" + "github.com/joomcode/errorx" +) -// ErrNoDefaultPaymailAddress is when the user has no default paymail - it actually means that the user has no paymail addresses at all. -var ErrNoDefaultPaymailAddress = models.SPVError{Message: "no default paymail address for user", StatusCode: 400, Code: "error-no-default-paymail-address"} +var Namespace = errorx.NewNamespace("paymail") -// ErrInvalidPaymailAddress is when the paymail address is invalid. -var ErrInvalidPaymailAddress = models.SPVError{Message: "invalid paymail address", StatusCode: 400, Code: "error-invalid-paymail-address"} +var InvalidAvatarURL = Namespace.NewType("invalid_avatar_url", errdef.TraitIllegalArgument) -// ErrInvalidAvatarURL is when url provided for paymail is not empty and is invalid URL format -var ErrInvalidAvatarURL = models.SPVError{Message: "invalid avatar url", StatusCode: 500, Code: "error-invalid-avatar-url"} +var InvalidPaymailAddress = Namespace.NewType("invalid_paymail_address", errdef.TraitIllegalArgument) + +var UserDoesntExist = Namespace.NewType("user_doesnt_exist", errdef.TraitNotFound) + +var NoDefaultPaymailAddress = Namespace.NewType("no_default_paymail_address", errdef.TraitIllegalArgument) diff --git a/engine/v2/paymails/paymails_service.go b/engine/v2/paymails/paymails_service.go index b7d07af5b..7828c6264 100644 --- a/engine/v2/paymails/paymails_service.go +++ b/engine/v2/paymails/paymails_service.go @@ -3,10 +3,10 @@ package paymails import ( "context" "errors" + "github.com/joomcode/errorx" "github.com/bitcoin-sv/go-paymail" "github.com/bitcoin-sv/spv-wallet/config" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" "gorm.io/gorm" @@ -31,17 +31,17 @@ func NewService(paymails PaymailRepo, users UsersService, cfg *config.AppConfig) // Create creates a new paymail attached to a user func (s *Service) Create(ctx context.Context, newPaymail *paymailsmodels.NewPaymail) (*paymailsmodels.Paymail, error) { if err := s.config.Paymail.CheckDomain(newPaymail.Domain); err != nil { - return nil, spverrors.Wrapf(err, "invalid domain during paymail creation") + return nil, errorx.Decorate(err, "invalid domain during paymail creation") } if exists, err := s.usersService.Exists(ctx, newPaymail.UserID); err != nil { - return nil, spverrors.Wrapf(err, "failed to check if user exists") + return nil, errorx.Decorate(err, "paymails service failed to check if user exists") } else if !exists { - return nil, spverrors.Newf("user does not exist") + return nil, paymailerrors.UserDoesntExist.New("user with ID %s does not exist", newPaymail.UserID) } if err := newPaymail.ValidateAvatar(); err != nil { - return nil, spverrors.Wrapf(err, "invalid avatar url during paymail creation") + return nil, errorx.Decorate(err, "invalid avatar url during user creation") } if newPaymail.PublicName == "" { newPaymail.PublicName = newPaymail.Alias @@ -49,29 +49,29 @@ func (s *Service) Create(ctx context.Context, newPaymail *paymailsmodels.NewPaym createdPaymail, err := s.paymailsRepo.Create(ctx, newPaymail) if err != nil { - return nil, spverrors.Wrapf(err, "failed to append paymail") + return nil, errorx.Decorate(err, "paymails service failed to create new paymail for user") } return createdPaymail, nil } // Find returns a paymail by alias and domain func (s *Service) Find(ctx context.Context, alias, domain string) (*paymailsmodels.Paymail, error) { - paymail, err := s.paymailsRepo.Find(ctx, alias, domain) + address, err := s.paymailsRepo.Find(ctx, alias, domain) if err != nil { - return nil, spverrors.Wrapf(err, "failed to get paymail") + return nil, errorx.Decorate(err, "paymails service failed to find paymail by alias and domain") } - return paymail, nil + return address, nil } // HasPaymailAddress checks if the given address belongs to a given User. func (s *Service) HasPaymailAddress(ctx context.Context, userID string, address string) (bool, error) { alias, domain, sanitized := paymail.SanitizePaymail(address) if sanitized == "" { - return false, paymailerrors.ErrInvalidPaymailAddress + return false, paymailerrors.InvalidPaymailAddress.New("invalid paymail address: %s", address) } pm, err := s.paymailsRepo.FindForUser(ctx, alias, domain, userID) if err != nil { - return false, spverrors.ErrInternal.Wrap(err) + return false, errorx.Decorate(err, "paymails service failed to find paymail by alias and domain for user") } return pm != nil, nil @@ -81,9 +81,9 @@ func (s *Service) HasPaymailAddress(ctx context.Context, userID string, address func (s *Service) GetDefaultPaymailAddress(ctx context.Context, xPubID string) (string, error) { pm, err := s.paymailsRepo.GetDefault(ctx, xPubID) if errors.Is(err, gorm.ErrRecordNotFound) { - return "", paymailerrors.ErrNoDefaultPaymailAddress + return "", paymailerrors.NoDefaultPaymailAddress.New("no default paymail address found for xPubID: %s", xPubID) } else if err != nil { - return "", spverrors.ErrInternal.Wrap(err) + return "", errorx.Decorate(err, "paymails service failed to get default paymail address for xPubID: %s", xPubID) } return pm.Alias + "@" + pm.Domain, nil diff --git a/engine/v2/paymails/paymailsmodels/paymail.go b/engine/v2/paymails/paymailsmodels/paymail.go index d5df97a7b..7414aab2a 100644 --- a/engine/v2/paymails/paymailsmodels/paymail.go +++ b/engine/v2/paymails/paymailsmodels/paymail.go @@ -1,10 +1,10 @@ package paymailsmodels import ( + "github.com/bitcoin-sv/spv-wallet/errdef" "net/url" "time" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" ) @@ -41,11 +41,15 @@ func (np *NewPaymail) ValidateAvatar() error { URL, err := url.Parse(np.Avatar) if err != nil { - return paymailerrors.ErrInvalidAvatarURL.Wrap(err) + return paymailerrors.InvalidAvatarURL. + Wrap(err, "avatarURL is not a valid URL: %s", np.Avatar). + WithProperty(errdef.PropPublicHint, "Avatar should be a valid URL") } if URL.Scheme != "http" && URL.Scheme != "https" { - return paymailerrors.ErrInvalidAvatarURL.Wrap(spverrors.Newf("avatarURL should have http(s) scheme")) + return paymailerrors.InvalidAvatarURL. + New("avatar has not valid scheme (http or https): %s", np.Avatar). + WithProperty(errdef.PropPublicHint, "Avatar should have a valid scheme (http or https)") } return nil diff --git a/engine/v2/users/users_service.go b/engine/v2/users/users_service.go index 65f4e460f..b7fb6629f 100644 --- a/engine/v2/users/users_service.go +++ b/engine/v2/users/users_service.go @@ -5,10 +5,10 @@ import ( primitives "github.com/bitcoin-sv/go-sdk/primitives/ec" "github.com/bitcoin-sv/spv-wallet/config" - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/users/usersmodels" "github.com/bitcoin-sv/spv-wallet/models/bsv" "github.com/bitcoin-sv/spv-wallet/models/transaction/bucket" + "github.com/joomcode/errorx" ) // Service is a user domain service @@ -29,10 +29,10 @@ func NewService(users UserRepo, cfg *config.AppConfig) *Service { func (s *Service) Create(ctx context.Context, newUser *usersmodels.NewUser) (*usersmodels.User, error) { if newUser.Paymail != nil { if err := s.config.Paymail.CheckDomain(newUser.Paymail.Domain); err != nil { - return nil, spverrors.Wrapf(err, "invalid domain during user creation") + return nil, errorx.Decorate(err, "invalid domain during user creation") } if err := newUser.Paymail.ValidateAvatar(); err != nil { - return nil, spverrors.Wrapf(err, "invalid avatar url during user creation") + return nil, errorx.Decorate(err, "invalid avatar url during user creation") } if newUser.Paymail.PublicName == "" { newUser.Paymail.PublicName = newUser.Paymail.Alias @@ -40,7 +40,7 @@ func (s *Service) Create(ctx context.Context, newUser *usersmodels.NewUser) (*us } createdUser, err := s.usersRepo.Create(ctx, newUser) if err != nil { - return nil, spverrors.Wrapf(err, "failed to create user") + return nil, errorx.Decorate(err, "users service failed to create new user") } return createdUser, nil } @@ -49,7 +49,7 @@ func (s *Service) Create(ctx context.Context, newUser *usersmodels.NewUser) (*us func (s *Service) Exists(ctx context.Context, userID string) (bool, error) { exists, err := s.usersRepo.Exists(ctx, userID) if err != nil { - return false, spverrors.Wrapf(err, "failed to check if user exists") + return false, errorx.Decorate(err, "users service failed to check if user exists") } return exists, nil } @@ -58,7 +58,7 @@ func (s *Service) Exists(ctx context.Context, userID string) (bool, error) { func (s *Service) GetByID(ctx context.Context, userID string) (*usersmodels.User, error) { user, err := s.usersRepo.Get(ctx, userID) if err != nil { - return nil, spverrors.Wrapf(err, "failed to get user with paymails") + return nil, errorx.Decorate(err, "users service failed to get user by ID") } return user, nil } @@ -67,7 +67,7 @@ func (s *Service) GetByID(ctx context.Context, userID string) (*usersmodels.User func (s *Service) GetIDByPubKey(ctx context.Context, pubKey string) (string, error) { userID, err := s.usersRepo.GetIDByPubKey(ctx, pubKey) if err != nil { - return "", spverrors.Wrapf(err, "Cannot get user") + return "", errorx.Decorate(err, "users service failed to get user ID by public key") } return userID, nil @@ -77,12 +77,12 @@ func (s *Service) GetIDByPubKey(ctx context.Context, pubKey string) (string, err func (s *Service) GetPubKey(ctx context.Context, userID string) (*primitives.PublicKey, error) { user, err := s.usersRepo.Get(ctx, userID) if err != nil { - return nil, spverrors.Wrapf(err, "Cannot get user") + return nil, errorx.Decorate(err, "users service failed to get user by ID") } pubKey, err := user.PubKeyObj() if err != nil { - return nil, spverrors.Wrapf(err, "Cannot get user's public key") + return nil, errorx.Decorate(err, "users service failed to get public key from user") } return pubKey, nil } @@ -91,7 +91,7 @@ func (s *Service) GetPubKey(ctx context.Context, userID string) (*primitives.Pub func (s *Service) GetBalance(ctx context.Context, userID string) (bsv.Satoshis, error) { balance, err := s.usersRepo.GetBalance(ctx, userID, bucket.BSV) if err != nil { - return 0, spverrors.Wrapf(err, "Cannot get user's balance") + return 0, errorx.Decorate(err, "users service failed to get balance for user by ID") } return balance, nil } diff --git a/errdef/traits.go b/errdef/traits.go index 4f3ec6ed3..d5ae62671 100644 --- a/errdef/traits.go +++ b/errdef/traits.go @@ -4,6 +4,7 @@ import "github.com/joomcode/errorx" var TraitConfig = errorx.RegisterTrait("config") var TraitIllegalArgument = errorx.RegisterTrait("illegal_argument") +var TraitNotFound = errorx.RegisterTrait("not_found") var TraitAuth = errorx.RegisterTrait("auth") var TraitARC = errorx.RegisterTrait("arc") var TraitShouldNeverHappen = errorx.RegisterTrait("should_never_happen") From 65d856ec3d544a2fa90131dd3eb54f2e39510a68 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 07:16:12 +0100 Subject: [PATCH 3/9] feat(SPV-1583): Detailed and Map --- actions/v2/admin/errors/errors.go | 24 ------- actions/v2/admin/internal/mapping/paymail.go | 10 ++- actions/v2/admin/users/create.go | 30 +++++---- actions/v2/admin/users/create_test.go | 8 +-- actions/v2/admin/users/paymail_add.go | 25 ++++---- actions/v2/admin/users/paymail_add_test.go | 8 +-- .../mock_paymail_address_service.go | 2 +- errdef/clienterr/builder.go | 18 +++--- errdef/clienterr/client_error_definition.go | 4 +- errdef/clienterr/mapper.go | 62 +++++++++++++++++++ errdef/problem_details.go | 2 +- 11 files changed, 117 insertions(+), 76 deletions(-) delete mode 100644 actions/v2/admin/errors/errors.go create mode 100644 errdef/clienterr/mapper.go diff --git a/actions/v2/admin/errors/errors.go b/actions/v2/admin/errors/errors.go deleted file mode 100644 index c74d8db86..000000000 --- a/actions/v2/admin/errors/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package errors - -import "github.com/bitcoin-sv/spv-wallet/models" - -// ErrInvalidPublicKey is returned when the public key provided by admin is invalid -var ErrInvalidPublicKey = models.SPVError{Message: "invalid public key", StatusCode: 400, Code: "error-user-invalid-pubkey"} - -// ErrCreatingUser is returned when the user creation fails -var ErrCreatingUser = models.SPVError{Message: "error creating user", StatusCode: 500, Code: "error-user-creating"} - -// ErrInvalidPaymail is returned when the paymail is invalid -var ErrInvalidPaymail = models.SPVError{Message: "invalid paymail", StatusCode: 400, Code: "error-user-invalid-paymail"} - -// ErrInvalidAvatarURL is returned when the avatar url is invalid -var ErrInvalidAvatarURL = models.SPVError{Message: "invalid avatar url", StatusCode: 422, Code: "error-user-invalid-avatar-url"} - -// ErrAddingPaymail is returned when the paymail addition fails -var ErrAddingPaymail = models.SPVError{Message: "error adding paymail", StatusCode: 500, Code: "error-user-adding-paymail"} - -// ErrPaymailInconsistent is returned when both paymail address and alias + domain are provided and they are inconsistent -var ErrPaymailInconsistent = models.SPVError{Message: "inconsistent paymail address and alias/domain", StatusCode: 400, Code: "error-user-inconsistent-paymail"} - -// ErrInvalidDomain is when the domain is wrong -var ErrInvalidDomain = models.SPVError{Message: "invalid domain", StatusCode: 400, Code: "error-invalid-domain"} diff --git a/actions/v2/admin/internal/mapping/paymail.go b/actions/v2/admin/internal/mapping/paymail.go index 1cecb7ef4..fbc259ec5 100644 --- a/actions/v2/admin/internal/mapping/paymail.go +++ b/actions/v2/admin/internal/mapping/paymail.go @@ -84,14 +84,20 @@ func parsePaymail(r *api.RequestsAddPaymail) (string, string, error) { if request.HasAddress() && (request.HasAlias() || request.HasDomain()) && !request.AddressEqualsTo(request.Alias+"@"+request.Domain) { - return "", "", clienterr.BadRequest.New().WithDetailf("Alias and Domain are inconsistent with Address").Err() + return "", "", clienterr.BadRequest. + Detailed( + "inconsistent_alias_domain_and_address", + "Inconsistent alias@domain and address fields: %s, %s, %s. Hint: use either alias and domain or address (not both)", + request.Alias, request.Domain, request.Address, + ).Err() } if !request.HasAddress() { request.Address = request.Alias + "@" + request.Domain } alias, domain, sanitized := paymail.SanitizePaymail(request.Address) if sanitized == "" { - return "", "", clienterr.BadRequest.New().WithDetailf("Invalid paymail address: %s", request.Address).Err() + return "", "", clienterr.BadRequest. + Detailed("invalid_paymail_address", "Invalid paymail address: %s", request.Address).Err() } return alias, domain, nil } diff --git a/actions/v2/admin/users/create.go b/actions/v2/admin/users/create.go index b05bc7b7e..292ed1ce8 100644 --- a/actions/v2/admin/users/create.go +++ b/actions/v2/admin/users/create.go @@ -2,7 +2,6 @@ package users import ( "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" - "github.com/joomcode/errorx" "net/http" primitives "github.com/bitcoin-sv/go-sdk/primitives/ec" @@ -19,9 +18,7 @@ func (s *APIAdminUsers) CreateUser(c *gin.Context) { if err := c.Bind(&request); err != nil { // TODO: Bind does AbortWithError internally, so we should not call Response, I guess - clienterr.UnprocessableEntity. - Wrap(err, "cannot bind request"). - Response(c, s.logger) + clienterr.UnprocessableEntity.New().Wrap(err).Response(c, s.logger) return } @@ -37,18 +34,18 @@ func (s *APIAdminUsers) CreateUser(c *gin.Context) { } createdUser, err := s.engine.UsersService().Create(c, newUser) + if err != nil { - if errorx.IsOfType(err, configerrors.UnsupportedDomain) { - clienterr.BadRequest. - Wrap(err, "Unsupported domain"). - Response(c, s.logger) - } else if errorx.IsOfType(err, paymailerrors.InvalidAvatarURL) { - clienterr.UnprocessableEntity. - Wrap(err, "Invalid avatar url"). - Response(c, s.logger) - } else { - clienterr.Response(c, err, s.logger) - } + clienterr.Map(err). + IfOfType(configerrors.UnsupportedDomain). + Then( + clienterr.BadRequest.Detailed("unsupported_domain", "Unsupported domain: '%s'", newUser.Paymail.Domain), + ). + IfOfType(paymailerrors.InvalidAvatarURL). + Then( + clienterr.UnprocessableEntity.Detailed("invalid_avatar_url", "Invalid avatar URL: '%s'", newUser.Paymail.Avatar), + ). + Response(c, s.logger) return } @@ -59,7 +56,8 @@ func validatePubKey(pubKey string) error { _, err := primitives.PublicKeyFromString(pubKey) if err != nil { return clienterr.BadRequest. - Wrap(err, "Cannot parse public key: '%s'", pubKey).Err() + Detailed("invalid_public_key", "Invalid public key: '%s'", pubKey). + Wrap(err).Err() } return nil } diff --git a/actions/v2/admin/users/create_test.go b/actions/v2/admin/users/create_test.go index 146ea6d24..124bc03f7 100644 --- a/actions/v2/admin/users/create_test.go +++ b/actions/v2/admin/users/create_test.go @@ -130,7 +130,7 @@ func TestCreateUserWithBadURLAvatar(t *testing.T) { // then: then.Response(res). - WithProblemDetails(422, "unprocessable_entity", "Invalid avatar url") + WithProblemDetails(422, "invalid_avatar_url", "Invalid avatar URL") } @@ -439,7 +439,7 @@ func TestAddUserWithWrongPaymailDomain(t *testing.T) { // then: then.Response(res). - WithProblemDetails(400, "bad_request", "Unsupported domain") + WithProblemDetails(400, "unsupported_domain", "Unsupported domain") }) t.Run("Try to add using alias and domain as address", func(t *testing.T) { @@ -458,7 +458,7 @@ func TestAddUserWithWrongPaymailDomain(t *testing.T) { // then: then.Response(res). - WithProblemDetails(400, "bad_request", "Unsupported domain") + WithProblemDetails(400, "unsupported_domain", "Unsupported domain") }) } @@ -482,5 +482,5 @@ func TestTryToAddWithWrongPubKey(t *testing.T) { // then: then.Response(res). - WithProblemDetails(400, "bad_request", "Cannot parse public key") + WithProblemDetails(400, "invalid_public_key", "Invalid public key") } diff --git a/actions/v2/admin/users/paymail_add.go b/actions/v2/admin/users/paymail_add.go index 608458239..ea055a4b8 100644 --- a/actions/v2/admin/users/paymail_add.go +++ b/actions/v2/admin/users/paymail_add.go @@ -2,7 +2,6 @@ package users import ( "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" - "github.com/joomcode/errorx" "net/http" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" @@ -16,7 +15,7 @@ import ( func (s *APIAdminUsers) AddPaymailToUser(c *gin.Context, id string) { var request api.RequestsAddPaymail if err := c.Bind(&request); err != nil { - clienterr.UnprocessableEntity.Wrap(err, "cannot bind request").Response(c, s.logger) + clienterr.UnprocessableEntity.New().Wrap(err).Response(c, s.logger) return } @@ -27,18 +26,18 @@ func (s *APIAdminUsers) AddPaymailToUser(c *gin.Context, id string) { } createdPaymail, err := s.engine.PaymailsService().Create(c, newPaymail) + if err != nil { - if errorx.IsOfType(err, configerrors.UnsupportedDomain) { - clienterr.BadRequest. - Wrap(err, "Unsupported domain"). - Response(c, s.logger) - } else if errorx.IsOfType(err, paymailerrors.InvalidAvatarURL) { - clienterr.UnprocessableEntity. - Wrap(err, "Invalid avatar url"). - Response(c, s.logger) - } else { - clienterr.Response(c, err, s.logger) - } + clienterr.Map(err). + IfOfType(configerrors.UnsupportedDomain). + Then( + clienterr.BadRequest.Detailed("unsupported_domain", "Unsupported domain: '%s'", newPaymail.Domain), + ). + IfOfType(paymailerrors.InvalidAvatarURL). + Then( + clienterr.UnprocessableEntity.Detailed("invalid_avatar_url", "Invalid avatar URL: '%s'", newPaymail.Avatar), + ). + Response(c, s.logger) return } diff --git a/actions/v2/admin/users/paymail_add_test.go b/actions/v2/admin/users/paymail_add_test.go index bd14e46d4..f8e740087 100644 --- a/actions/v2/admin/users/paymail_add_test.go +++ b/actions/v2/admin/users/paymail_add_test.go @@ -79,7 +79,7 @@ func TestAddPaymail(t *testing.T) { // then: then.Response(res). - WithProblemDetails(422, "unprocessable_entity", "Invalid avatar url") + WithProblemDetails(422, "invalid_avatar_url", "Invalid avatar URL") }) t.Run("Add a paymail to a user as admin using alias and domain as address", func(t *testing.T) { @@ -203,7 +203,7 @@ func TestAddPaymailWithWrongDomain(t *testing.T) { // then: then.Response(res). - WithProblemDetails(400, "bad_request", "Unsupported domain") + WithProblemDetails(400, "unsupported_domain", "Unsupported domain") }) t.Run("Try to add using alias and domain as address", func(t *testing.T) { @@ -220,7 +220,7 @@ func TestAddPaymailWithWrongDomain(t *testing.T) { // then: then.Response(res). - WithProblemDetails(400, "bad_request", "Unsupported domain") + WithProblemDetails(400, "unsupported_domain", "Unsupported domain") }) } @@ -282,6 +282,6 @@ func TestAddPaymailWithBothPaymailAndAliasDomainPair(t *testing.T) { // then: then.Response(res). HasStatus(400). - WithProblemDetails(400, "bad_request", "inconsistent") + WithProblemDetails(400, "inconsistent_alias_domain_and_address", "Inconsistent alias@domain and address fields") }) } diff --git a/engine/v2/transaction/outlines/testabilities/mock_paymail_address_service.go b/engine/v2/transaction/outlines/testabilities/mock_paymail_address_service.go index 26604600e..5f4476242 100644 --- a/engine/v2/transaction/outlines/testabilities/mock_paymail_address_service.go +++ b/engine/v2/transaction/outlines/testabilities/mock_paymail_address_service.go @@ -36,5 +36,5 @@ func (m *mockPaymailAddressService) GetDefaultPaymailAddress(_ context.Context, return user.DefaultPaymail().Address(), nil } } - return "", paymailerrors.ErrNoDefaultPaymailAddress + return "", paymailerrors.NoDefaultPaymailAddress.NewWithNoMessage() } diff --git a/errdef/clienterr/builder.go b/errdef/clienterr/builder.go index d01a0a8fa..4c5d1d9f7 100644 --- a/errdef/clienterr/builder.go +++ b/errdef/clienterr/builder.go @@ -19,19 +19,24 @@ type Builder struct { cause error } -func (b *Builder) Wrap(cause error, msg string, args ...any) *Builder { +func (b *Builder) Wrap(cause error) *Builder { b.cause = cause b.problemDetails. - FromInternalError(cause). - PushDetail(fmt.Sprintf(msg, args...)) + FromInternalError(cause) return b } +func (b *Builder) Detailed(errType string, detail string, args ...any) *Builder { + b.problemDetails.Type = errType + b.problemDetails.PushDetail(fmt.Sprintf(detail, args...)) + return b +} + func (b *Builder) Err() *errorx.Error { var err *errorx.Error if b.cause != nil { - err = clientError.Wrap(b.cause, "") + err = clientError.WrapWithNoMessage(b.cause) } else { err = clientError.NewWithNoMessage() } @@ -42,11 +47,6 @@ func (b *Builder) Response(c *gin.Context, log *zerolog.Logger) { Response(c, b.Err(), log) } -func (b *Builder) WithDetailf(detail string, args ...any) *Builder { - b.problemDetails.PushDetail(fmt.Sprintf(detail, args...)) - return b -} - func (b *Builder) WithInstance(parts ...any) *Builder { var sb strings.Builder for i, p := range parts { diff --git a/errdef/clienterr/client_error_definition.go b/errdef/clienterr/client_error_definition.go index 0f73efcc6..9b36d04de 100644 --- a/errdef/clienterr/client_error_definition.go +++ b/errdef/clienterr/client_error_definition.go @@ -6,8 +6,8 @@ type ClientErrorDefinition struct { httpCode int } -func (c ClientErrorDefinition) Wrap(cause error, msg string, args ...any) *Builder { - return c.New().Wrap(cause, msg, args...) +func (c ClientErrorDefinition) Detailed(errType string, detail string, args ...any) *Builder { + return c.New().Detailed(errType, detail, args...) } func (c ClientErrorDefinition) New() *Builder { diff --git a/errdef/clienterr/mapper.go b/errdef/clienterr/mapper.go new file mode 100644 index 000000000..224ccbf80 --- /dev/null +++ b/errdef/clienterr/mapper.go @@ -0,0 +1,62 @@ +package clienterr + +import ( + "github.com/gin-gonic/gin" + "github.com/joomcode/errorx" + "github.com/rs/zerolog" +) + +type Mapper interface { + IfOfType(typeToMatch *errorx.Type) OnMatch + Finalize() error + Response(c *gin.Context, log *zerolog.Logger) +} + +// OnMatch provides a fluent API for defining the response to return when a match is found. +type OnMatch interface { + Then(errToReturn *Builder) Mapper +} + +// Map creates a new Mapper instance. +func Map(err error) Mapper { + return &MapperBuilder{ + baseErr: err, + } +} + +type MapperBuilder struct { + baseErr error + matching bool + final *Builder +} + +func (r *MapperBuilder) IfOfType(typeToMatch *errorx.Type) OnMatch { + if r.final != nil { + return r + } + r.matching = false + if errorx.IsOfType(r.baseErr, typeToMatch) { + r.matching = true + } + return r +} + +func (r *MapperBuilder) Then(errToReturn *Builder) Mapper { + if r.final == nil && r.matching { + r.final = errToReturn + } + return r +} + +func (r *MapperBuilder) Finalize() error { + if r.final == nil { + return r.baseErr + } + + return r.final.Wrap(r.baseErr).Err() +} + +func (r *MapperBuilder) Response(c *gin.Context, log *zerolog.Logger) { + err := r.Finalize() + Response(c, err, log) +} diff --git a/errdef/problem_details.go b/errdef/problem_details.go index c2005efdd..b9936750c 100644 --- a/errdef/problem_details.go +++ b/errdef/problem_details.go @@ -20,7 +20,7 @@ func (p *ProblemDetails) PushDetail(detail string) *ProblemDetails { if p.Detail != "" { separator = "; " } - p.Detail = fmt.Sprintf("%v%s%v", detail, separator, p.Detail) + p.Detail = fmt.Sprintf("%v%s%v", p.Detail, separator, detail) return p } From 249032f914a2b71037a0b483b147646bc28c71af Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 07:38:11 +0100 Subject: [PATCH 4/9] feat(SPV-1583): linter --- actions/v2/admin/users/create.go | 2 +- actions/v2/admin/users/get.go | 2 +- actions/v2/admin/users/paymail_add.go | 2 +- config/errors/errors.go | 1 + config/utils.go | 2 +- engine/v2/database/errors/errors.go | 1 + engine/v2/database/repository/paymails.go | 2 +- engine/v2/database/repository/users.go | 2 +- engine/v2/paymails/paymailerrors/paymail_errors.go | 4 +--- engine/v2/paymails/paymails_service.go | 2 +- engine/v2/paymails/paymailsmodels/paymail.go | 2 +- errdef/clienterr/builder.go | 8 +++++++- errdef/clienterr/client_error_definition.go | 3 +++ errdef/clienterr/client_errors.go | 7 +------ errdef/clienterr/http_error.go | 7 ++++--- errdef/clienterr/mapper.go | 13 +++++++------ errdef/problem_details.go | 4 ++++ errdef/properties.go | 1 + errdef/traits.go | 1 + 19 files changed, 39 insertions(+), 27 deletions(-) diff --git a/actions/v2/admin/users/create.go b/actions/v2/admin/users/create.go index 292ed1ce8..57ca876a7 100644 --- a/actions/v2/admin/users/create.go +++ b/actions/v2/admin/users/create.go @@ -1,7 +1,6 @@ package users import ( - "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "net/http" primitives "github.com/bitcoin-sv/go-sdk/primitives/ec" @@ -9,6 +8,7 @@ import ( "github.com/bitcoin-sv/spv-wallet/api" configerrors "github.com/bitcoin-sv/spv-wallet/config/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "github.com/gin-gonic/gin" ) diff --git a/actions/v2/admin/users/get.go b/actions/v2/admin/users/get.go index c9f469016..f694ec9ba 100644 --- a/actions/v2/admin/users/get.go +++ b/actions/v2/admin/users/get.go @@ -1,10 +1,10 @@ package users import ( - "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "net/http" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "github.com/gin-gonic/gin" ) diff --git a/actions/v2/admin/users/paymail_add.go b/actions/v2/admin/users/paymail_add.go index ea055a4b8..dd4f7dfd8 100644 --- a/actions/v2/admin/users/paymail_add.go +++ b/actions/v2/admin/users/paymail_add.go @@ -1,13 +1,13 @@ package users import ( - "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "net/http" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" "github.com/bitcoin-sv/spv-wallet/api" configerrors "github.com/bitcoin-sv/spv-wallet/config/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" + "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "github.com/gin-gonic/gin" ) diff --git a/config/errors/errors.go b/config/errors/errors.go index e215ed3f9..334669dc8 100644 --- a/config/errors/errors.go +++ b/config/errors/errors.go @@ -1,3 +1,4 @@ +//nolint:revive // Error types should be self-explanatory package errors import ( diff --git a/config/utils.go b/config/utils.go index 30703c45e..6303dc206 100644 --- a/config/utils.go +++ b/config/utils.go @@ -2,10 +2,10 @@ package config import ( "fmt" - "github.com/bitcoin-sv/spv-wallet/errdef" "slices" configerrors "github.com/bitcoin-sv/spv-wallet/config/errors" + "github.com/bitcoin-sv/spv-wallet/errdef" ) // CheckDomain will check if the domain is allowed diff --git a/engine/v2/database/errors/errors.go b/engine/v2/database/errors/errors.go index 76b991a3b..ed28b1beb 100644 --- a/engine/v2/database/errors/errors.go +++ b/engine/v2/database/errors/errors.go @@ -1,3 +1,4 @@ +//nolint:revive // Error types should be self-explanatory package dberrors import ( diff --git a/engine/v2/database/repository/paymails.go b/engine/v2/database/repository/paymails.go index 1e88cf6b6..e30245aef 100644 --- a/engine/v2/database/repository/paymails.go +++ b/engine/v2/database/repository/paymails.go @@ -3,9 +3,9 @@ package repository import ( "context" "errors" - dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/database" + dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" "gorm.io/gorm" ) diff --git a/engine/v2/database/repository/users.go b/engine/v2/database/repository/users.go index f8f47d501..9015fb17e 100644 --- a/engine/v2/database/repository/users.go +++ b/engine/v2/database/repository/users.go @@ -2,9 +2,9 @@ package repository import ( "context" - dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/database" + dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" "github.com/bitcoin-sv/spv-wallet/engine/v2/users/usersmodels" "github.com/bitcoin-sv/spv-wallet/models/bsv" diff --git a/engine/v2/paymails/paymailerrors/paymail_errors.go b/engine/v2/paymails/paymailerrors/paymail_errors.go index 6737f55cb..f216dfd05 100644 --- a/engine/v2/paymails/paymailerrors/paymail_errors.go +++ b/engine/v2/paymails/paymailerrors/paymail_errors.go @@ -1,3 +1,4 @@ +//nolint:revive // Error types should be self-explanatory package paymailerrors import ( @@ -8,9 +9,6 @@ import ( var Namespace = errorx.NewNamespace("paymail") var InvalidAvatarURL = Namespace.NewType("invalid_avatar_url", errdef.TraitIllegalArgument) - var InvalidPaymailAddress = Namespace.NewType("invalid_paymail_address", errdef.TraitIllegalArgument) - var UserDoesntExist = Namespace.NewType("user_doesnt_exist", errdef.TraitNotFound) - var NoDefaultPaymailAddress = Namespace.NewType("no_default_paymail_address", errdef.TraitIllegalArgument) diff --git a/engine/v2/paymails/paymails_service.go b/engine/v2/paymails/paymails_service.go index 7828c6264..9b45f931d 100644 --- a/engine/v2/paymails/paymails_service.go +++ b/engine/v2/paymails/paymails_service.go @@ -3,12 +3,12 @@ package paymails import ( "context" "errors" - "github.com/joomcode/errorx" "github.com/bitcoin-sv/go-paymail" "github.com/bitcoin-sv/spv-wallet/config" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailsmodels" + "github.com/joomcode/errorx" "gorm.io/gorm" ) diff --git a/engine/v2/paymails/paymailsmodels/paymail.go b/engine/v2/paymails/paymailsmodels/paymail.go index 7414aab2a..5190fe540 100644 --- a/engine/v2/paymails/paymailsmodels/paymail.go +++ b/engine/v2/paymails/paymailsmodels/paymail.go @@ -1,11 +1,11 @@ package paymailsmodels import ( - "github.com/bitcoin-sv/spv-wallet/errdef" "net/url" "time" "github.com/bitcoin-sv/spv-wallet/engine/v2/paymails/paymailerrors" + "github.com/bitcoin-sv/spv-wallet/errdef" ) // Paymail represents a domain model from paymails service diff --git a/errdef/clienterr/builder.go b/errdef/clienterr/builder.go index 4c5d1d9f7..19655f3d4 100644 --- a/errdef/clienterr/builder.go +++ b/errdef/clienterr/builder.go @@ -2,9 +2,9 @@ package clienterr import ( "fmt" - "github.com/bitcoin-sv/spv-wallet/errdef" "strings" + "github.com/bitcoin-sv/spv-wallet/errdef" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/rs/zerolog" @@ -13,12 +13,14 @@ import ( var propProblemDetails = errorx.RegisterProperty("problem_details") var clientError = errorx.NewNamespace("client").NewType("error") +// Builder is a fluent API for building client (4xx) errors. type Builder struct { from *ClientErrorDefinition problemDetails errdef.ProblemDetails cause error } +// Wrap wraps the provided error as the cause of the client error. func (b *Builder) Wrap(cause error) *Builder { b.cause = cause b.problemDetails. @@ -27,12 +29,14 @@ func (b *Builder) Wrap(cause error) *Builder { return b } +// Detailed changes the error type and adds a detail message. func (b *Builder) Detailed(errType string, detail string, args ...any) *Builder { b.problemDetails.Type = errType b.problemDetails.PushDetail(fmt.Sprintf(detail, args...)) return b } +// Err returns the client error as an errorx.Error (which also implements error interface). func (b *Builder) Err() *errorx.Error { var err *errorx.Error if b.cause != nil { @@ -43,10 +47,12 @@ func (b *Builder) Err() *errorx.Error { return err.WithProperty(propProblemDetails, b.problemDetails) } +// Response sends the client error as a JSON response to the client. func (b *Builder) Response(c *gin.Context, log *zerolog.Logger) { Response(c, b.Err(), log) } +// WithInstance sets the instance field of the problem details. func (b *Builder) WithInstance(parts ...any) *Builder { var sb strings.Builder for i, p := range parts { diff --git a/errdef/clienterr/client_error_definition.go b/errdef/clienterr/client_error_definition.go index 9b36d04de..6124ba73a 100644 --- a/errdef/clienterr/client_error_definition.go +++ b/errdef/clienterr/client_error_definition.go @@ -1,15 +1,18 @@ package clienterr +// ClientErrorDefinition is a definition of a client error. type ClientErrorDefinition struct { title string typeName string httpCode int } +// Detailed is a shortcut for creating a new client error with a detailed message. func (c ClientErrorDefinition) Detailed(errType string, detail string, args ...any) *Builder { return c.New().Detailed(errType, detail, args...) } +// New creates a new client error builder. func (c ClientErrorDefinition) New() *Builder { b := &Builder{from: &c} b.problemDetails.Status = c.httpCode diff --git a/errdef/clienterr/client_errors.go b/errdef/clienterr/client_errors.go index 8df437c69..d530e8073 100644 --- a/errdef/clienterr/client_errors.go +++ b/errdef/clienterr/client_errors.go @@ -1,3 +1,4 @@ +//nolint:revive // Error types should be self-explanatory package clienterr var BadRequest = ClientErrorDefinition{ @@ -11,9 +12,3 @@ var UnprocessableEntity = ClientErrorDefinition{ typeName: "unprocessable_entity", httpCode: 422, } - -var Unauthorized = ClientErrorDefinition{ - title: "Unauthorized", - typeName: "unauthorized", - httpCode: 401, -} diff --git a/errdef/clienterr/http_error.go b/errdef/clienterr/http_error.go index 6fc16f7ac..933998d6f 100644 --- a/errdef/clienterr/http_error.go +++ b/errdef/clienterr/http_error.go @@ -2,13 +2,14 @@ package clienterr import ( "errors" - errdef2 "github.com/bitcoin-sv/spv-wallet/errdef" + errdef2 "github.com/bitcoin-sv/spv-wallet/errdef" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/rs/zerolog" ) +// Response sends the error as a JSON response to the client. func Response(c *gin.Context, err error, log *zerolog.Logger) { problem, logLevel := problemDetailsFromError(err) log.WithLevel(logLevel).Err(err).Msgf("Error HTTP response, returning %d: %s", problem.Status, problem.Detail) @@ -44,10 +45,10 @@ func problemDetailsFromError(err error) (problem errdef2.ProblemDetails, level z level = zerolog.ErrorLevel problem = errdef2.ProblemDetails{ - Type: "unknown_error", + Type: "internal", Title: "Unknown error", Status: 500, - Instance: "unknown_error", + Instance: "", } return } diff --git a/errdef/clienterr/mapper.go b/errdef/clienterr/mapper.go index 224ccbf80..3e0cab2a7 100644 --- a/errdef/clienterr/mapper.go +++ b/errdef/clienterr/mapper.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" ) +// Mapper is a fluent API for mapping errors to client errors. type Mapper interface { IfOfType(typeToMatch *errorx.Type) OnMatch Finalize() error @@ -19,18 +20,18 @@ type OnMatch interface { // Map creates a new Mapper instance. func Map(err error) Mapper { - return &MapperBuilder{ + return &mapperBuilder{ baseErr: err, } } -type MapperBuilder struct { +type mapperBuilder struct { baseErr error matching bool final *Builder } -func (r *MapperBuilder) IfOfType(typeToMatch *errorx.Type) OnMatch { +func (r *mapperBuilder) IfOfType(typeToMatch *errorx.Type) OnMatch { if r.final != nil { return r } @@ -41,14 +42,14 @@ func (r *MapperBuilder) IfOfType(typeToMatch *errorx.Type) OnMatch { return r } -func (r *MapperBuilder) Then(errToReturn *Builder) Mapper { +func (r *mapperBuilder) Then(errToReturn *Builder) Mapper { if r.final == nil && r.matching { r.final = errToReturn } return r } -func (r *MapperBuilder) Finalize() error { +func (r *mapperBuilder) Finalize() error { if r.final == nil { return r.baseErr } @@ -56,7 +57,7 @@ func (r *MapperBuilder) Finalize() error { return r.final.Wrap(r.baseErr).Err() } -func (r *MapperBuilder) Response(c *gin.Context, log *zerolog.Logger) { +func (r *mapperBuilder) Response(c *gin.Context, log *zerolog.Logger) { err := r.Finalize() Response(c, err, log) } diff --git a/errdef/problem_details.go b/errdef/problem_details.go index b9936750c..1589d6eb4 100644 --- a/errdef/problem_details.go +++ b/errdef/problem_details.go @@ -7,6 +7,8 @@ import ( "github.com/joomcode/errorx" ) +// ProblemDetails is a struct that represents a problem details object as defined in RFC 7807. +// https://datatracker.ietf.org/doc/html/rfc7807 type ProblemDetails struct { Type string `json:"type"` Title string `json:"title"` @@ -15,6 +17,7 @@ type ProblemDetails struct { Instance string `json:"instance"` } +// PushDetail appends a detail to the existing details, separated by a semicolon. func (p *ProblemDetails) PushDetail(detail string) *ProblemDetails { separator := "" if p.Detail != "" { @@ -24,6 +27,7 @@ func (p *ProblemDetails) PushDetail(detail string) *ProblemDetails { return p } +// FromInternalError maps an internal error to a ProblemDetails object. func (p *ProblemDetails) FromInternalError(err error) *ProblemDetails { ex := errorx.Cast(err) if ex == nil { diff --git a/errdef/properties.go b/errdef/properties.go index 16a9e4036..992052414 100644 --- a/errdef/properties.go +++ b/errdef/properties.go @@ -1,3 +1,4 @@ +//nolint:revive // Properties should be self-explanatory package errdef import "github.com/joomcode/errorx" diff --git a/errdef/traits.go b/errdef/traits.go index d5ae62671..eccdb903b 100644 --- a/errdef/traits.go +++ b/errdef/traits.go @@ -1,3 +1,4 @@ +//nolint:revive // Traits should be self-explanatory package errdef import "github.com/joomcode/errorx" From c6d741dcf6af66196cbe6fb2f829d3adfcf8855d Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 09:40:20 +0100 Subject: [PATCH 5/9] feat(SPV-1583): logging error & stack trace --- cmd/main.go | 1 - errdef/logging_test.go | 61 +++++++++++++++++++++++++ logging/error_marshal.go | 13 ------ logging/logging.go | 97 ++++++++++++++++++++++++++-------------- 4 files changed, 124 insertions(+), 48 deletions(-) create mode 100644 errdef/logging_test.go delete mode 100644 logging/error_marshal.go diff --git a/cmd/main.go b/cmd/main.go index 264ab3bbb..49f608303 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -31,7 +31,6 @@ var version = "development" // @in header // @name authorization func main() { - logging.SetupGlobalZerologErrorHandler() defaultLogger := logging.GetDefaultLogger() // Load the Application Configuration diff --git a/errdef/logging_test.go b/errdef/logging_test.go new file mode 100644 index 000000000..9996ff9c1 --- /dev/null +++ b/errdef/logging_test.go @@ -0,0 +1,61 @@ +package errdef_test + +import ( + "strconv" + "strings" + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/tester/jsonrequire" + "github.com/bitcoin-sv/spv-wallet/logging" + "github.com/joomcode/errorx" + "github.com/rs/zerolog" +) + +func TestLoggingError(t *testing.T) { + // given: + writer := &StringWriter{} + logger := logging.CreateLogger(writer, "spv-wallet-default", zerolog.DebugLevel, true) + + // when: + err := a() + logger.Info().Stack().Err(err).Msg("test-message") + + // then: + logMsg := writer.builder.String() + + jsonrequire.Match(t, `{ + "log.level": "info", + "ecs.version": {{ anything }}, + "application": "spv-wallet-default", + "error.message": "a-wrap, cause: b-wrap, cause: common.internal_error: conversion error, cause: strconv.Atoi: parsing \"a\": invalid syntax", + "@timestamp": "{{ matchTimestamp }}", + "log.origin": {{ anything }}, + "message": "test-message", + "error.stack_trace": {{ anything }} + }`, map[string]any{ + "matchStackTrace": `/.+/`, // regex to check that it's not empty + }, logMsg) + + t.Log(logMsg) +} + +func a() error { + return errorx.Decorate(b(), "a-wrap") +} + +func b() error { + return errorx.Decorate(c(), "b-wrap") +} + +func c() error { + _, err := strconv.Atoi("a") + return errorx.InternalError.Wrap(err, "conversion error") +} + +type StringWriter struct { + builder strings.Builder +} + +func (w *StringWriter) Write(p []byte) (n int, err error) { + return w.builder.Write(p) +} diff --git a/logging/error_marshal.go b/logging/error_marshal.go deleted file mode 100644 index 2e13b6d31..000000000 --- a/logging/error_marshal.go +++ /dev/null @@ -1,13 +0,0 @@ -package logging - -import ( - "github.com/bitcoin-sv/spv-wallet/engine/spverrors" - "github.com/rs/zerolog" -) - -// SetupGlobalZerologErrorHandler setup the ErrorMarshalFunc to print detailed error info -func SetupGlobalZerologErrorHandler() { - zerolog.ErrorMarshalFunc = func(err error) any { - return spverrors.UnfoldError(err) - } -} diff --git a/logging/logging.go b/logging/logging.go index 8fac4b54c..a99f304ef 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -1,12 +1,15 @@ package logging import ( + "fmt" "io" "os" + "strings" "time" "github.com/bitcoin-sv/spv-wallet/config" "github.com/bitcoin-sv/spv-wallet/engine/spverrors" + "github.com/joomcode/errorx" "github.com/rs/zerolog" "go.elastic.co/ecszerolog" ) @@ -18,55 +21,81 @@ const ( // GetDefaultLogger create and configure default zerolog logger. It should be used before config is loaded func GetDefaultLogger() zerolog.Logger { - logger, err := createLogger("spv-wallet-default", jsonLogFormat, "debug", true) - if err != nil { - panic(err) - } - return logger + return CreateLogger(writerFor(jsonLogFormat), "spv-wallet-default", zerolog.DebugLevel, true) } // CreateLoggerWithConfig creates a logger based on the given config func CreateLoggerWithConfig(config *config.AppConfig) (zerolog.Logger, error) { loggingConfig := config.Logging - return createLogger(loggingConfig.InstanceName, loggingConfig.Format, loggingConfig.Level, loggingConfig.LogOrigin) + parsedLevel, err := zerolog.ParseLevel(loggingConfig.Level) + if err != nil { + return zerolog.Nop(), spverrors.Wrapf(err, "failed to parse log level") + } + + return CreateLogger(writerFor(loggingConfig.Format), loggingConfig.InstanceName, parsedLevel, loggingConfig.LogOrigin), nil } -func createLogger(instanceName, format, level string, logOrigin bool) (zerolog.Logger, error) { - var writer io.Writer - if format == consoleLogFormat { - writer = zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: "2006-01-02 15:04:05.000", - } - } else { - writer = os.Stdout +// CreateLogger creates a logger with the given writer, instance name and log level +func CreateLogger(writer io.Writer, instanceName string, level zerolog.Level, logOrigin bool) zerolog.Logger { + options := []ecszerolog.Option{ + ecszerolog.Level(level), } - parsedLevel, err := zerolog.ParseLevel(level) - if err != nil { - err = spverrors.Wrapf(err, "failed to parse log level") - return zerolog.Nop(), err + if logOrigin { + options = append(options, ecszerolog.Origin()) } - logLevel := ecszerolog.Level(parsedLevel) - origin := ecszerolog.Origin() - var logger zerolog.Logger + logger := ecszerolog.New(writer, options...). + With(). + Str("application", instanceName). + Logger() - if logOrigin { - logger = ecszerolog.New(writer, logLevel, origin). - With(). - Str("application", instanceName). - Logger() - } else { - logger = ecszerolog.New(writer, logLevel). - With(). - Str("application", instanceName). - Logger() - } + // NOTE: zerolog.New() overwrites global handlers, so we need to set them AFTER creating the logger + setGlobalHandlers() + return logger +} + +func setGlobalHandlers() { zerolog.TimestampFunc = func() time.Time { return time.Now().In(time.Local) //nolint:gosmopolitan // We want local time inside logger. } - return logger, nil + zerolog.ErrorMarshalFunc = func(err error) any { + if errorx.Cast(err) != nil { + return fmt.Sprintf("%v", err) + } + return spverrors.UnfoldError(err) + } + + zerolog.ErrorStackMarshaler = func(err error) any { + if errorx.Cast(err) != nil { + fullMessage := fmt.Sprintf("%+v", err) + const startingNewLine = "\n " + const stackTraceMarker = startingNewLine + "at " + stackTraceStart := strings.Index(fullMessage, stackTraceMarker) + if stackTraceStart == -1 { + return nil + } + return formatStackTrace(fullMessage[stackTraceStart+len(startingNewLine):]) + } + return nil + } +} + +func writerFor(format string) io.Writer { + switch format { + case consoleLogFormat: + return zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: "2006-01-02 15:04:05.000", + } + default: + return os.Stdout + } +} + +func formatStackTrace(stackMsg string) []string { + stackMsg = strings.ReplaceAll(stackMsg, "\n\t", " ") + return strings.Split(stackMsg, "\n ") } From fafd4c3012889aa58b50e646d0129ebd177b3392 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:25:45 +0100 Subject: [PATCH 6/9] feat(SPV-1583): ProblemDetails defined in oapi --- actions/v2/admin/users/get.go | 5 +- actions/v2/admin/users/get_test.go | 71 ++++++ api/components/errors.yaml | 25 ++ api/components/responses.yaml | 7 + api/endpoints/admin.yaml | 22 +- api/gen.api.yaml | 154 +++--------- api/gen.models.go | 255 ++------------------ api/manualtests/client/client.gen.go | 319 +++---------------------- engine/tester/jsonrequire/funcs.go | 3 + engine/v2/database/errors/errors.go | 3 + engine/v2/database/errors/helpers.go | 16 ++ engine/v2/database/repository/users.go | 2 +- errdef/clienterr/client_errors.go | 6 + errdef/clienterr/http_error.go | 19 +- errdef/problem_details.go | 7 +- 15 files changed, 236 insertions(+), 678 deletions(-) create mode 100644 actions/v2/admin/users/get_test.go create mode 100644 engine/v2/database/errors/helpers.go diff --git a/actions/v2/admin/users/get.go b/actions/v2/admin/users/get.go index f694ec9ba..81c3cd5a0 100644 --- a/actions/v2/admin/users/get.go +++ b/actions/v2/admin/users/get.go @@ -1,6 +1,7 @@ package users import ( + dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "net/http" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" @@ -12,7 +13,9 @@ import ( func (s *APIAdminUsers) UserById(c *gin.Context, id string) { user, err := s.engine.UsersService().GetByID(c, id) if err != nil { - clienterr.Response(c, err, s.logger) + clienterr.Map(err). + IfOfType(dberrors.NotFound).Then(clienterr.NotFound.New()). + Response(c, s.logger) return } diff --git a/actions/v2/admin/users/get_test.go b/actions/v2/admin/users/get_test.go new file mode 100644 index 000000000..630734adf --- /dev/null +++ b/actions/v2/admin/users/get_test.go @@ -0,0 +1,71 @@ +package users_test + +import ( + "github.com/bitcoin-sv/spv-wallet/actions/testabilities" + testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" + "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" + "testing" +) + +func TestGetUser(t *testing.T) { + // given: + given, then := testabilities.New(t) + cleanup := given.StartedSPVWalletWithConfiguration(testengine.WithV2()) + defer cleanup() + + // and: + client := given.HttpClient().ForAdmin() + + // when: + resp, _ := client. + R(). + SetPathParam("id", fixtures.Sender.ID()). + Get("/api/v2/admin/users/{id}") + + // then: + then.Response(resp). + HasStatus(200). + WithJSONMatching(`{ + "id": "{{ .id }}", + "createdAt": "{{ matchTimestamp }}", + "updatedAt": "{{ matchTimestamp }}", + "publicKey": "{{ .publicKey }}", + "paymails": [ + { + "alias": "{{ .alias }}", + "avatar": "", + "domain": "{{ .domain }}", + "id": 3, + "paymail": "{{ .paymail }}", + "publicName": "{{ .publicName }}" + } + ] + }`, map[string]any{ + "id": fixtures.Sender.ID(), + "publicKey": fixtures.Sender.PublicKey().ToDERHex(), + "paymail": fixtures.Sender.DefaultPaymail(), + "publicName": fixtures.Sender.DefaultPaymail().PublicName(), + "alias": fixtures.Sender.DefaultPaymail().Alias(), + "domain": fixtures.Sender.DefaultPaymail().Domain(), + }) +} + +func TestTryGetNonExistingUser(t *testing.T) { + // given: + given, then := testabilities.New(t) + cleanup := given.StartedSPVWalletWithConfiguration(testengine.WithV2()) + defer cleanup() + + // and: + client := given.HttpClient().ForAdmin() + + // when: + resp, _ := client. + R(). + SetPathParam("id", "non-existing-id"). + Get("/api/v2/admin/users/{id}") + + // then: + then.Response(resp). + WithProblemDetails(404, "not_found") +} diff --git a/api/components/errors.yaml b/api/components/errors.yaml index 34629c96f..67061ce59 100644 --- a/api/components/errors.yaml +++ b/api/components/errors.yaml @@ -5,6 +5,31 @@ info: components: schemas: + ProblemDetails: + type: object + required: + - type + - title + - status + - detail + - instance + properties: + type: + type: string + description: A URI reference that identifies the problem type + title: + type: string + description: A short, human-readable summary of the problem type + status: + type: integer + description: The HTTP status code + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem + instance: + type: string + description: A URI reference that identifies the specific occurrence of the problem + Schema: type: object properties: diff --git a/api/components/responses.yaml b/api/components/responses.yaml index a3f7ca711..9a8de6b04 100644 --- a/api/components/responses.yaml +++ b/api/components/responses.yaml @@ -173,6 +173,13 @@ components: schema: $ref: "./models.yaml#/components/schemas/RecordedOutline" + ProblemDetails: + description: RFC 7807 Problem Details. For 4xx codes, you can switch by 'type' field to get more specific error. + content: + application/json: + schema: + $ref: "./errors.yaml#/components/schemas/ProblemDetails" + RecordTransactionBadRequest: description: Bad request is an error that occurs when the request is malformed. content: diff --git a/api/endpoints/admin.yaml b/api/endpoints/admin.yaml index a3251054c..657082ecb 100644 --- a/api/endpoints/admin.yaml +++ b/api/endpoints/admin.yaml @@ -40,14 +40,8 @@ paths: responses: 201: $ref: "../components/responses.yaml#/components/responses/AdminCreateUserSuccess" - 400: - $ref: "../components/responses.yaml#/components/responses/AdminUserBadRequest" - 401: - $ref: "../components/responses.yaml#/components/responses/NotAuthorizedToAdminEndpoint" - 422: - $ref: "../components/responses.yaml#/components/responses/AdminInvalidAvatarURL" - 500: - $ref: "../components/responses.yaml#/components/responses/AdminCreateUserInternalServerError" + default: + $ref: "../components/responses.yaml#/components/responses/ProblemDetails" /api/v2/admin/users/{id}: get: @@ -70,8 +64,8 @@ paths: responses: 200: $ref: "../components/responses.yaml#/components/responses/AdminGetUser" - 500: - $ref: "../components/responses.yaml#/components/responses/AdminGetUserInternalServerError" + default: + $ref: "../components/responses.yaml#/components/responses/ProblemDetails" /api/v2/admin/users/{id}/paymails: post: @@ -100,9 +94,5 @@ paths: responses: 201: $ref: "../components/responses.yaml#/components/responses/AdminAddPaymailSuccess" - 400: - $ref: "../components/responses.yaml#/components/responses/AdminUserBadRequest" - 401: - $ref: "../components/responses.yaml#/components/responses/NotAuthorizedToAdminEndpoint" - 422: - $ref: "../components/responses.yaml#/components/responses/AdminInvalidAvatarURL" + default: + $ref: "../components/responses.yaml#/components/responses/ProblemDetails" diff --git a/api/gen.api.yaml b/api/gen.api.yaml index 8638f3273..f122c125a 100644 --- a/api/gen.api.yaml +++ b/api/gen.api.yaml @@ -34,14 +34,8 @@ paths: responses: "201": $ref: '#/components/responses/responses_AdminCreateUserSuccess' - "400": - $ref: '#/components/responses/responses_AdminUserBadRequest' - "401": - $ref: '#/components/responses/responses_NotAuthorizedToAdminEndpoint' - "422": - $ref: '#/components/responses/responses_AdminInvalidAvatarURL' - "500": - $ref: '#/components/responses/responses_AdminCreateUserInternalServerError' + default: + $ref: '#/components/responses/responses_ProblemDetails' security: - XPubAuth: - admin @@ -62,8 +56,8 @@ paths: responses: "200": $ref: '#/components/responses/responses_AdminGetUser' - "500": - $ref: '#/components/responses/responses_AdminGetUserInternalServerError' + default: + $ref: '#/components/responses/responses_ProblemDetails' security: - XPubAuth: - admin @@ -90,12 +84,8 @@ paths: responses: "201": $ref: '#/components/responses/responses_AdminAddPaymailSuccess' - "400": - $ref: '#/components/responses/responses_AdminUserBadRequest' - "401": - $ref: '#/components/responses/responses_NotAuthorizedToAdminEndpoint' - "422": - $ref: '#/components/responses/responses_AdminInvalidAvatarURL' + default: + $ref: '#/components/responses/responses_ProblemDetails' security: - XPubAuth: - admin @@ -288,12 +278,6 @@ components: schema: $ref: '#/components/schemas/models_Paymail' description: Paymail added to user - responses_AdminCreateUserInternalServerError: - content: - application/json: - schema: - $ref: '#/components/schemas/errors_CreatingUser' - description: Internal error while creating user responses_AdminCreateUserSuccess: content: application/json: @@ -306,30 +290,6 @@ components: schema: $ref: '#/components/schemas/models_User' description: User found - responses_AdminGetUserInternalServerError: - content: - application/json: - schema: - $ref: '#/components/schemas/errors_GettingUser' - description: Internal error while getting user - responses_AdminInvalidAvatarURL: - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/errors_InvalidAvatarURL' - description: Unprocessable entity is an error that occurs when the request cannot be fulfilled. - responses_AdminUserBadRequest: - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/errors_CannotBindRequest' - - $ref: '#/components/schemas/errors_InvalidPubKey' - - $ref: '#/components/schemas/errors_InvalidPaymail' - - $ref: '#/components/schemas/errors_PaymailInconsistent' - - $ref: '#/components/schemas/errors_InvalidDomain' - description: Bad request is an error that occurs when the request is malformed. responses_CreateTransactionOutlineBadRequest: content: application/json: @@ -392,6 +352,12 @@ components: schema: $ref: '#/components/schemas/errors_AdminAuthorization' description: Security requirements failed + responses_ProblemDetails: + content: + application/json: + schema: + $ref: '#/components/schemas/errors_ProblemDetails' + description: RFC 7807 Problem Details. For 4xx codes, you can switch by 'type' field to get more specific error. responses_RecordTransactionBadRequest: content: application/json: @@ -493,24 +459,6 @@ components: message: example: xpub authorization required type: object - errors_CannotBindRequest: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-bind-body-invalid - message: - example: cannot bind request body - type: object - errors_CreatingUser: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-user-creating - message: - example: error creating user - type: object errors_DataNotFound: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -529,15 +477,6 @@ components: message: example: failed to get outputs type: object - errors_GettingUser: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-unknown - message: - example: Internal server error - type: object errors_Internal: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -547,15 +486,6 @@ components: message: example: internal server error type: object - errors_InvalidAvatarURL: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-user-invalid-avatar-url - message: - example: invalid avatar url - type: object errors_InvalidDataID: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -565,33 +495,6 @@ components: message: example: invalid data id type: object - errors_InvalidDomain: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-invalid-domain - message: - example: invalid domain - type: object - errors_InvalidPaymail: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-user-invalid-paymail - message: - example: invalid paymail - type: object - errors_InvalidPubKey: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-user-invalid-pubkey - message: - example: invalid public key - type: object errors_NoOperations: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -601,15 +504,30 @@ components: message: example: no operations to save type: object - errors_PaymailInconsistent: - allOf: - - $ref: '#/components/schemas/errors_Schema' - - properties: - code: - example: error-user-inconsistent-paymail - message: - example: inconsistent paymail address and alias/domain - type: object + errors_ProblemDetails: + properties: + detail: + description: A human-readable explanation specific to this occurrence of the problem + type: string + instance: + description: A URI reference that identifies the specific occurrence of the problem + type: string + status: + description: The HTTP status code + type: integer + title: + description: A short, human-readable summary of the problem type + type: string + type: + description: A URI reference that identifies the problem type + type: string + required: + - type + - title + - status + - detail + - instance + type: object errors_Schema: additionalProperties: false properties: diff --git a/api/gen.models.go b/api/gen.models.go index 1a550f342..670573b8c 100644 --- a/api/gen.models.go +++ b/api/gen.models.go @@ -119,18 +119,6 @@ type ErrorsAuthXPubRequired struct { Message interface{} `json:"message"` } -// ErrorsCannotBindRequest defines model for errors_CannotBindRequest. -type ErrorsCannotBindRequest struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - -// ErrorsCreatingUser defines model for errors_CreatingUser. -type ErrorsCreatingUser struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - // ErrorsDataNotFound defines model for errors_DataNotFound. type ErrorsDataNotFound struct { Code interface{} `json:"code"` @@ -143,58 +131,40 @@ type ErrorsGettingOutputs struct { Message interface{} `json:"message"` } -// ErrorsGettingUser defines model for errors_GettingUser. -type ErrorsGettingUser struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - // ErrorsInternal defines model for errors_Internal. type ErrorsInternal struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } -// ErrorsInvalidAvatarURL defines model for errors_InvalidAvatarURL. -type ErrorsInvalidAvatarURL struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - // ErrorsInvalidDataID defines model for errors_InvalidDataID. type ErrorsInvalidDataID struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } -// ErrorsInvalidDomain defines model for errors_InvalidDomain. -type ErrorsInvalidDomain struct { +// ErrorsNoOperations defines model for errors_NoOperations. +type ErrorsNoOperations struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } -// ErrorsInvalidPaymail defines model for errors_InvalidPaymail. -type ErrorsInvalidPaymail struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} +// ErrorsProblemDetails defines model for errors_ProblemDetails. +type ErrorsProblemDetails struct { + // Detail A human-readable explanation specific to this occurrence of the problem + Detail string `json:"detail"` -// ErrorsInvalidPubKey defines model for errors_InvalidPubKey. -type ErrorsInvalidPubKey struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} + // Instance A URI reference that identifies the specific occurrence of the problem + Instance string `json:"instance"` -// ErrorsNoOperations defines model for errors_NoOperations. -type ErrorsNoOperations struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} + // Status The HTTP status code + Status int `json:"status"` -// ErrorsPaymailInconsistent defines model for errors_PaymailInconsistent. -type ErrorsPaymailInconsistent struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` + // Title A short, human-readable summary of the problem type + Title string `json:"title"` + + // Type A URI reference that identifies the problem type + Type string `json:"type"` } // ErrorsSchema defines model for errors_Schema. @@ -589,28 +559,12 @@ type RequestsSortBy = string // ResponsesAdminAddPaymailSuccess defines model for responses_AdminAddPaymailSuccess. type ResponsesAdminAddPaymailSuccess = ModelsPaymail -// ResponsesAdminCreateUserInternalServerError defines model for responses_AdminCreateUserInternalServerError. -type ResponsesAdminCreateUserInternalServerError = ErrorsCreatingUser - // ResponsesAdminCreateUserSuccess defines model for responses_AdminCreateUserSuccess. type ResponsesAdminCreateUserSuccess = ModelsUser // ResponsesAdminGetUser defines model for responses_AdminGetUser. type ResponsesAdminGetUser = ModelsUser -// ResponsesAdminGetUserInternalServerError defines model for responses_AdminGetUserInternalServerError. -type ResponsesAdminGetUserInternalServerError = ErrorsGettingUser - -// ResponsesAdminInvalidAvatarURL defines model for responses_AdminInvalidAvatarURL. -type ResponsesAdminInvalidAvatarURL struct { - union json.RawMessage -} - -// ResponsesAdminUserBadRequest defines model for responses_AdminUserBadRequest. -type ResponsesAdminUserBadRequest struct { - union json.RawMessage -} - // ResponsesCreateTransactionOutlineBadRequest defines model for responses_CreateTransactionOutlineBadRequest. type ResponsesCreateTransactionOutlineBadRequest struct { union json.RawMessage @@ -644,6 +598,9 @@ type ResponsesNotAuthorized = ErrorsAnyAuthorization // ResponsesNotAuthorizedToAdminEndpoint defines model for responses_NotAuthorizedToAdminEndpoint. type ResponsesNotAuthorizedToAdminEndpoint = ErrorsAdminAuthorization +// ResponsesProblemDetails defines model for responses_ProblemDetails. +type ResponsesProblemDetails = ErrorsProblemDetails + // ResponsesRecordTransactionBadRequest defines model for responses_RecordTransactionBadRequest. type ResponsesRecordTransactionBadRequest struct { union json.RawMessage @@ -1133,182 +1090,6 @@ func (t *RequestsTransactionOutlineOutputSpecification) UnmarshalJSON(b []byte) return err } -// AsErrorsInvalidAvatarURL returns the union data inside the ResponsesAdminInvalidAvatarURL as a ErrorsInvalidAvatarURL -func (t ResponsesAdminInvalidAvatarURL) AsErrorsInvalidAvatarURL() (ErrorsInvalidAvatarURL, error) { - var body ErrorsInvalidAvatarURL - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidAvatarURL overwrites any union data inside the ResponsesAdminInvalidAvatarURL as the provided ErrorsInvalidAvatarURL -func (t *ResponsesAdminInvalidAvatarURL) FromErrorsInvalidAvatarURL(v ErrorsInvalidAvatarURL) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidAvatarURL performs a merge with any union data inside the ResponsesAdminInvalidAvatarURL, using the provided ErrorsInvalidAvatarURL -func (t *ResponsesAdminInvalidAvatarURL) MergeErrorsInvalidAvatarURL(v ErrorsInvalidAvatarURL) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ResponsesAdminInvalidAvatarURL) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ResponsesAdminInvalidAvatarURL) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsErrorsCannotBindRequest returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsCannotBindRequest -func (t ResponsesAdminUserBadRequest) AsErrorsCannotBindRequest() (ErrorsCannotBindRequest, error) { - var body ErrorsCannotBindRequest - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsCannotBindRequest overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsCannotBindRequest -func (t *ResponsesAdminUserBadRequest) FromErrorsCannotBindRequest(v ErrorsCannotBindRequest) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsCannotBindRequest performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsCannotBindRequest -func (t *ResponsesAdminUserBadRequest) MergeErrorsCannotBindRequest(v ErrorsCannotBindRequest) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsInvalidPubKey returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsInvalidPubKey -func (t ResponsesAdminUserBadRequest) AsErrorsInvalidPubKey() (ErrorsInvalidPubKey, error) { - var body ErrorsInvalidPubKey - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidPubKey overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsInvalidPubKey -func (t *ResponsesAdminUserBadRequest) FromErrorsInvalidPubKey(v ErrorsInvalidPubKey) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidPubKey performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsInvalidPubKey -func (t *ResponsesAdminUserBadRequest) MergeErrorsInvalidPubKey(v ErrorsInvalidPubKey) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsInvalidPaymail returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsInvalidPaymail -func (t ResponsesAdminUserBadRequest) AsErrorsInvalidPaymail() (ErrorsInvalidPaymail, error) { - var body ErrorsInvalidPaymail - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidPaymail overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsInvalidPaymail -func (t *ResponsesAdminUserBadRequest) FromErrorsInvalidPaymail(v ErrorsInvalidPaymail) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidPaymail performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsInvalidPaymail -func (t *ResponsesAdminUserBadRequest) MergeErrorsInvalidPaymail(v ErrorsInvalidPaymail) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsPaymailInconsistent returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsPaymailInconsistent -func (t ResponsesAdminUserBadRequest) AsErrorsPaymailInconsistent() (ErrorsPaymailInconsistent, error) { - var body ErrorsPaymailInconsistent - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsPaymailInconsistent overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsPaymailInconsistent -func (t *ResponsesAdminUserBadRequest) FromErrorsPaymailInconsistent(v ErrorsPaymailInconsistent) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsPaymailInconsistent performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsPaymailInconsistent -func (t *ResponsesAdminUserBadRequest) MergeErrorsPaymailInconsistent(v ErrorsPaymailInconsistent) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsInvalidDomain returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsInvalidDomain -func (t ResponsesAdminUserBadRequest) AsErrorsInvalidDomain() (ErrorsInvalidDomain, error) { - var body ErrorsInvalidDomain - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidDomain overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsInvalidDomain -func (t *ResponsesAdminUserBadRequest) FromErrorsInvalidDomain(v ErrorsInvalidDomain) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidDomain performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsInvalidDomain -func (t *ResponsesAdminUserBadRequest) MergeErrorsInvalidDomain(v ErrorsInvalidDomain) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ResponsesAdminUserBadRequest) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ResponsesAdminUserBadRequest) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - // AsErrorsTxSpecNoDefaultPaymailAddress returns the union data inside the ResponsesCreateTransactionOutlineBadRequest as a ErrorsTxSpecNoDefaultPaymailAddress func (t ResponsesCreateTransactionOutlineBadRequest) AsErrorsTxSpecNoDefaultPaymailAddress() (ErrorsTxSpecNoDefaultPaymailAddress, error) { var body ErrorsTxSpecNoDefaultPaymailAddress diff --git a/api/manualtests/client/client.gen.go b/api/manualtests/client/client.gen.go index f7555a5f2..cbdab240d 100644 --- a/api/manualtests/client/client.gen.go +++ b/api/manualtests/client/client.gen.go @@ -126,18 +126,6 @@ type ErrorsAuthXPubRequired struct { Message interface{} `json:"message"` } -// ErrorsCannotBindRequest defines model for errors_CannotBindRequest. -type ErrorsCannotBindRequest struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - -// ErrorsCreatingUser defines model for errors_CreatingUser. -type ErrorsCreatingUser struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - // ErrorsDataNotFound defines model for errors_DataNotFound. type ErrorsDataNotFound struct { Code interface{} `json:"code"` @@ -150,58 +138,40 @@ type ErrorsGettingOutputs struct { Message interface{} `json:"message"` } -// ErrorsGettingUser defines model for errors_GettingUser. -type ErrorsGettingUser struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - // ErrorsInternal defines model for errors_Internal. type ErrorsInternal struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } -// ErrorsInvalidAvatarURL defines model for errors_InvalidAvatarURL. -type ErrorsInvalidAvatarURL struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} - // ErrorsInvalidDataID defines model for errors_InvalidDataID. type ErrorsInvalidDataID struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } -// ErrorsInvalidDomain defines model for errors_InvalidDomain. -type ErrorsInvalidDomain struct { +// ErrorsNoOperations defines model for errors_NoOperations. +type ErrorsNoOperations struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } -// ErrorsInvalidPaymail defines model for errors_InvalidPaymail. -type ErrorsInvalidPaymail struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} +// ErrorsProblemDetails defines model for errors_ProblemDetails. +type ErrorsProblemDetails struct { + // Detail A human-readable explanation specific to this occurrence of the problem + Detail string `json:"detail"` -// ErrorsInvalidPubKey defines model for errors_InvalidPubKey. -type ErrorsInvalidPubKey struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} + // Instance A URI reference that identifies the specific occurrence of the problem + Instance string `json:"instance"` -// ErrorsNoOperations defines model for errors_NoOperations. -type ErrorsNoOperations struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` -} + // Status The HTTP status code + Status int `json:"status"` -// ErrorsPaymailInconsistent defines model for errors_PaymailInconsistent. -type ErrorsPaymailInconsistent struct { - Code interface{} `json:"code"` - Message interface{} `json:"message"` + // Title A short, human-readable summary of the problem type + Title string `json:"title"` + + // Type A URI reference that identifies the problem type + Type string `json:"type"` } // ErrorsSchema defines model for errors_Schema. @@ -596,28 +566,12 @@ type RequestsSortBy = string // ResponsesAdminAddPaymailSuccess defines model for responses_AdminAddPaymailSuccess. type ResponsesAdminAddPaymailSuccess = ModelsPaymail -// ResponsesAdminCreateUserInternalServerError defines model for responses_AdminCreateUserInternalServerError. -type ResponsesAdminCreateUserInternalServerError = ErrorsCreatingUser - // ResponsesAdminCreateUserSuccess defines model for responses_AdminCreateUserSuccess. type ResponsesAdminCreateUserSuccess = ModelsUser // ResponsesAdminGetUser defines model for responses_AdminGetUser. type ResponsesAdminGetUser = ModelsUser -// ResponsesAdminGetUserInternalServerError defines model for responses_AdminGetUserInternalServerError. -type ResponsesAdminGetUserInternalServerError = ErrorsGettingUser - -// ResponsesAdminInvalidAvatarURL defines model for responses_AdminInvalidAvatarURL. -type ResponsesAdminInvalidAvatarURL struct { - union json.RawMessage -} - -// ResponsesAdminUserBadRequest defines model for responses_AdminUserBadRequest. -type ResponsesAdminUserBadRequest struct { - union json.RawMessage -} - // ResponsesCreateTransactionOutlineBadRequest defines model for responses_CreateTransactionOutlineBadRequest. type ResponsesCreateTransactionOutlineBadRequest struct { union json.RawMessage @@ -651,6 +605,9 @@ type ResponsesNotAuthorized = ErrorsAnyAuthorization // ResponsesNotAuthorizedToAdminEndpoint defines model for responses_NotAuthorizedToAdminEndpoint. type ResponsesNotAuthorizedToAdminEndpoint = ErrorsAdminAuthorization +// ResponsesProblemDetails defines model for responses_ProblemDetails. +type ResponsesProblemDetails = ErrorsProblemDetails + // ResponsesRecordTransactionBadRequest defines model for responses_RecordTransactionBadRequest. type ResponsesRecordTransactionBadRequest struct { union json.RawMessage @@ -1140,182 +1097,6 @@ func (t *RequestsTransactionOutlineOutputSpecification) UnmarshalJSON(b []byte) return err } -// AsErrorsInvalidAvatarURL returns the union data inside the ResponsesAdminInvalidAvatarURL as a ErrorsInvalidAvatarURL -func (t ResponsesAdminInvalidAvatarURL) AsErrorsInvalidAvatarURL() (ErrorsInvalidAvatarURL, error) { - var body ErrorsInvalidAvatarURL - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidAvatarURL overwrites any union data inside the ResponsesAdminInvalidAvatarURL as the provided ErrorsInvalidAvatarURL -func (t *ResponsesAdminInvalidAvatarURL) FromErrorsInvalidAvatarURL(v ErrorsInvalidAvatarURL) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidAvatarURL performs a merge with any union data inside the ResponsesAdminInvalidAvatarURL, using the provided ErrorsInvalidAvatarURL -func (t *ResponsesAdminInvalidAvatarURL) MergeErrorsInvalidAvatarURL(v ErrorsInvalidAvatarURL) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ResponsesAdminInvalidAvatarURL) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ResponsesAdminInvalidAvatarURL) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsErrorsCannotBindRequest returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsCannotBindRequest -func (t ResponsesAdminUserBadRequest) AsErrorsCannotBindRequest() (ErrorsCannotBindRequest, error) { - var body ErrorsCannotBindRequest - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsCannotBindRequest overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsCannotBindRequest -func (t *ResponsesAdminUserBadRequest) FromErrorsCannotBindRequest(v ErrorsCannotBindRequest) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsCannotBindRequest performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsCannotBindRequest -func (t *ResponsesAdminUserBadRequest) MergeErrorsCannotBindRequest(v ErrorsCannotBindRequest) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsInvalidPubKey returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsInvalidPubKey -func (t ResponsesAdminUserBadRequest) AsErrorsInvalidPubKey() (ErrorsInvalidPubKey, error) { - var body ErrorsInvalidPubKey - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidPubKey overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsInvalidPubKey -func (t *ResponsesAdminUserBadRequest) FromErrorsInvalidPubKey(v ErrorsInvalidPubKey) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidPubKey performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsInvalidPubKey -func (t *ResponsesAdminUserBadRequest) MergeErrorsInvalidPubKey(v ErrorsInvalidPubKey) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsInvalidPaymail returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsInvalidPaymail -func (t ResponsesAdminUserBadRequest) AsErrorsInvalidPaymail() (ErrorsInvalidPaymail, error) { - var body ErrorsInvalidPaymail - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidPaymail overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsInvalidPaymail -func (t *ResponsesAdminUserBadRequest) FromErrorsInvalidPaymail(v ErrorsInvalidPaymail) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidPaymail performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsInvalidPaymail -func (t *ResponsesAdminUserBadRequest) MergeErrorsInvalidPaymail(v ErrorsInvalidPaymail) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsPaymailInconsistent returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsPaymailInconsistent -func (t ResponsesAdminUserBadRequest) AsErrorsPaymailInconsistent() (ErrorsPaymailInconsistent, error) { - var body ErrorsPaymailInconsistent - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsPaymailInconsistent overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsPaymailInconsistent -func (t *ResponsesAdminUserBadRequest) FromErrorsPaymailInconsistent(v ErrorsPaymailInconsistent) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsPaymailInconsistent performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsPaymailInconsistent -func (t *ResponsesAdminUserBadRequest) MergeErrorsPaymailInconsistent(v ErrorsPaymailInconsistent) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsErrorsInvalidDomain returns the union data inside the ResponsesAdminUserBadRequest as a ErrorsInvalidDomain -func (t ResponsesAdminUserBadRequest) AsErrorsInvalidDomain() (ErrorsInvalidDomain, error) { - var body ErrorsInvalidDomain - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromErrorsInvalidDomain overwrites any union data inside the ResponsesAdminUserBadRequest as the provided ErrorsInvalidDomain -func (t *ResponsesAdminUserBadRequest) FromErrorsInvalidDomain(v ErrorsInvalidDomain) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeErrorsInvalidDomain performs a merge with any union data inside the ResponsesAdminUserBadRequest, using the provided ErrorsInvalidDomain -func (t *ResponsesAdminUserBadRequest) MergeErrorsInvalidDomain(v ErrorsInvalidDomain) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t ResponsesAdminUserBadRequest) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *ResponsesAdminUserBadRequest) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - // AsErrorsTxSpecNoDefaultPaymailAddress returns the union data inside the ResponsesCreateTransactionOutlineBadRequest as a ErrorsTxSpecNoDefaultPaymailAddress func (t ResponsesCreateTransactionOutlineBadRequest) AsErrorsTxSpecNoDefaultPaymailAddress() (ErrorsTxSpecNoDefaultPaymailAddress, error) { var body ErrorsTxSpecNoDefaultPaymailAddress @@ -2615,10 +2396,7 @@ type CreateUserResponse struct { Body []byte HTTPResponse *http.Response JSON201 *ResponsesAdminCreateUserSuccess - JSON400 *ResponsesAdminUserBadRequest - JSON401 *ResponsesNotAuthorizedToAdminEndpoint - JSON422 *ResponsesAdminInvalidAvatarURL - JSON500 *ResponsesAdminCreateUserInternalServerError + JSONDefault *ResponsesProblemDetails } // Status returns HTTPResponse.Status @@ -2651,7 +2429,7 @@ type UserByIdResponse struct { Body []byte HTTPResponse *http.Response JSON200 *ResponsesAdminGetUser - JSON500 *ResponsesAdminGetUserInternalServerError + JSONDefault *ResponsesProblemDetails } // Status returns HTTPResponse.Status @@ -2684,9 +2462,7 @@ type AddPaymailToUserResponse struct { Body []byte HTTPResponse *http.Response JSON201 *ResponsesAdminAddPaymailSuccess - JSON400 *ResponsesAdminUserBadRequest - JSON401 *ResponsesNotAuthorizedToAdminEndpoint - JSON422 *ResponsesAdminInvalidAvatarURL + JSONDefault *ResponsesProblemDetails } // Status returns HTTPResponse.Status @@ -3093,33 +2869,12 @@ func ParseCreateUserResponse(rsp *http.Response) (*CreateUserResponse, error) { } response.JSON201 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest ResponsesAdminUserBadRequest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest ResponsesProblemDetails if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest ResponsesNotAuthorizedToAdminEndpoint - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest ResponsesAdminInvalidAvatarURL - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest ResponsesAdminCreateUserInternalServerError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest + response.JSONDefault = &dest } @@ -3147,12 +2902,12 @@ func ParseUserByIdResponse(rsp *http.Response) (*UserByIdResponse, error) { } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest ResponsesAdminGetUserInternalServerError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest ResponsesProblemDetails if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSONDefault = &dest } @@ -3180,26 +2935,12 @@ func ParseAddPaymailToUserResponse(rsp *http.Response) (*AddPaymailToUserRespons } response.JSON201 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest ResponsesAdminUserBadRequest - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest ResponsesNotAuthorizedToAdminEndpoint + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest ResponsesProblemDetails if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest ResponsesAdminInvalidAvatarURL - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON422 = &dest + response.JSONDefault = &dest } diff --git a/engine/tester/jsonrequire/funcs.go b/engine/tester/jsonrequire/funcs.go index 8a9016d33..297b06197 100644 --- a/engine/tester/jsonrequire/funcs.go +++ b/engine/tester/jsonrequire/funcs.go @@ -78,6 +78,9 @@ func matchNumber() string { } func containsAll(parts []string) string { + if len(parts) == 0 { + return "*" + } partsRegex := strings.Builder{} partsRegex.WriteString("^") for _, part := range parts { diff --git a/engine/v2/database/errors/errors.go b/engine/v2/database/errors/errors.go index ed28b1beb..0dad5bd90 100644 --- a/engine/v2/database/errors/errors.go +++ b/engine/v2/database/errors/errors.go @@ -2,9 +2,12 @@ package dberrors import ( + "github.com/bitcoin-sv/spv-wallet/errdef" "github.com/joomcode/errorx" ) var Namespace = errorx.NewNamespace("db") var QueryFailed = Namespace.NewType("query_failed") + +var NotFound = Namespace.NewType("not_found", errdef.TraitNotFound) diff --git a/engine/v2/database/errors/helpers.go b/engine/v2/database/errors/helpers.go new file mode 100644 index 000000000..0a3a6f48f --- /dev/null +++ b/engine/v2/database/errors/helpers.go @@ -0,0 +1,16 @@ +package dberrors + +import ( + "errors" + "github.com/joomcode/errorx" + "gorm.io/gorm" +) + +// QueryOrNotFoundError wraps the error with QueryFailed or NotFound error type depending on the GORM error type. +func QueryOrNotFoundError(err error, message string, args ...any) *errorx.Error { + if errors.Is(err, gorm.ErrRecordNotFound) { + return NotFound.Wrap(err, message, args...) + } else { + return QueryFailed.Wrap(err, message, args...) + } +} diff --git a/engine/v2/database/repository/users.go b/engine/v2/database/repository/users.go index 9015fb17e..32c4e919e 100644 --- a/engine/v2/database/repository/users.go +++ b/engine/v2/database/repository/users.go @@ -58,7 +58,7 @@ func (u *Users) Get(ctx context.Context, userID string) (*usersmodels.User, erro Where("id = ?", userID). First(&user).Error if err != nil { - return nil, dberrors.QueryFailed.Wrap(err, "failed to get user by ID") + return nil, dberrors.QueryOrNotFoundError(err, "failed to get user by ID") } return mapToDomainUser(&user), nil diff --git a/errdef/clienterr/client_errors.go b/errdef/clienterr/client_errors.go index d530e8073..d2859cc8f 100644 --- a/errdef/clienterr/client_errors.go +++ b/errdef/clienterr/client_errors.go @@ -12,3 +12,9 @@ var UnprocessableEntity = ClientErrorDefinition{ typeName: "unprocessable_entity", httpCode: 422, } + +var NotFound = ClientErrorDefinition{ + title: "Not found", + typeName: "not_found", + httpCode: 404, +} diff --git a/errdef/clienterr/http_error.go b/errdef/clienterr/http_error.go index 933998d6f..32d8e2edc 100644 --- a/errdef/clienterr/http_error.go +++ b/errdef/clienterr/http_error.go @@ -3,7 +3,7 @@ package clienterr import ( "errors" - errdef2 "github.com/bitcoin-sv/spv-wallet/errdef" + "github.com/bitcoin-sv/spv-wallet/errdef" "github.com/gin-gonic/gin" "github.com/joomcode/errorx" "github.com/rs/zerolog" @@ -16,11 +16,11 @@ func Response(c *gin.Context, err error, log *zerolog.Logger) { c.JSON(problem.Status, problem) } -func problemDetailsFromError(err error) (problem errdef2.ProblemDetails, level zerolog.Level) { +func problemDetailsFromError(err error) (problem errdef.ProblemDetails, level zerolog.Level) { var ex *errorx.Error if errors.As(err, &ex) { if details, ok := ex.Property(propProblemDetails); ok { - problem = details.(errdef2.ProblemDetails) + problem = details.(errdef.ProblemDetails) level = zerolog.InfoLevel return } @@ -29,12 +29,12 @@ func problemDetailsFromError(err error) (problem errdef2.ProblemDetails, level z level = zerolog.WarnLevel problem.Type = "internal" problem.FromInternalError(ex) - if errorx.HasTrait(ex, errdef2.TraitUnsupported) { + if errorx.HasTrait(ex, errdef.TraitUnsupported) { problem.Title = "Unsupported operation" problem.Status = 501 return } - if errorx.HasTrait(ex, errdef2.TraitShouldNeverHappen) { + if errorx.HasTrait(ex, errdef.TraitShouldNeverHappen) { problem.Detail = "This should never happen" } @@ -44,11 +44,8 @@ func problemDetailsFromError(err error) (problem errdef2.ProblemDetails, level z } level = zerolog.ErrorLevel - problem = errdef2.ProblemDetails{ - Type: "internal", - Title: "Unknown error", - Status: 500, - Instance: "", - } + problem.Title = "Unknown error" + problem.Status = 500 + problem.Type = "internal" return } diff --git a/errdef/problem_details.go b/errdef/problem_details.go index 1589d6eb4..51ec8c18c 100644 --- a/errdef/problem_details.go +++ b/errdef/problem_details.go @@ -2,6 +2,7 @@ package errdef import ( "fmt" + "github.com/bitcoin-sv/spv-wallet/api" "strings" "github.com/joomcode/errorx" @@ -10,11 +11,7 @@ import ( // ProblemDetails is a struct that represents a problem details object as defined in RFC 7807. // https://datatracker.ietf.org/doc/html/rfc7807 type ProblemDetails struct { - Type string `json:"type"` - Title string `json:"title"` - Status int `json:"status"` - Detail string `json:"detail"` - Instance string `json:"instance"` + api.ErrorsProblemDetails } // PushDetail appends a detail to the existing details, separated by a semicolon. From c32adac9d529afb29018635c46cfce45bababecd Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 12:27:05 +0100 Subject: [PATCH 7/9] feat(SPV-1583): lint --- actions/v2/admin/users/get.go | 2 +- actions/v2/admin/users/get_test.go | 3 ++- engine/v2/database/errors/helpers.go | 1 + errdef/problem_details.go | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/actions/v2/admin/users/get.go b/actions/v2/admin/users/get.go index 81c3cd5a0..da64b4dc1 100644 --- a/actions/v2/admin/users/get.go +++ b/actions/v2/admin/users/get.go @@ -1,10 +1,10 @@ package users import ( - dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "net/http" "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping" + dberrors "github.com/bitcoin-sv/spv-wallet/engine/v2/database/errors" "github.com/bitcoin-sv/spv-wallet/errdef/clienterr" "github.com/gin-gonic/gin" ) diff --git a/actions/v2/admin/users/get_test.go b/actions/v2/admin/users/get_test.go index 630734adf..c2f3b827e 100644 --- a/actions/v2/admin/users/get_test.go +++ b/actions/v2/admin/users/get_test.go @@ -1,10 +1,11 @@ package users_test import ( + "testing" + "github.com/bitcoin-sv/spv-wallet/actions/testabilities" testengine "github.com/bitcoin-sv/spv-wallet/engine/testabilities" "github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures" - "testing" ) func TestGetUser(t *testing.T) { diff --git a/engine/v2/database/errors/helpers.go b/engine/v2/database/errors/helpers.go index 0a3a6f48f..f93a9f79e 100644 --- a/engine/v2/database/errors/helpers.go +++ b/engine/v2/database/errors/helpers.go @@ -2,6 +2,7 @@ package dberrors import ( "errors" + "github.com/joomcode/errorx" "gorm.io/gorm" ) diff --git a/errdef/problem_details.go b/errdef/problem_details.go index 51ec8c18c..cfa7ea8b8 100644 --- a/errdef/problem_details.go +++ b/errdef/problem_details.go @@ -2,9 +2,9 @@ package errdef import ( "fmt" - "github.com/bitcoin-sv/spv-wallet/api" "strings" + "github.com/bitcoin-sv/spv-wallet/api" "github.com/joomcode/errorx" ) From 4716e2eb3ee3e10574040332228269d0cc4812cd Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:21:54 +0100 Subject: [PATCH 8/9] feat(SPV-1583): fix after merge --- api/gen.api.yaml | 211 ++++++++++++ api/gen.models.go | 305 +++++++++++++++++ api/manualtests/client/client.gen.go | 495 +++++++++++++++++++++++++++ 3 files changed, 1011 insertions(+) diff --git a/api/gen.api.yaml b/api/gen.api.yaml index f122c125a..e1a90582f 100644 --- a/api/gen.api.yaml +++ b/api/gen.api.yaml @@ -137,6 +137,45 @@ paths: summary: Get data for user tags: - Data + /api/v2/merkleroots: + get: + description: This endpoint fetches merkleroots from block header service according to the given query parameters. + operationId: merkleRoots + parameters: + - description: Batch size of merkleroots to be returned + example: 100 + in: query + name: batchSize + schema: + default: 2000 + minimum: 0 + type: integer + - description: Last processed merkleroot in client's database + example: ac973196d58e42da6ad030dc39f5fcc343bd040e1db29b30c146e9aea9354bab + in: query + name: lastEvaluatedKey + schema: + default: "" + type: string + responses: + "200": + $ref: '#/components/responses/responses_GetMerklerootsSuccess' + "400": + $ref: '#/components/responses/responses_GetMerklerootsBadRequest' + "401": + $ref: '#/components/responses/responses_UserNotAuthorized' + "404": + $ref: '#/components/responses/responses_GetMerklerootsNotFound' + "409": + $ref: '#/components/responses/responses_GetMerklerootsConflict' + "500": + $ref: '#/components/responses/responses_GetMerklerootsInternalServerError' + security: + - XPubAuth: + - user + summary: Get Merkleroots + tags: + - Merkleroots /api/v2/operations/search: get: description: This endpoint allows to search operations for authenticated user @@ -334,6 +373,43 @@ components: schema: $ref: '#/components/schemas/models_Data' description: Data found + responses_GetMerklerootsBadRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/errors_InvalidBatchSize' + description: Bad request is an error that occurs when the request is malformed. + responses_GetMerklerootsConflict: + content: + application/json: + schema: + $ref: '#/components/schemas/errors_MerkleRootNotInLongestChain' + description: Conflict is an error that occurs when the request conflicts with the current state of the server. + responses_GetMerklerootsInternalServerError: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/errors_BHSUnreachable' + - $ref: '#/components/schemas/errors_BHSNoSuccessResponse' + - $ref: '#/components/schemas/errors_BHSUnauthorized' + - $ref: '#/components/schemas/errors_BHSBadRequest' + - $ref: '#/components/schemas/errors_BHSUnhealthy' + - $ref: '#/components/schemas/errors_BHSBadURL' + - $ref: '#/components/schemas/errors_BHSParsingResponse' + description: Internal server error + responses_GetMerklerootsNotFound: + content: + application/json: + schema: + $ref: '#/components/schemas/errors_MerkleRootNotFound' + description: Not found is an error that occurs when the requested resource is not found. + responses_GetMerklerootsSuccess: + content: + application/json: + schema: + $ref: '#/components/schemas/models_GetMerkleRootResult' + description: Merkleroots found responses_InternalServerError: content: application/json: @@ -459,6 +535,69 @@ components: message: example: xpub authorization required type: object + errors_BHSBadRequest: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-bad-request + message: + example: Block Headers Service bad request + type: object + errors_BHSBadURL: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-bad-url + message: + example: cannot create Block Headers Service url. Please check your configuration + type: object + errors_BHSNoSuccessResponse: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-no-success-response + message: + example: Block Headers Service request did not return a success response + type: object + errors_BHSParsingResponse: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-parse-response + message: + example: cannot parse Block Headers Service response + type: object + errors_BHSUnauthorized: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-unauthorized + message: + example: Block Headers Service returned unauthorized + type: object + errors_BHSUnhealthy: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-unhealthy + message: + example: Block Headers Service is unhealthy + type: object + errors_BHSUnreachable: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-bhs-unreachable + message: + example: Block Headers Service cannot be requested + type: object errors_DataNotFound: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -486,6 +625,15 @@ components: message: example: internal server error type: object + errors_InvalidBatchSize: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-invalid-batch-size + message: + example: batchSize must be 0 or a positive integer + type: object errors_InvalidDataID: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -495,6 +643,24 @@ components: message: example: invalid data id type: object + errors_MerkleRootNotFound: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-merkleroot-not-found + message: + example: No block with provided merkleroot was found + type: object + errors_MerkleRootNotInLongestChain: + allOf: + - $ref: '#/components/schemas/errors_Schema' + - properties: + code: + example: error-merkleroot-not-part-of-longest-chain + message: + example: Provided merkleroot is not part of the longest chain + type: object errors_NoOperations: allOf: - $ref: '#/components/schemas/errors_Schema' @@ -697,6 +863,37 @@ components: required: - bucket type: object + models_ExclusiveStartKeySearchPage: + properties: + lastEvaluatedKey: + description: Last evaluated key + example: ac973196d58e42da6ad030dc39f5fcc343bd040e1db29b30c146e9aea9354bab + type: string + size: + description: Number of items in returned data + example: 50 + type: integer + totalElements: + description: Total number of items + example: 456 + type: integer + required: + - totalElements + - size + - lastEvaluatedKey + type: object + models_GetMerkleRootResult: + properties: + content: + items: + $ref: '#/components/schemas/models_MerkleRoot' + type: array + page: + $ref: '#/components/schemas/models_ExclusiveStartKeySearchPage' + required: + - content + - page + type: object models_InputAnnotation: properties: customInstructions: @@ -719,6 +916,20 @@ components: required: - inputs type: object + models_MerkleRoot: + properties: + blockHeight: + description: Block height + example: 1234 + type: integer + merkleRoot: + description: Transaction ID + example: bb8593f85ef8056a77026ad415f02128f3768906de53e9e8bf8749fe2d66cf50 + type: string + required: + - merkleRoot + - blockHeight + type: object models_Operation: properties: counterparty: diff --git a/api/gen.models.go b/api/gen.models.go index 670573b8c..2629da2bb 100644 --- a/api/gen.models.go +++ b/api/gen.models.go @@ -119,6 +119,48 @@ type ErrorsAuthXPubRequired struct { Message interface{} `json:"message"` } +// ErrorsBHSBadRequest defines model for errors_BHSBadRequest. +type ErrorsBHSBadRequest struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSBadURL defines model for errors_BHSBadURL. +type ErrorsBHSBadURL struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSNoSuccessResponse defines model for errors_BHSNoSuccessResponse. +type ErrorsBHSNoSuccessResponse struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSParsingResponse defines model for errors_BHSParsingResponse. +type ErrorsBHSParsingResponse struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSUnauthorized defines model for errors_BHSUnauthorized. +type ErrorsBHSUnauthorized struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSUnhealthy defines model for errors_BHSUnhealthy. +type ErrorsBHSUnhealthy struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSUnreachable defines model for errors_BHSUnreachable. +type ErrorsBHSUnreachable struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + // ErrorsDataNotFound defines model for errors_DataNotFound. type ErrorsDataNotFound struct { Code interface{} `json:"code"` @@ -137,12 +179,30 @@ type ErrorsInternal struct { Message interface{} `json:"message"` } +// ErrorsInvalidBatchSize defines model for errors_InvalidBatchSize. +type ErrorsInvalidBatchSize struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + // ErrorsInvalidDataID defines model for errors_InvalidDataID. type ErrorsInvalidDataID struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } +// ErrorsMerkleRootNotFound defines model for errors_MerkleRootNotFound. +type ErrorsMerkleRootNotFound struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsMerkleRootNotInLongestChain defines model for errors_MerkleRootNotInLongestChain. +type ErrorsMerkleRootNotInLongestChain struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + // ErrorsNoOperations defines model for errors_NoOperations. type ErrorsNoOperations struct { Code interface{} `json:"code"` @@ -294,6 +354,24 @@ type ModelsDataAnnotation struct { // ModelsDataAnnotationBucket defines model for ModelsDataAnnotation.Bucket. type ModelsDataAnnotationBucket string +// ModelsExclusiveStartKeySearchPage defines model for models_ExclusiveStartKeySearchPage. +type ModelsExclusiveStartKeySearchPage struct { + // LastEvaluatedKey Last evaluated key + LastEvaluatedKey string `json:"lastEvaluatedKey"` + + // Size Number of items in returned data + Size int `json:"size"` + + // TotalElements Total number of items + TotalElements int `json:"totalElements"` +} + +// ModelsGetMerkleRootResult defines model for models_GetMerkleRootResult. +type ModelsGetMerkleRootResult struct { + Content []ModelsMerkleRoot `json:"content"` + Page ModelsExclusiveStartKeySearchPage `json:"page"` +} + // ModelsInputAnnotation defines model for models_InputAnnotation. type ModelsInputAnnotation struct { CustomInstructions ModelsCustomInstructions `json:"customInstructions"` @@ -305,6 +383,15 @@ type ModelsInputsAnnotations struct { Inputs map[string]ModelsInputAnnotation `json:"inputs"` } +// ModelsMerkleRoot defines model for models_MerkleRoot. +type ModelsMerkleRoot struct { + // BlockHeight Block height + BlockHeight int `json:"blockHeight"` + + // MerkleRoot Transaction ID + MerkleRoot string `json:"merkleRoot"` +} + // ModelsOperation defines model for models_Operation. type ModelsOperation struct { // Counterparty Counterparty of operation @@ -589,6 +676,23 @@ type ResponsesGetDataNotFound struct { // ResponsesGetDataSuccess defines model for responses_GetDataSuccess. type ResponsesGetDataSuccess = ModelsData +// ResponsesGetMerklerootsBadRequest defines model for responses_GetMerklerootsBadRequest. +type ResponsesGetMerklerootsBadRequest = ErrorsInvalidBatchSize + +// ResponsesGetMerklerootsConflict defines model for responses_GetMerklerootsConflict. +type ResponsesGetMerklerootsConflict = ErrorsMerkleRootNotInLongestChain + +// ResponsesGetMerklerootsInternalServerError defines model for responses_GetMerklerootsInternalServerError. +type ResponsesGetMerklerootsInternalServerError struct { + union json.RawMessage +} + +// ResponsesGetMerklerootsNotFound defines model for responses_GetMerklerootsNotFound. +type ResponsesGetMerklerootsNotFound = ErrorsMerkleRootNotFound + +// ResponsesGetMerklerootsSuccess defines model for responses_GetMerklerootsSuccess. +type ResponsesGetMerklerootsSuccess = ModelsGetMerkleRootResult + // ResponsesInternalServerError defines model for responses_InternalServerError. type ResponsesInternalServerError = ErrorsInternal @@ -629,6 +733,15 @@ type ResponsesUserBadRequest = ErrorsInvalidDataID // ResponsesUserNotAuthorized defines model for responses_UserNotAuthorized. type ResponsesUserNotAuthorized = ErrorsUserAuthorization +// MerkleRootsParams defines parameters for MerkleRoots. +type MerkleRootsParams struct { + // BatchSize Batch size of merkleroots to be returned + BatchSize *int `form:"batchSize,omitempty" json:"batchSize,omitempty"` + + // LastEvaluatedKey Last processed merkleroot in client's database + LastEvaluatedKey *string `form:"lastEvaluatedKey,omitempty" json:"lastEvaluatedKey,omitempty"` +} + // SearchOperationsParams defines parameters for SearchOperations. type SearchOperationsParams struct { // Page Page number for pagination @@ -1328,6 +1441,198 @@ func (t *ResponsesGetDataNotFound) UnmarshalJSON(b []byte) error { return err } +// AsErrorsBHSUnreachable returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSUnreachable +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSUnreachable() (ErrorsBHSUnreachable, error) { + var body ErrorsBHSUnreachable + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSUnreachable overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSUnreachable +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSUnreachable(v ErrorsBHSUnreachable) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSUnreachable performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSUnreachable +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSUnreachable(v ErrorsBHSUnreachable) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSNoSuccessResponse returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSNoSuccessResponse +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSNoSuccessResponse() (ErrorsBHSNoSuccessResponse, error) { + var body ErrorsBHSNoSuccessResponse + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSNoSuccessResponse overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSNoSuccessResponse +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSNoSuccessResponse(v ErrorsBHSNoSuccessResponse) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSNoSuccessResponse performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSNoSuccessResponse +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSNoSuccessResponse(v ErrorsBHSNoSuccessResponse) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSUnauthorized returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSUnauthorized +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSUnauthorized() (ErrorsBHSUnauthorized, error) { + var body ErrorsBHSUnauthorized + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSUnauthorized overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSUnauthorized +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSUnauthorized(v ErrorsBHSUnauthorized) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSUnauthorized performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSUnauthorized +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSUnauthorized(v ErrorsBHSUnauthorized) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSBadRequest returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSBadRequest +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSBadRequest() (ErrorsBHSBadRequest, error) { + var body ErrorsBHSBadRequest + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSBadRequest overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSBadRequest +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSBadRequest(v ErrorsBHSBadRequest) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSBadRequest performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSBadRequest +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSBadRequest(v ErrorsBHSBadRequest) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSUnhealthy returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSUnhealthy +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSUnhealthy() (ErrorsBHSUnhealthy, error) { + var body ErrorsBHSUnhealthy + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSUnhealthy overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSUnhealthy +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSUnhealthy(v ErrorsBHSUnhealthy) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSUnhealthy performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSUnhealthy +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSUnhealthy(v ErrorsBHSUnhealthy) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSBadURL returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSBadURL +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSBadURL() (ErrorsBHSBadURL, error) { + var body ErrorsBHSBadURL + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSBadURL overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSBadURL +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSBadURL(v ErrorsBHSBadURL) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSBadURL performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSBadURL +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSBadURL(v ErrorsBHSBadURL) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSParsingResponse returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSParsingResponse +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSParsingResponse() (ErrorsBHSParsingResponse, error) { + var body ErrorsBHSParsingResponse + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSParsingResponse overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSParsingResponse +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSParsingResponse(v ErrorsBHSParsingResponse) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSParsingResponse performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSParsingResponse +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSParsingResponse(v ErrorsBHSParsingResponse) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t ResponsesGetMerklerootsInternalServerError) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ResponsesGetMerklerootsInternalServerError) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsErrorsInvalidDataID returns the union data inside the ResponsesRecordTransactionBadRequest as a ErrorsInvalidDataID func (t ResponsesRecordTransactionBadRequest) AsErrorsInvalidDataID() (ErrorsInvalidDataID, error) { var body ErrorsInvalidDataID diff --git a/api/manualtests/client/client.gen.go b/api/manualtests/client/client.gen.go index cbdab240d..1a28d2d8a 100644 --- a/api/manualtests/client/client.gen.go +++ b/api/manualtests/client/client.gen.go @@ -126,6 +126,48 @@ type ErrorsAuthXPubRequired struct { Message interface{} `json:"message"` } +// ErrorsBHSBadRequest defines model for errors_BHSBadRequest. +type ErrorsBHSBadRequest struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSBadURL defines model for errors_BHSBadURL. +type ErrorsBHSBadURL struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSNoSuccessResponse defines model for errors_BHSNoSuccessResponse. +type ErrorsBHSNoSuccessResponse struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSParsingResponse defines model for errors_BHSParsingResponse. +type ErrorsBHSParsingResponse struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSUnauthorized defines model for errors_BHSUnauthorized. +type ErrorsBHSUnauthorized struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSUnhealthy defines model for errors_BHSUnhealthy. +type ErrorsBHSUnhealthy struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsBHSUnreachable defines model for errors_BHSUnreachable. +type ErrorsBHSUnreachable struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + // ErrorsDataNotFound defines model for errors_DataNotFound. type ErrorsDataNotFound struct { Code interface{} `json:"code"` @@ -144,12 +186,30 @@ type ErrorsInternal struct { Message interface{} `json:"message"` } +// ErrorsInvalidBatchSize defines model for errors_InvalidBatchSize. +type ErrorsInvalidBatchSize struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + // ErrorsInvalidDataID defines model for errors_InvalidDataID. type ErrorsInvalidDataID struct { Code interface{} `json:"code"` Message interface{} `json:"message"` } +// ErrorsMerkleRootNotFound defines model for errors_MerkleRootNotFound. +type ErrorsMerkleRootNotFound struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + +// ErrorsMerkleRootNotInLongestChain defines model for errors_MerkleRootNotInLongestChain. +type ErrorsMerkleRootNotInLongestChain struct { + Code interface{} `json:"code"` + Message interface{} `json:"message"` +} + // ErrorsNoOperations defines model for errors_NoOperations. type ErrorsNoOperations struct { Code interface{} `json:"code"` @@ -301,6 +361,24 @@ type ModelsDataAnnotation struct { // ModelsDataAnnotationBucket defines model for ModelsDataAnnotation.Bucket. type ModelsDataAnnotationBucket string +// ModelsExclusiveStartKeySearchPage defines model for models_ExclusiveStartKeySearchPage. +type ModelsExclusiveStartKeySearchPage struct { + // LastEvaluatedKey Last evaluated key + LastEvaluatedKey string `json:"lastEvaluatedKey"` + + // Size Number of items in returned data + Size int `json:"size"` + + // TotalElements Total number of items + TotalElements int `json:"totalElements"` +} + +// ModelsGetMerkleRootResult defines model for models_GetMerkleRootResult. +type ModelsGetMerkleRootResult struct { + Content []ModelsMerkleRoot `json:"content"` + Page ModelsExclusiveStartKeySearchPage `json:"page"` +} + // ModelsInputAnnotation defines model for models_InputAnnotation. type ModelsInputAnnotation struct { CustomInstructions ModelsCustomInstructions `json:"customInstructions"` @@ -312,6 +390,15 @@ type ModelsInputsAnnotations struct { Inputs map[string]ModelsInputAnnotation `json:"inputs"` } +// ModelsMerkleRoot defines model for models_MerkleRoot. +type ModelsMerkleRoot struct { + // BlockHeight Block height + BlockHeight int `json:"blockHeight"` + + // MerkleRoot Transaction ID + MerkleRoot string `json:"merkleRoot"` +} + // ModelsOperation defines model for models_Operation. type ModelsOperation struct { // Counterparty Counterparty of operation @@ -596,6 +683,23 @@ type ResponsesGetDataNotFound struct { // ResponsesGetDataSuccess defines model for responses_GetDataSuccess. type ResponsesGetDataSuccess = ModelsData +// ResponsesGetMerklerootsBadRequest defines model for responses_GetMerklerootsBadRequest. +type ResponsesGetMerklerootsBadRequest = ErrorsInvalidBatchSize + +// ResponsesGetMerklerootsConflict defines model for responses_GetMerklerootsConflict. +type ResponsesGetMerklerootsConflict = ErrorsMerkleRootNotInLongestChain + +// ResponsesGetMerklerootsInternalServerError defines model for responses_GetMerklerootsInternalServerError. +type ResponsesGetMerklerootsInternalServerError struct { + union json.RawMessage +} + +// ResponsesGetMerklerootsNotFound defines model for responses_GetMerklerootsNotFound. +type ResponsesGetMerklerootsNotFound = ErrorsMerkleRootNotFound + +// ResponsesGetMerklerootsSuccess defines model for responses_GetMerklerootsSuccess. +type ResponsesGetMerklerootsSuccess = ModelsGetMerkleRootResult + // ResponsesInternalServerError defines model for responses_InternalServerError. type ResponsesInternalServerError = ErrorsInternal @@ -636,6 +740,15 @@ type ResponsesUserBadRequest = ErrorsInvalidDataID // ResponsesUserNotAuthorized defines model for responses_UserNotAuthorized. type ResponsesUserNotAuthorized = ErrorsUserAuthorization +// MerkleRootsParams defines parameters for MerkleRoots. +type MerkleRootsParams struct { + // BatchSize Batch size of merkleroots to be returned + BatchSize *int `form:"batchSize,omitempty" json:"batchSize,omitempty"` + + // LastEvaluatedKey Last processed merkleroot in client's database + LastEvaluatedKey *string `form:"lastEvaluatedKey,omitempty" json:"lastEvaluatedKey,omitempty"` +} + // SearchOperationsParams defines parameters for SearchOperations. type SearchOperationsParams struct { // Page Page number for pagination @@ -1335,6 +1448,198 @@ func (t *ResponsesGetDataNotFound) UnmarshalJSON(b []byte) error { return err } +// AsErrorsBHSUnreachable returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSUnreachable +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSUnreachable() (ErrorsBHSUnreachable, error) { + var body ErrorsBHSUnreachable + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSUnreachable overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSUnreachable +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSUnreachable(v ErrorsBHSUnreachable) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSUnreachable performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSUnreachable +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSUnreachable(v ErrorsBHSUnreachable) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSNoSuccessResponse returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSNoSuccessResponse +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSNoSuccessResponse() (ErrorsBHSNoSuccessResponse, error) { + var body ErrorsBHSNoSuccessResponse + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSNoSuccessResponse overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSNoSuccessResponse +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSNoSuccessResponse(v ErrorsBHSNoSuccessResponse) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSNoSuccessResponse performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSNoSuccessResponse +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSNoSuccessResponse(v ErrorsBHSNoSuccessResponse) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSUnauthorized returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSUnauthorized +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSUnauthorized() (ErrorsBHSUnauthorized, error) { + var body ErrorsBHSUnauthorized + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSUnauthorized overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSUnauthorized +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSUnauthorized(v ErrorsBHSUnauthorized) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSUnauthorized performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSUnauthorized +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSUnauthorized(v ErrorsBHSUnauthorized) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSBadRequest returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSBadRequest +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSBadRequest() (ErrorsBHSBadRequest, error) { + var body ErrorsBHSBadRequest + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSBadRequest overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSBadRequest +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSBadRequest(v ErrorsBHSBadRequest) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSBadRequest performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSBadRequest +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSBadRequest(v ErrorsBHSBadRequest) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSUnhealthy returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSUnhealthy +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSUnhealthy() (ErrorsBHSUnhealthy, error) { + var body ErrorsBHSUnhealthy + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSUnhealthy overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSUnhealthy +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSUnhealthy(v ErrorsBHSUnhealthy) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSUnhealthy performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSUnhealthy +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSUnhealthy(v ErrorsBHSUnhealthy) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSBadURL returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSBadURL +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSBadURL() (ErrorsBHSBadURL, error) { + var body ErrorsBHSBadURL + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSBadURL overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSBadURL +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSBadURL(v ErrorsBHSBadURL) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSBadURL performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSBadURL +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSBadURL(v ErrorsBHSBadURL) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsErrorsBHSParsingResponse returns the union data inside the ResponsesGetMerklerootsInternalServerError as a ErrorsBHSParsingResponse +func (t ResponsesGetMerklerootsInternalServerError) AsErrorsBHSParsingResponse() (ErrorsBHSParsingResponse, error) { + var body ErrorsBHSParsingResponse + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromErrorsBHSParsingResponse overwrites any union data inside the ResponsesGetMerklerootsInternalServerError as the provided ErrorsBHSParsingResponse +func (t *ResponsesGetMerklerootsInternalServerError) FromErrorsBHSParsingResponse(v ErrorsBHSParsingResponse) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeErrorsBHSParsingResponse performs a merge with any union data inside the ResponsesGetMerklerootsInternalServerError, using the provided ErrorsBHSParsingResponse +func (t *ResponsesGetMerklerootsInternalServerError) MergeErrorsBHSParsingResponse(v ErrorsBHSParsingResponse) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t ResponsesGetMerklerootsInternalServerError) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ResponsesGetMerklerootsInternalServerError) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsErrorsInvalidDataID returns the union data inside the ResponsesRecordTransactionBadRequest as a ErrorsInvalidDataID func (t ResponsesRecordTransactionBadRequest) AsErrorsInvalidDataID() (ErrorsInvalidDataID, error) { var body ErrorsInvalidDataID @@ -1658,6 +1963,9 @@ type ClientInterface interface { // DataById request DataById(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // MerkleRoots request + MerkleRoots(ctx context.Context, params *MerkleRootsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // SearchOperations request SearchOperations(ctx context.Context, params *SearchOperationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1771,6 +2079,18 @@ func (c *Client) DataById(ctx context.Context, id string, reqEditors ...RequestE return c.Client.Do(req) } +func (c *Client) MerkleRoots(ctx context.Context, params *MerkleRootsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMerkleRootsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) SearchOperations(ctx context.Context, params *SearchOperationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewSearchOperationsRequest(c.Server, params) if err != nil { @@ -2052,6 +2372,71 @@ func NewDataByIdRequest(server string, id string) (*http.Request, error) { return req, nil } +// NewMerkleRootsRequest generates requests for MerkleRoots +func NewMerkleRootsRequest(server string, params *MerkleRootsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v2/merkleroots") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.BatchSize != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "batchSize", runtime.ParamLocationQuery, *params.BatchSize); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.LastEvaluatedKey != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "lastEvaluatedKey", runtime.ParamLocationQuery, *params.LastEvaluatedKey); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewSearchOperationsRequest generates requests for SearchOperations func NewSearchOperationsRequest(server string, params *SearchOperationsParams) (*http.Request, error) { var err error @@ -2343,6 +2728,9 @@ type ClientWithResponsesInterface interface { // DataByIdWithResponse request DataByIdWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*DataByIdResponse, error) + // MerkleRootsWithResponse request + MerkleRootsWithResponse(ctx context.Context, params *MerkleRootsParams, reqEditors ...RequestEditorFn) (*MerkleRootsResponse, error) + // SearchOperationsWithResponse request SearchOperationsWithResponse(ctx context.Context, params *SearchOperationsParams, reqEditors ...RequestEditorFn) (*SearchOperationsResponse, error) @@ -2560,6 +2948,43 @@ func (r DataByIdResponse) Bytes() []byte { return r.Body } +type MerkleRootsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ResponsesGetMerklerootsSuccess + JSON400 *ResponsesGetMerklerootsBadRequest + JSON401 *ResponsesUserNotAuthorized + JSON404 *ResponsesGetMerklerootsNotFound + JSON409 *ResponsesGetMerklerootsConflict + JSON500 *ResponsesGetMerklerootsInternalServerError +} + +// Status returns HTTPResponse.Status +func (r MerkleRootsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r MerkleRootsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// HTTPResponse returns http.Response from which this response was parsed. +func (r MerkleRootsResponse) Response() *http.Response { + return r.HTTPResponse +} + +// Bytes is a convenience method to retrieve the raw bytes from the HTTP response +func (r MerkleRootsResponse) Bytes() []byte { + return r.Body +} + type SearchOperationsResponse struct { Body []byte HTTPResponse *http.Response @@ -2770,6 +3195,15 @@ func (c *ClientWithResponses) DataByIdWithResponse(ctx context.Context, id strin return ParseDataByIdResponse(rsp) } +// MerkleRootsWithResponse request returning *MerkleRootsResponse +func (c *ClientWithResponses) MerkleRootsWithResponse(ctx context.Context, params *MerkleRootsParams, reqEditors ...RequestEditorFn) (*MerkleRootsResponse, error) { + rsp, err := c.MerkleRoots(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseMerkleRootsResponse(rsp) +} + // SearchOperationsWithResponse request returning *SearchOperationsResponse func (c *ClientWithResponses) SearchOperationsWithResponse(ctx context.Context, params *SearchOperationsParams, reqEditors ...RequestEditorFn) (*SearchOperationsResponse, error) { rsp, err := c.SearchOperations(ctx, params, reqEditors...) @@ -3034,6 +3468,67 @@ func ParseDataByIdResponse(rsp *http.Response) (*DataByIdResponse, error) { return response, nil } +// ParseMerkleRootsResponse parses an HTTP response from a MerkleRootsWithResponse call +func ParseMerkleRootsResponse(rsp *http.Response) (*MerkleRootsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &MerkleRootsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ResponsesGetMerklerootsSuccess + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ResponsesGetMerklerootsBadRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest ResponsesUserNotAuthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ResponsesGetMerklerootsNotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ResponsesGetMerklerootsConflict + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ResponsesGetMerklerootsInternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseSearchOperationsResponse parses an HTTP response from a SearchOperationsWithResponse call func ParseSearchOperationsResponse(rsp *http.Response) (*SearchOperationsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) From 428db93dd6a367ff4c10d3e92f1b6cee4c7d784c Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:57:11 +0100 Subject: [PATCH 9/9] feat(SPV-1583): log with stack for 5xx --- errdef/clienterr/http_error.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/errdef/clienterr/http_error.go b/errdef/clienterr/http_error.go index 32d8e2edc..fc5c98227 100644 --- a/errdef/clienterr/http_error.go +++ b/errdef/clienterr/http_error.go @@ -12,7 +12,13 @@ import ( // Response sends the error as a JSON response to the client. func Response(c *gin.Context, err error, log *zerolog.Logger) { problem, logLevel := problemDetailsFromError(err) - log.WithLevel(logLevel).Err(err).Msgf("Error HTTP response, returning %d: %s", problem.Status, problem.Detail) + + l := log.WithLevel(logLevel) + if problem.Status >= 500 { + l.Stack() + } + l.Err(err).Msgf("Error HTTP response, returning %d: %s", problem.Status, problem.Detail) + c.JSON(problem.Status, problem) }