Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
check-latest: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: v2.11.4

Expand Down
10 changes: 10 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ linters:
- errcheck
- gosec
- revive
# Example functions in external test packages need an explicit
# `var svc *pkg.Service = client.Field` declaration so `go vet`
# can resolve the Service identifier and attach
# ExampleService_Method to the right method on pkg.go.dev.
# staticcheck flags the annotation as redundant (ST1023); the
# redundancy is intentional here.
- path: example_test\.go
linters:
- staticcheck
text: "(ST1023|QF1011):"

formatters:
enable:
Expand Down
15 changes: 15 additions & 0 deletions services/accountdetails/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (

const getPath = "/api/public/v1/account-details"

// Well-known country and currency codes accepted by the API. These are
// plain string constants because the endpoint may add new values over time;
// they exist purely as a documented starting point.
const (
CountryUS = "US"
CountryEU = "EU"
Expand All @@ -18,19 +21,25 @@ const (
CurrencyEUR = "EUR"
)

// Service issues requests against the Wallbit account-details endpoint.
type Service struct {
sender transport.Sender
}

// NewService wires a [Service] to the given [transport.Sender].
func NewService(sender transport.Sender) *Service {
return &Service{sender: sender}
}

// GetRequest parameterises a call to [Service.Get]. Both fields are
// optional; when blank, the server returns the account details configured
// as default for the authenticated user.
type GetRequest struct {
Country string
Currency string
}

// AccountAddress is the postal address attached to a set of [AccountDetails].
type AccountAddress struct {
StreetLine1 string `json:"street_line_1"`
StreetLine2 *string `json:"street_line_2,omitempty"`
Expand All @@ -40,6 +49,9 @@ type AccountAddress struct {
Country string `json:"country"`
}

// AccountDetails describes the bank account configured for the caller to
// deposit or withdraw fiat. Optional fields (IBAN, BIC, routing number, …)
// are nil when the destination bank network does not use them.
type AccountDetails struct {
BankName string `json:"bank_name"`
Currency string `json:"currency"`
Expand All @@ -55,10 +67,13 @@ type AccountDetails struct {
Address *AccountAddress `json:"address,omitempty"`
}

// GetResponse is the top-level envelope for [Service.Get].
type GetResponse struct {
Data AccountDetails `json:"data"`
}

// Get returns the bank account details the user should use to fund or
// withdraw from their Wallbit account. A nil req uses server defaults.
func (s *Service) Get(ctx context.Context, req *GetRequest) (*transport.Response[GetResponse], error) {
path := getPath
if req != nil {
Expand Down
6 changes: 6 additions & 0 deletions services/apikey/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ import (

const revokePath = "/api/public/v1/api-key"

// Service issues requests against the Wallbit API-key endpoint.
type Service struct {
sender transport.Sender
}

// NewService wires a [Service] to the given [transport.Sender].
func NewService(sender transport.Sender) *Service {
return &Service{sender: sender}
}

// RevokeResponse is the top-level envelope for [Service.Revoke].
type RevokeResponse struct {
Message string `json:"message"`
}

// Revoke invalidates the API key carried by the client. Subsequent calls
// with the same key will fail authentication; issue a new key out-of-band
// to keep using the SDK.
func (s *Service) Revoke(ctx context.Context) (*transport.Response[RevokeResponse], error) {
return transport.SendJSON(ctx, s.sender, http.MethodDelete, revokePath, nil, &RevokeResponse{})
}
19 changes: 19 additions & 0 deletions services/assets/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,38 @@ const listPath = "/api/public/v1/assets"
// ErrEmptySymbol is returned by [Service.Get] when symbol is empty or whitespace-only.
var ErrEmptySymbol = errors.New("assets: symbol is required")

// Service issues requests against the Wallbit assets endpoints.
type Service struct {
sender transport.Sender
}

// NewService wires a [Service] to the given [transport.Sender].
func NewService(sender transport.Sender) *Service {
return &Service{sender: sender}
}

// ListRequest parameterises a call to [Service.List]. Every field is
// optional; unset values are omitted from the query string. Use
// [github.com/jeremyjsx/wallbit-go/wallbit.Ptr] to set Page and Limit.
type ListRequest struct {
Category string
Search string
Page *int
Limit *int
}

// Dividend captures the most recent dividend event announced for an asset,
// when available. All fields are nil when the instrument does not pay
// dividends or when the API has not published the event yet.
type Dividend struct {
Amount *float64 `json:"amount"`
Yield *float64 `json:"yield"`
ExDate *string `json:"ex_date"`
PaymentDate *string `json:"payment_date"`
}

// Asset is a tradable instrument row. Pointer fields carry the server's
// own omitempty: they are nil whenever the API would omit them.
type Asset struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Expand All @@ -57,17 +67,23 @@ type Asset struct {
Dividend *Dividend `json:"dividend"`
}

// ListResponse is the top-level envelope for [Service.List]. CurrentPage
// equals Pages on the last page; Count is the total number of rows across
// every page.
type ListResponse struct {
Data []Asset `json:"data"`
Pages int `json:"pages"`
CurrentPage int `json:"current_page"`
Count int `json:"count"`
}

// GetResponse is the top-level envelope for [Service.Get].
type GetResponse struct {
Data Asset `json:"data"`
}

// Get fetches a single asset by its ticker symbol. An empty symbol returns
// [ErrEmptySymbol].
func (s *Service) Get(ctx context.Context, symbol string) (*transport.Response[GetResponse], error) {
if strings.TrimSpace(symbol) == "" {
return nil, ErrEmptySymbol
Expand All @@ -76,6 +92,9 @@ func (s *Service) Get(ctx context.Context, symbol string) (*transport.Response[G
return transport.SendJSON(ctx, s.sender, http.MethodGet, path, nil, &GetResponse{})
}

// List fetches a single page of assets matching req's filters. A nil req
// returns the first page with server defaults. For lazy iteration over
// every page, use [Service.ListAll].
func (s *Service) List(ctx context.Context, req *ListRequest) (*transport.Response[ListResponse], error) {
path := listPath
if req != nil {
Expand Down
10 changes: 10 additions & 0 deletions services/balance/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,46 @@ const (
stocksPath = "/api/public/v1/balance/stocks"
)

// Service issues requests against the Wallbit balance endpoints.
type Service struct {
sender transport.Sender
}

// NewService wires a [Service] to the given [transport.Sender].
func NewService(sender transport.Sender) *Service {
return &Service{sender: sender}
}

// CheckingBalance is the balance of a single fiat currency held in the
// user's checking account.
type CheckingBalance struct {
Currency string `json:"currency"`
Balance float64 `json:"balance"`
}

// CheckingBalanceResponse is the top-level envelope for [Service.GetChecking].
type CheckingBalanceResponse struct {
Data []CheckingBalance `json:"data"`
}

// StockPosition is a single equity holding in the user's stocks account.
type StockPosition struct {
Symbol string `json:"symbol"`
Shares float64 `json:"shares"`
}

// StocksBalanceResponse is the top-level envelope for [Service.GetStocks].
type StocksBalanceResponse struct {
Data []StockPosition `json:"data"`
}

// GetChecking returns every fiat balance (checking account) held by the
// authenticated user.
func (s *Service) GetChecking(ctx context.Context) (*transport.Response[CheckingBalanceResponse], error) {
return transport.SendJSON(ctx, s.sender, http.MethodGet, checkingPath, nil, &CheckingBalanceResponse{})
}

// GetStocks returns every equity position held by the authenticated user.
func (s *Service) GetStocks(ctx context.Context) (*transport.Response[StocksBalanceResponse], error) {
return transport.SendJSON(ctx, s.sender, http.MethodGet, stocksPath, nil, &StocksBalanceResponse{})
}
15 changes: 15 additions & 0 deletions services/cards/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
const listPath = "/api/public/v1/cards"
const updateStatusPathFormat = "/api/public/v1/cards/%s/status"

// Card status values accepted by the update-status endpoint.
const (
StatusActive = "ACTIVE"
StatusSuspended = "SUSPENDED"
Expand All @@ -21,10 +22,12 @@ const (
// ErrEmptyCardUUID is returned by [Service.Block] and [Service.Unblock] when cardUUID is empty or whitespace-only.
var ErrEmptyCardUUID = errors.New("cards: card uuid is required")

// Service issues requests against the Wallbit cards endpoints.
type Service struct {
sender transport.Sender
}

// NewService wires a [Service] to the given [transport.Sender].
func NewService(sender transport.Sender) *Service {
return &Service{sender: sender}
}
Expand All @@ -33,11 +36,14 @@ type updateStatusRequest struct {
Status string `json:"status"`
}

// CardStatus is the minimal card row returned by the update-status endpoint.
type CardStatus struct {
UUID string `json:"uuid"`
Status string `json:"status"`
}

// Card describes a card row as returned by [Service.List]. Expiration is
// nil when the API does not expose it.
type Card struct {
UUID string `json:"uuid"`
Status string `json:"status"`
Expand All @@ -47,22 +53,31 @@ type Card struct {
Expiration *string `json:"expiration"`
}

// ListResponse is the top-level envelope for [Service.List].
type ListResponse struct {
Data []Card `json:"data"`
}

// UpdateStatusResponse is the top-level envelope for [Service.Block] and
// [Service.Unblock].
type UpdateStatusResponse struct {
Data CardStatus `json:"data"`
}

// List returns every card visible to the authenticated user, regardless of
// status.
func (s *Service) List(ctx context.Context) (*transport.Response[ListResponse], error) {
return transport.SendJSON(ctx, s.sender, http.MethodGet, listPath, nil, &ListResponse{})
}

// Block suspends the card identified by cardUUID. It returns
// [ErrEmptyCardUUID] if cardUUID is empty or whitespace-only.
func (s *Service) Block(ctx context.Context, cardUUID string) (*transport.Response[UpdateStatusResponse], error) {
return s.updateStatus(ctx, cardUUID, StatusSuspended)
}

// Unblock re-activates the card identified by cardUUID. It returns
// [ErrEmptyCardUUID] if cardUUID is empty or whitespace-only.
func (s *Service) Unblock(ctx context.Context, cardUUID string) (*transport.Response[UpdateStatusResponse], error) {
return s.updateStatus(ctx, cardUUID, StatusActive)
}
Expand Down
16 changes: 16 additions & 0 deletions services/fees/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,42 @@ import (

const getPath = "/api/public/v1/fees"

// Service issues requests against the Wallbit fees endpoint.
type Service struct {
sender transport.Sender
}

// NewService wires a [Service] to the given [transport.Sender].
func NewService(sender transport.Sender) *Service {
return &Service{sender: sender}
}

// GetRequest selects which fee schedule to fetch. Type is the fee type key
// documented by the API (e.g. "card_issuance", "withdrawal_local").
type GetRequest struct {
Type string `json:"type"`
}

// FeeSetting is the single fee row returned by the API. PercentageFee and
// FixedFeeUSD are kept as strings because the API serializes them that way
// (they are decimals and preserving the server-side precision matters).
type FeeSetting struct {
FeeType string `json:"fee_type"`
Tier *string `json:"tier"`
PercentageFee string `json:"percentage_fee"`
FixedFeeUSD string `json:"fixed_fee_usd"`
}

// GetData is the union payload for the fees endpoint. The API returns either
// an object (a concrete [FeeSetting], decoded into Row) or an empty array
// (Empty == true) when no rule applies to the requested fee type.
type GetData struct {
Row *FeeSetting
Empty bool
}

// UnmarshalJSON decodes either a FeeSetting object or an empty JSON array
// into the receiver, rejecting non-empty arrays and any other shape.
func (d *GetData) UnmarshalJSON(b []byte) error {
*d = GetData{}
b = bytes.TrimSpace(b)
Expand Down Expand Up @@ -66,10 +78,14 @@ func (d *GetData) UnmarshalJSON(b []byte) error {
}
}

// GetResponse is the top-level envelope for [Service.Get].
type GetResponse struct {
Data GetData `json:"data"`
}

// Get fetches the fee schedule for req.Type. The response's Data may be
// either a concrete [FeeSetting] (when a rule exists) or an empty payload
// (when none does); consult [GetData] for the discriminator.
func (s *Service) Get(ctx context.Context, req GetRequest) (*transport.Response[GetResponse], error) {
return transport.SendJSON(ctx, s.sender, http.MethodPost, getPath, req, &GetResponse{})
}
Loading
Loading