Golang REST API that fetches Solana balances, Docker + Mongo + tests.
| Item | Spec |
|---|---|
| Endpoint | POST /api/get-balance |
| Body | { "wallets": [ARRAY_OF_SOLANA_PUBKEYS] } |
| External RPC | https://helius-rpc.com/?api-key=… |
| Must | • Mongo‑backed API‑key auth |
• IP‑rate‑limit > 10 req/min
• Per‑wallet cache (TTL 10 s)
• Mutex: 2 concurrent hits → 1 RPC call
• Respond “as fast as possible” |
├── go.mod
├── Dockerfile
├── docker-compose.yml
├── main.go # starts Gin HTTP server
├── internal/ # cohesive packages
│ ├── cache.go # TTL cache + per‑wallet mutex
│ ├── limiter.go # IP rate‑limiting middleware
│ ├── mongo.go # API‑key middleware (Mongo)
│ ├── solana.go # RPC helper using gagliardetto/solana-go
│ └── handler.go # POST /api/get-balance logic
└── solana_api_tests/ # complete Python functional test‑suite
# ---------- build stage ----------
FROM golang:1.24.4 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/solana-api ./main.go
# ---------- runtime stage ----------
FROM gcr.io/distroless/base-debian12
COPY --from=builder /bin/solana-api /solana-api
EXPOSE 8080
ENTRYPOINT ["/solana-api"]services:
mongo:
image: mongo:7
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: secret
volumes:
- mongo-data:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
ports:
- "27017:27017"
api:
build: .
depends_on:
- mongo
environment:
SOLANA_RPC: https://helius-rpc.com/?api-key=
MONGO_URI: mongodb://admin:secret@mongo:27017/infra?authSource=admin
MONGO_DB: infra
PORT: 8080
ports:
- "8080:8080"
restart: unless-stopped
volumes:
mongo-data:mongo-init.js automatically inserts:
db = db.getSiblingDB('infra');
db.apikeys.insertOne({ api_key: 'demo-key-123', active: true });docker compose up --build -d
# API → http://localhost:8080
# Mongo → mongodb://admin:secret@localhost:27017curl -XPOST http://localhost:8080/api/get-balance \
-H 'Content-Type: application/json' \
-H 'X-API-Key: demo-key-123' \
-d '{"wallets":["9yg3qP8XqWBzhiyh9frehBmpbZ1S9X4yJ3YwCRxN5ZMx"]}'Response (lamports):
{
"balances": {
"9yg3qP8…": 763982739
}
}Convert lamports ÷ 1 000 000 000 for SOL.
Path: solana_api_tests/
cd solana_api_tests
pip install -r requirements.txt
# short run (skips 60‑s rate‑limit check)
python run_tests.py --base http://localhost:8080 --key demo-key-123 --skip-rate
# full run (≈80 s, includes rate‑limit validation)
python run_tests.py --base http://localhost:8080 --key demo-key-123- Auth – bad key ⇒ 401, good key ⇒ 200
- Single wallet – simple happy path
- Multiple wallets – parallel fetch
- Cache TTL 10 s – miss → hit → expire → miss pattern
- Mutex collapse – adapts to limiter burst (1 … 10) and still proves only the first request hits RPC
- Rate‑limit – adapts to burst and confirms 11ᵗʰ request within 60 s ⇒ 429
All steps log ISO‑timestamps.
| Concern | Implementation |
|---|---|
| Rate‑limiting | limiter.go uses golang.org/x/time/rate. NewLimiterCache(max=10, per=1 min) so refill = 6 s. Burst defaults to max but can be set to 1 for stricter ops. |
| Caching + mutex | cache.go holds map[string]*cacheEntry and a per‑wallet sync.Mutex in a sync.Map ensuring only one goroutine per wallet performs the RPC. TTL = 10 s. |
| Mongo auth | Middleware checks X-API-Key against apikeys collection (active:true). |
| RPC | Thin wrapper around gagliardetto/solana-go/rpc’s GetBalance. |
| Parameter | Default | How to change |
|---|---|---|
| Rate‑limit window | 10 req / 1 min / IP | NewLimiterCache(max, per) in main.go |
| Burst size | = max (10) |
change 2ᵈ arg in rate.NewLimiter |
| Cache TTL | 10 s | NewWalletCache(…) |
| RPC timeout | 4 s | context.WithTimeout in solana.go |