diff --git a/.golangci.yml b/.golangci.yml index 6b508c5..76bc33e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -84,7 +84,7 @@ linters-settings: max-blank-identifiers: 2 dupl: # tokens count to trigger issue, 150 by default - threshold: 100 + threshold: 250 errcheck: # report about not checking of errors in type assertions: `a := b.(MyStruct)`; # default is false: such cases aren't reported by default. @@ -462,4 +462,4 @@ severity: rules: - linters: - dupl - severity: info \ No newline at end of file + severity: info diff --git a/balance.go b/balance.go deleted file mode 100644 index 64e0524..0000000 --- a/balance.go +++ /dev/null @@ -1,31 +0,0 @@ -package handcash - -/* -// BalanceResponse is the balance response -type BalanceResponse struct { - SpendableSatoshiBalance uint64 `json:"spendableSatoshiBalance"` - SpendableFiatBalance float64 `json:"spendableFiatBalance"` - CurrencyCode CurrencyCode `json:"currencyCode"` -}*/ - -/* -// GetBalance gets the user's balance from the handcash connect API -func GetBalance(authToken string) (balanceResponse *BalanceResponse, err error) { - - // Make sure we have an auth token - if len(authToken) == 0 { - return nil, fmt.Errorf("missing auth token") - } - - // TODO: The actual request - - balanceResponse = new(BalanceResponse) - - err = json.Unmarshal([]byte("dummy"), balanceResponse) - if err != nil { - return nil, fmt.Errorf("failed to get balance %w", err) - } - - return -} -*/ diff --git a/config.go b/config.go index 1dd8562..57fc6fd 100644 --- a/config.go +++ b/config.go @@ -70,8 +70,8 @@ const ( // endpointWallet is for accessing wallet information endpointWallet = "/" + apiVersion + "/connect/wallet" - // endpointGetSpendableBalance will return a spendable balance amount - // endpointGetSpendableBalance = endpointProfile + "/spendableBalance" + // endpointGetSpendableBalanceRequest will return a spendable balance amount + endpointGetSpendableBalanceRequest = endpointWallet + "/spendableBalance" // endpointGetPayRequest will create a new pay request endpointGetPayRequest = endpointWallet + "/pay" diff --git a/definitions.go b/definitions.go index dd80ffa..39768f6 100644 --- a/definitions.go +++ b/definitions.go @@ -84,6 +84,11 @@ type PaymentRequest struct { TransactionID string `json:"transactionId"` } +// BalanceRequest is used for GetSpendableBalance() +type BalanceRequest struct { + CurrencyCode CurrencyCode `json:"currencyCode"` +} + // AppAction enum type AppAction string diff --git a/spendable_balance.go b/spendable_balance.go new file mode 100644 index 0000000..7977fdd --- /dev/null +++ b/spendable_balance.go @@ -0,0 +1,75 @@ +package handcash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// SpendableBalanceResponse is the balance response +type SpendableBalanceResponse struct { + SpendableSatoshiBalance uint64 `json:"spendableSatoshiBalance"` + SpendableFiatBalance float64 `json:"spendableFiatBalance"` + CurrencyCode CurrencyCode `json:"currencyCode"` +} + +// GetSpendableBalance gets the user's spendable balance from the handcash connect API +func (c *Client) GetSpendableBalance(ctx context.Context, authToken string, currencyCode CurrencyCode) (spendableBalanceResponse *SpendableBalanceResponse, err error) { + + // Make sure we have an auth token + if len(authToken) == 0 { + return nil, fmt.Errorf("missing auth token") + } + + if len(currencyCode) == 0 { + return nil, fmt.Errorf("missing currency code") + } + + // Get the signed request + signed, err := c.getSignedRequest( + http.MethodGet, + endpointGetSpendableBalanceRequest, + authToken, + &BalanceRequest{CurrencyCode: currencyCode}, + currentISOTimestamp(), + ) + if err != nil { + return nil, fmt.Errorf("error creating signed request: %w", err) + } + + // Convert into bytes + var params []byte + if params, err = json.Marshal( + &BalanceRequest{CurrencyCode: currencyCode}, + ); err != nil { + return nil, err + } + + // Make the HTTP request + response := httpRequest( + ctx, + c, + &httpPayload{ + Data: params, + ExpectedStatus: http.StatusOK, + Method: signed.Method, + URL: signed.URI, + }, + signed, + ) + + if response.Error != nil { + return nil, response.Error + } + + spendableBalanceResponse = new(SpendableBalanceResponse) + + if err = json.Unmarshal(response.BodyContents, &spendableBalanceResponse); err != nil { + return nil, fmt.Errorf("failed unmarshal %w", err) + } else if spendableBalanceResponse == nil || spendableBalanceResponse.CurrencyCode == "" { + return nil, fmt.Errorf("failed to get balance") + } + + return spendableBalanceResponse, nil +} diff --git a/spendable_balance_test.go b/spendable_balance_test.go new file mode 100644 index 0000000..aac7cf8 --- /dev/null +++ b/spendable_balance_test.go @@ -0,0 +1,116 @@ +package handcash + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +// mockHTTPGetSpendableBalance for mocking requests +type mockHTTPGetSpendableBalance struct{} + +// Do is a mock http request +func (m *mockHTTPGetSpendableBalance) Do(req *http.Request) (*http.Response, error) { + resp := new(http.Response) + + // No req found + if req == nil { + return resp, fmt.Errorf("missing request") + } + + // Beta + if req.URL.String() == environments[EnvironmentBeta].APIURL+endpointGetSpendableBalanceRequest { + resp.StatusCode = http.StatusOK + resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(`{"spendableSatoshiBalance":1424992,"spendableFiatBalance":2.7792,"currencyCode":"USD"}`))) + } + + // Default is valid + return resp, nil +} + +// mockHTTPInvalidSpendableBalanceData for mocking requests +type mockHTTPInvalidSpendableBalanceData struct{} + +// Do is a mock http request +func (m *mockHTTPInvalidSpendableBalanceData) Do(req *http.Request) (*http.Response, error) { + resp := new(http.Response) + + // No req found + if req == nil { + return resp, fmt.Errorf("missing request") + } + + resp.StatusCode = http.StatusOK + resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(`{"invalid":"currencyCode"}`))) + + // Default is valid + return resp, nil +} + +func TestClient_GetSpendableBalance(t *testing.T) { + t.Parallel() + + t.Run("missing auth token", func(t *testing.T) { + client := newTestClient(&mockHTTPGetSpendableBalance{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "", "") + assert.Error(t, err) + assert.Nil(t, balance) + }) + + t.Run("missing currency code", func(t *testing.T) { + client := newTestClient(&mockHTTPGetSpendableBalance{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "000000", "") + assert.Error(t, err) + assert.Nil(t, balance) + }) + + t.Run("invalid auth token", func(t *testing.T) { + client := newTestClient(&mockHTTPGetSpendableBalance{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "0", "USD") + assert.Error(t, err) + assert.Nil(t, balance) + }) + + t.Run("invalid currency code", func(t *testing.T) { + client := newTestClient(&mockHTTPGetSpendableBalance{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "0", "FOO") + assert.Error(t, err) + assert.Nil(t, balance) + }) + + t.Run("bad request", func(t *testing.T) { + client := newTestClient(&mockHTTPBadRequest{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "000000", "USD") + assert.Error(t, err) + assert.Nil(t, balance) + }) + + t.Run("invalid spendable balance data", func(t *testing.T) { + client := newTestClient(&mockHTTPInvalidSpendableBalanceData{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "000000", "USD") + assert.Error(t, err) + assert.Nil(t, balance) + }) + + t.Run("valid spendable balance response", func(t *testing.T) { + client := newTestClient(&mockHTTPGetSpendableBalance{}, EnvironmentBeta) + assert.NotNil(t, client) + balance, err := client.GetSpendableBalance(context.Background(), "000000", "USD") + assert.NoError(t, err) + assert.NotNil(t, balance) + assert.Equal(t, CurrencyUSD, balance.CurrencyCode) + assert.Equal(t, uint64(1424992), balance.SpendableSatoshiBalance) + assert.Equal(t, float64(2.7792), balance.SpendableFiatBalance) + }) +}