diff --git a/CAPABILITIES.md b/CAPABILITIES.md deleted file mode 100644 index 7a97c7d..0000000 --- a/CAPABILITIES.md +++ /dev/null @@ -1,35 +0,0 @@ -# Capabilities - -The Coinbase SDK has different capabilities for different wallet types and networks. This page summarizes -those capabilities for the Go SDK: - -## Developer Wallets - -| Concept | Base-Sepolia | Base-Mainnet | Ethereum-Holesky | Ethereum-Mainnet | -| ------------- | :----------: | :----------: | :--------------: | :--------------: | -| Addresses | ❌ | ❌ | ❌ | ❌ | -| Send | ❌ | ❌ | ❌ | ❌ | -| Trade | ❌ | ❌ | ❌ | ❌ | -| Faucet | ❌ | ❌ | ❌ | ❌ | -| Server-Signer | ❌ | ❌ | ❌ | ❌ | -| Stake [^1] | ❌ | ❌ | ❌ | ❌ | - -[^1]: Currently only available for Shared ETH Staking. - -## End-User Wallets - -| Concept | Base-Sepolia | Base-Mainnet | Ethereum-Holesky | Ethereum-Mainnet | -| ------------------ | :----------: | :----------: | :--------------: | :--------------: | -| External Addresses | ❌ | ❌ | ❌ | ❌ | -| Stake [^2] | ❌ | ❌ | ❌ | ❌ | - -[^2]: Dedicated ETH Staking is currently only available on Testnet (Ethereum-Holesky). - -## Testnet vs. Mainnet - -The Coinbase SDK supports both testnets and mainnets. - -- Testnets are for building and testing applications. Funds are not real, and you can get test currencies from a faucet. -- Mainnet is where the funds, contracts and applications are real. - -Wallets, assets, etc, cannot be moved from testnet to mainnet (or vice versa). diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e285a9..7cb6692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/build_staking_operation.go b/examples/build_staking_operation.go index beaa9c1..7ca2e46 100644 --- a/examples/build_staking_operation.go +++ b/examples/build_staking_operation.go @@ -13,6 +13,7 @@ import ( func main() { ctx := context.Background() + client, err := coinbase.NewClient( coinbase.WithAPIKeyFromJSON(os.Args[1]), ) @@ -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) } diff --git a/gen/client/model_error.go b/gen/client/model_error.go index eaaeee7..15feb1b 100644 --- a/gen/client/model_error.go +++ b/gen/client/model_error.go @@ -25,6 +25,8 @@ type Error struct { Code string `json:"code"` // A human-readable message providing more details about the error. Message string `json:"message"` + // A unique identifier for the request that generated the error. This can be used to help debug issues with the API. + CorrelationId *string `json:"correlation_id,omitempty"` } type _Error Error @@ -96,6 +98,38 @@ func (o *Error) SetMessage(v string) { o.Message = v } +// GetCorrelationId returns the CorrelationId field value if set, zero value otherwise. +func (o *Error) GetCorrelationId() string { + if o == nil || IsNil(o.CorrelationId) { + var ret string + return ret + } + return *o.CorrelationId +} + +// GetCorrelationIdOk returns a tuple with the CorrelationId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Error) GetCorrelationIdOk() (*string, bool) { + if o == nil || IsNil(o.CorrelationId) { + return nil, false + } + return o.CorrelationId, true +} + +// HasCorrelationId returns a boolean if a field has been set. +func (o *Error) HasCorrelationId() bool { + if o != nil && !IsNil(o.CorrelationId) { + return true + } + + return false +} + +// SetCorrelationId gets a reference to the given string and assigns it to the CorrelationId field. +func (o *Error) SetCorrelationId(v string) { + o.CorrelationId = &v +} + func (o Error) MarshalJSON() ([]byte, error) { toSerialize,err := o.ToMap() if err != nil { @@ -108,6 +142,9 @@ func (o Error) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} toSerialize["code"] = o.Code toSerialize["message"] = o.Message + if !IsNil(o.CorrelationId) { + toSerialize["correlation_id"] = o.CorrelationId + } return toSerialize, nil } diff --git a/pkg/coinbase/validators_test.go b/pkg/coinbase/validators_test.go index 09d849c..ba91228 100644 --- a/pkg/coinbase/validators_test.go +++ b/pkg/coinbase/validators_test.go @@ -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() { @@ -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) { diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 0bd13f0..4da4c9b 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "net/http" + "reflect" + "strings" "github.com/coinbase/coinbase-sdk-go/gen/client" ) @@ -11,14 +13,25 @@ import ( // 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. diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index d8a6c64..27e88cc 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -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} @@ -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} @@ -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()) + }) + } +}