Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0f2fa3c
feat(transport): add Response[T] wrapper and return metadata from Sender
jeremyjsx Apr 20, 2026
25f68e3
ci: add github actions workflow and golangci lint config
jeremyjsx Apr 20, 2026
66e6936
feat(wallbit): jitter retry backoff and inject SDK version into User-…
jeremyjsx Apr 20, 2026
ed84f27
feat(wallbit): bound response body size with io.LimitReader
jeremyjsx Apr 20, 2026
8acfb72
feat(wallbit): add Ptr helper, Attempt field on hook meta, and for-ra…
jeremyjsx Apr 20, 2026
1f3f64f
refactor(transport): centralize JSON encode/decode in SendJSON helper
jeremyjsx Apr 20, 2026
464337b
refactor(wallbit): extract hook and decode helpers from Client.do
jeremyjsx Apr 20, 2026
34d9191
test: add Example funcs per package and ErrorFromHTTP fuzz
jeremyjsx Apr 20, 2026
d259c97
docs(wallbit): document default timeout, retry, and body-size values
jeremyjsx Apr 20, 2026
b422bcb
feat!: parse CreatedAt and UpdatedAt into time.Time on all services
jeremyjsx Apr 20, 2026
f064448
feat: add rates service for Get Exchange Rate endpoint
jeremyjsx Apr 20, 2026
dff2b73
fix!: unwrap operations.Internal response to match API shape
jeremyjsx Apr 20, 2026
0d7ae43
feat: add ListAll iter.Seq2 pagination to transactions and assets
jeremyjsx Apr 20, 2026
aec6de0
feat: add SlogHook adapter for structured request logging
jeremyjsx Apr 20, 2026
2cc776e
docs: add CHANGELOG and Makefile, refresh README coverage
jeremyjsx Apr 20, 2026
2bf44c0
chore: add CODEOWNERS
jeremyjsx Apr 20, 2026
23d0b04
chore(changelog): set release date for v0.1.0-beta.1
jeremyjsx Apr 20, 2026
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
9 changes: 9 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# CODEOWNERS for wallbit-go.
#
# Every pull request is automatically reviewed by the repository
# maintainer. Order matters: the last matching pattern wins, so
# more specific paths below would override the catch-all above.
#
# Docs: https://docs.github.com/en/repositories/managing-your-repositories-settings-and-features/customizing-your-repository/about-code-owners

* @jeremyjsx
86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
push:
branches: [main, "release/**"]
pull_request:
branches: [main, "release/**"]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test (Go ${{ matrix.go }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go: ["1.23", "stable"]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
check-latest: true

- name: Download modules
run: go mod download

- name: Verify modules
run: go mod verify

- name: go vet
run: go vet ./...

- name: go test -race
run: go test -race -count=1 ./...

lint:
name: golangci-lint
runs-on: ubuntu-latest

permissions:
contents: read
pull-requests: read
checks: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
check-latest: true

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

vuln:
name: govulncheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
check-latest: true

- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest

- name: Run govulncheck
run: govulncheck ./...
58 changes: 58 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
version: "2"

run:
go: "1.23"

linters:
default: none
enable:
- errcheck
- govet
- staticcheck
- revive
- gosec
- ineffassign
- unused
- misspell
- unconvert
- nolintlint
settings:
errcheck:
exclude-functions:
- (io.Closer).Close
- fmt.Fprint
- fmt.Fprintln
- fmt.Fprintf
revive:
rules:
- name: exported
disabled: false
arguments:
- "disableStutteringCheck"
- name: package-comments
- name: var-naming
- name: indent-error-flow
- name: errorf
- name: context-as-argument
- name: context-keys-type
gosec:
excludes:
# G104 (errors unhandled) is already covered by errcheck with a
# better-tuned allowlist.
- G104
exclusions:
rules:
- path: _test\.go
linters:
- errcheck
- gosec
- revive

formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/jeremyjsx/wallbit-go
89 changes: 89 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

Nothing yet — changes land here first and graduate to a versioned
section at release time.

## [0.1.0-beta.1] — 2026-04-20

First public release.

### Added

- `wallbit.Client`: single HTTP client that exposes the Wallbit API as
per-resource service handles. Build with `wallbit.NewClient(apiKey, opts...)`
or `wallbit.NewClientFromConfig(apiKey, cfg)` when configuration
comes from a struct (env loader, DI container, etc.).
- Functional options: `WithBaseURL`, `WithHTTPClient`, `WithTimeout`,
`WithUserAgent`, `WithRetryPolicy`, `WithMaxResponseBytes`,
`WithHook`, `WithInsecureHTTPForTesting`.
- Service handles on `*wallbit.Client`: `Balance`, `Transactions`,
`Trades`, `Fees`, `Wallets`, `Assets`, `Operations`, `RoboAdvisor`,
`Cards`, `AccountDetails`, `APIKey`, `Rates`.
- `transport.Response[T]` generic wrapper pairing the decoded payload
(`Payload`) with the HTTP envelope (`StatusCode`, `Header`,
`RequestID`).
- `transport.Sender` interface for injecting custom HTTP transports
and `transport.SendJSON[T]` generic helper that every service uses
internally.
- `wallbit.Error` with typed fields (`StatusCode`, `Code`, `Message`,
`Details`, `RequestID`, `RetryAfter`) and predicates
`IsNotFound`, `IsAuthError`, `IsRateLimit`, `IsValidationError`,
`IsServerError`, `IsRetryable`. `ErrorFromHTTP` is fuzz-tested for
resilience against malformed upstream bodies.
- Retry loop with equal-jitter exponential backoff, `Retry-After`
honoring and attempt counting exposed to hooks. Default policy:
3 attempts, 250ms base delay, 2s cap. Idempotent methods (`GET`,
`HEAD`, `DELETE`, `OPTIONS`, `TRACE`) are retried on transport
errors; `429` and `5xx` are retried regardless of method.
- SDK version injected into the `User-Agent` at build time via
`-ldflags "-X github.com/jeremyjsx/wallbit-go/wallbit.Version=..."`,
with `runtime/debug.ReadBuildInfo` and `"dev"` fallbacks.
- `wallbit.Hook` interface with `RequestMeta` / `ResponseMeta`
carrying `Method`, `Path`, `Attempt`, `StatusCode` and `Duration`
for every HTTP attempt (retries included).
- `wallbit.SlogHook`: adapter that wires the hook to a
`*log/slog.Logger`, emitting one record per attempt with
`method`, `path`, `attempt`, `status` and `duration_ms`. Levels:
Debug on start, Info/Warn/Error on done based on status.
- `wallbit.Ptr[T any](v T) *T` helper for optional fields in request
bodies so call-sites read `wallbit.Ptr("value")` alongside the
client.
- Go 1.23 pagination: `transactions.ListAll` and `assets.ListAll`
return `iter.Seq2[T, error]` and walk every page lazily.
- Timestamp fields (`CreatedAt`, `UpdatedAt`) typed as `time.Time`
across services that expose them (`trades`, `transactions`,
`operations`, `roboadvisor`, `rates`). Invalid timestamps surface
as JSON decode errors instead of silently passing through.
- Nullable API fields consistently typed as pointers (`*string`,
`*float64`, …) so `nil` is distinguishable from "explicitly zero".
- Error `details` preserved as raw JSON so callers can re-decode the
server-specific payload without losing information.
- Input validation before issuing the HTTP call for paths that take a
required segment (asset symbol, card UUID, currency pair), with
typed sentinel errors (`ErrEmptySymbol`, `ErrEmptyCurrency`, …).
- `Example*` functions in every package so `pkg.go.dev` renders
runnable usage for each service method.

### Security

- HTTPS is required by default. Non-HTTPS base URLs are rejected with
`ErrInsecureBaseURL` unless `WithInsecureHTTPForTesting` is set,
preventing accidental plaintext transmission of the API key.
- Cross-host redirects are blocked by a `CheckRedirect` hook on the
cloned `*http.Client` so a hostile or misconfigured redirect cannot
exfiltrate the `X-API-Key` header to a foreign host.
- Response bodies are read through `io.LimitReader` with a default
cap of 10 MiB (`DefaultMaxResponseBytes`). Over-sized responses
return `ErrResponseTooLarge` instead of a partial payload so a
hostile or buggy upstream cannot exhaust process memory. Override
with `WithMaxResponseBytes`.

[Unreleased]: https://github.com/jeremyjsx/wallbit-go/compare/v0.1.0-beta.1...HEAD
[0.1.0-beta.1]: https://github.com/jeremyjsx/wallbit-go/releases/tag/v0.1.0-beta.1
82 changes: 82 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Makefile for wallbit-go.
#
# Assumes GNU make and a POSIX shell. On Windows, run from Git Bash or WSL.
# Run `make` or `make help` for the list of available targets.

GO ?= go
GOLANGCI_LINT_VERSION ?= v2.11.4
GOVULNCHECK_VERSION ?= latest
COVERFILE ?= coverage.out

.DEFAULT_GOAL := help

.PHONY: help
help: ## Show this help
@echo ""
@echo "Usage: make <target>"
@echo ""
@echo "Targets:"
@echo " test Run unit tests (fast, no race detector)"
@echo " test-race Run unit tests with the race detector (matches CI)"
@echo " vet Run go vet"
@echo " fmt Format code with gofmt"
@echo " lint Run golangci-lint"
@echo " tidy Run go mod tidy and verify"
@echo " vuln Run govulncheck against all packages"
@echo " cover Generate HTML coverage report (coverage.html)"
@echo " fuzz Run fuzz targets briefly (10s each)"
@echo " check Full pre-PR check (vet + lint + test-race + vuln)"
@echo " install-tools Install pinned golangci-lint and govulncheck"
@echo " clean Remove generated artifacts"
@echo ""

.PHONY: test
test: ## Run unit tests (fast, no race detector)
$(GO) test -count=1 ./...

.PHONY: test-race
test-race: ## Run unit tests with the race detector (matches CI)
$(GO) test -race -count=1 ./...

.PHONY: vet
vet: ## Run go vet
$(GO) vet ./...

.PHONY: fmt
fmt: ## Format code with gofmt
$(GO) fmt ./...

.PHONY: lint
lint: ## Run golangci-lint (requires `make install-tools` first)
golangci-lint run ./...

.PHONY: tidy
tidy: ## Run go mod tidy and verify
$(GO) mod tidy
$(GO) mod verify

.PHONY: vuln
vuln: ## Run govulncheck against all packages
$(GO) run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./...

.PHONY: cover
cover: ## Generate HTML coverage report (coverage.html)
$(GO) test -race -coverprofile=$(COVERFILE) ./...
$(GO) tool cover -html=$(COVERFILE) -o coverage.html
@echo "Coverage report written to coverage.html"

.PHONY: fuzz
fuzz: ## Run fuzz targets briefly (10s each)
$(GO) test -run=^$$ -fuzz=FuzzErrorFromHTTP -fuzztime=10s ./wallbit/...

.PHONY: check
check: vet lint test-race vuln ## Full pre-PR check (mirrors CI)

.PHONY: install-tools
install-tools: ## Install pinned versions of golangci-lint and govulncheck
$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
$(GO) install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)

.PHONY: clean
clean: ## Remove generated artifacts
@rm -f $(COVERFILE) coverage.html
Loading
Loading