Skip to content

Commit

Permalink
Return correlation ID from API Error
Browse files Browse the repository at this point in the history
  • Loading branch information
drohit-cb committed Sep 12, 2024
1 parent a98e1f9 commit 6cabecd
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 14 deletions.
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Coinbase Go SDK Changelog

## Unreleased

## [0.0.6] - 2024-09-12

### Added

- Return correlation ID from APIError response

## [0.0.5] - 2024-08-29

### Added

- Allow for use with directly set api key
- Return user facing error type APIError for a server side error

## [0.0.4] - 2024-08-28

### Added

- Add user facing validator statuses

## [0.0.3] - 2024-08-27

### Fixed

- Fixed a bug where we weren't handling the api returned validators properly resulting in an index out of range error.

## [0.0.2] - 2024-08-27

### Fixed
Expand Down
19 changes: 18 additions & 1 deletion examples/build_staking_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

func main() {
ctx := context.Background()

client, err := coinbase.NewClient(
coinbase.WithAPIKeyFromJSON(os.Args[1]),
)
Expand All @@ -21,11 +22,27 @@ func main() {
}

address := coinbase.NewExternalAddress("ethereum-holesky", "0x57a063e1df096aaA6b2068C3C7FE6Ac4BC3c4F58")
op, err := client.BuildStakeOperation(ctx, big.NewFloat(0.0001), coinbase.Eth, address)

stakeableBalance, err := client.GetStakeableBalance(ctx, coinbase.Eth, address, coinbase.WithStakingBalanceMode(coinbase.StakingOperationModePartial))
if err != nil {
log.Fatalf("error getting stakeable balance: %v", err)
}

log.Printf("stakeable balance: %s\n", stakeableBalance)

op, err := client.BuildStakeOperation(
ctx,
big.NewFloat(0.0001),
coinbase.Eth,
address,
coinbase.WithStakingOperationMode(coinbase.StakingOperationModePartial),
)
if err != nil {
log.Fatalf("error building staking operation: %v", err)
}

log.Printf("staking operation ID: %s\n", op.ID())

for _, transaction := range op.Transactions() {
log.Printf("staking operation Transaction: %+v\n", transaction)
}
Expand Down
37 changes: 37 additions & 0 deletions gen/client/model_error.go

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

4 changes: 2 additions & 2 deletions pkg/coinbase/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (s *ValidatorSuite) TestListValidators_Failure() {
validators, err := s.client.ListValidators(ctx, networkId, assetId)

s.Assert().Nil(validators)
s.EqualError(err, "APIError{ httpStatusCode: 500, apiCode: unknown, apiMessage: some error calling api }")
s.EqualError(err, "APIError{HttpStatusCode: 500, Code: unknown, Message: some error calling api}")
}

func (s *ValidatorSuite) TestGetValidator_Success() {
Expand Down Expand Up @@ -128,7 +128,7 @@ func (s *ValidatorSuite) TestGetValidator_Failure() {
validator, err := s.client.GetValidator(ctx, networkId, assetId, validatorId)

s.Assert().Empty(validator)
s.EqualError(err, "APIError{ httpStatusCode: 500, apiCode: unknown, apiMessage: some error calling api }")
s.EqualError(err, "APIError{HttpStatusCode: 500, Code: unknown, Message: some error calling api}")
}

func (s *ValidatorSuite) mockSuccessfulListValidators(ctx context.Context, networkId string, assetId string, mockValidators *api.ValidatorList) {
Expand Down
23 changes: 18 additions & 5 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,34 @@ import (
"errors"
"fmt"
"net/http"
"reflect"
"strings"

"github.com/coinbase/coinbase-sdk-go/gen/client"
)

// APIError is a custom error type for API errors.
type APIError struct {
HttpStatusCode int
// A short string representing the reported error. Can be used to handle errors programmatically.
Code string
// A human-readable message providing more details about the error.
Message string
Code string // A short string representing the reported error. Can be used to handle errors programmatically.
Message string // A human-readable message providing more details about the error.
CorrelationId string // A correlation ID that can be used to help debug the error.
}

func (e *APIError) Error() string {
return fmt.Sprintf("APIError{ httpStatusCode: %d, apiCode: %s, apiMessage: %s }", e.HttpStatusCode, e.Code, e.Message)
v := reflect.ValueOf(*e)
typeOfE := v.Type()
var fields []string

for i := 0; i < v.NumField(); i++ {
fieldValue := v.Field(i)
if !fieldValue.IsZero() {
field := fmt.Sprintf("%s: %v", typeOfE.Field(i).Name, fieldValue.Interface())
fields = append(fields, field)
}
}

return fmt.Sprintf("APIError{%s}", strings.Join(fields, ", "))
}

// MapToUserFacing maps the error to a custom user facing error type.
Expand Down
78 changes: 72 additions & 6 deletions pkg/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ import (
"github.com/stretchr/testify/suite"
)

type ErrorsTestSuite struct {
type MapErrorsTestSuite struct {
suite.Suite
}

func TestErrorsTestSuite(t *testing.T) {
suite.Run(t, new(ErrorsTestSuite))
func TestMapErrorsTestSuite(t *testing.T) {
suite.Run(t, new(MapErrorsTestSuite))
}

func (suite *ErrorsTestSuite) TestMapToUserFacing_NilError() {
func (suite *MapErrorsTestSuite) TestMapToUserFacing_NilError() {
err := MapToUserFacing(nil, nil)
assert.Nil(suite.T(), err)
}

func (suite *ErrorsTestSuite) TestMapToUserFacing_GenericOpenAPIError() {
func (suite *MapErrorsTestSuite) TestMapToUserFacing_GenericOpenAPIError() {
body := []byte(`{"code": "test_code", "message": "test_message"}`)
openAPIError := createGenericOpenAPIError(body)
resp := &http.Response{StatusCode: http.StatusBadRequest}
Expand All @@ -40,7 +40,7 @@ func (suite *ErrorsTestSuite) TestMapToUserFacing_GenericOpenAPIError() {
assert.Equal(suite.T(), http.StatusBadRequest, apiErr.HttpStatusCode)
}

func (suite *ErrorsTestSuite) TestMapToUserFacing_UnknownError() {
func (suite *MapErrorsTestSuite) TestMapToUserFacing_UnknownError() {
unknownErr := errors.New("unknown error")
resp := &http.Response{StatusCode: http.StatusInternalServerError}

Expand All @@ -63,3 +63,69 @@ func createGenericOpenAPIError(body []byte) client.GenericOpenAPIError {
reflect.NewAt(bodyField.Type(), unsafe.Pointer(bodyField.UnsafeAddr())).Elem().SetBytes(body)
return openAPIError
}

type APIErrorTestSuite struct {
suite.Suite
}

func TestAPIErrorTestSuite(t *testing.T) {
suite.Run(t, new(APIErrorTestSuite))
}

func (suite *APIErrorTestSuite) TestError() {
tests := []struct {
name string
apiError APIError
expected string
}{
{
name: "All fields set",
apiError: APIError{
HttpStatusCode: 400,
Code: "test_code",
Message: "test_message",
CorrelationId: "test_correlation_id",
},
expected: "APIError{HttpStatusCode: 400, Code: test_code, Message: test_message, CorrelationId: test_correlation_id}",
},
{
name: "Only HttpStatusCode set",
apiError: APIError{
HttpStatusCode: 400,
},
expected: "APIError{HttpStatusCode: 400}",
},
{
name: "Only Code set",
apiError: APIError{
Code: "test_code",
},
expected: "APIError{Code: test_code}",
},
{
name: "Only Message set",
apiError: APIError{
Message: "test_message",
},
expected: "APIError{Message: test_message}",
},
{
name: "Only CorrelationId set",
apiError: APIError{
CorrelationId: "test_correlation_id",
},
expected: "APIError{CorrelationId: test_correlation_id}",
},
{
name: "No fields set",
apiError: APIError{},
expected: "APIError{}",
},
}

for _, tt := range tests {
suite.Run(tt.name, func() {
assert.Equal(suite.T(), tt.expected, tt.apiError.Error())
})
}
}

0 comments on commit 6cabecd

Please sign in to comment.