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
28 changes: 28 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Test environment for CI contract smoke tests (.github/workflows/api-contract.yml).
# Non-secret stubs only — must satisfy src/config/env.ts format validation.
NODE_ENV=test
PORT=3000

DATABASE_URL=postgresql://user:pass@localhost:5432/db

STELLAR_NETWORK=testnet
STELLAR_RPC_URL=https://rpc.example.com
STELLAR_AGENT_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
VAULT_CONTRACT_ID=CDUMMYVAULTCONTRACTID
USDC_TOKEN_ADDRESS=CDUMMYUSDC

ANTHROPIC_API_KEY=sk-ant-smoke-key

WALLET_ENCRYPTION_KEY=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

JWT_SEED=smoke-jwt-seed-0123456789abcdef0123456789
JWT_SESSION_TTL_HOURS=24
JWT_NONCE_TTL_MS=300000
JWT_CLEANUP_INTERVAL_MS=86400000

TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
INTERNAL_SERVICE_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

TRUSTED_IPS=127.0.0.1
CORS_ORIGINS=*
HTTP_CLIENT_TIMEOUT_MS=1000
56 changes: 50 additions & 6 deletions .github/workflows/api-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'

- name: Install Redocly CLI
run: npm install -g @redocly/cli@latest
Expand All @@ -36,30 +36,74 @@ jobs:
name: Contract smoke tests
runs-on: ubuntu-latest
needs: validate-spec
services:
postgres:
image: postgres:14.4
env:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: db
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U user"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Prisma generate
run: npx prisma generate
env:
DATABASE_URL: postgresql://user:pass@localhost:5432/db

- name: Prisma migrate deploy
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://user:pass@localhost:5432/db

- name: Start server
run: |
cp .env.test .env
npm run build
node dist/index.js &
# Wait up to 15 seconds for the server to accept connections
for i in $(seq 1 15); do
curl -sf http://localhost:3000/health && break
node dist/index.js > /tmp/server.log 2>&1 &
echo $! > /tmp/server.pid
# initServices() boots the DB, event listener, and agent loop before
# app.listen(), so allow generous time — and fail loudly with the
# server log rather than letting a later step report a bare failure.
for i in $(seq 1 60); do
if curl -sf http://localhost:3000/health > /dev/null 2>&1; then
echo "Server ready after ${i}s"
exit 0
fi
if ! kill -0 "$(cat /tmp/server.pid)" 2>/dev/null; then
echo "::error::Server process exited during startup"
cat /tmp/server.log
exit 1
fi
sleep 1
done
echo "::error::Server did not become ready within 60s"
cat /tmp/server.log
exit 1
env:
NODE_ENV: test
# This job boots the real server, and startEventListener() calls
# getLatestLedger() with no client timeout before app.listen(). Point
# it at a reachable RPC (as production-smoke.yml does) so an
# unresolvable host cannot stall startup. dotenv does not override
# ambient vars, so this wins over .env.test.
STELLAR_RPC_URL: https://soroban-testnet.stellar.org

- name: Health liveness check
run: |
Expand Down
66 changes: 35 additions & 31 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ jobs:
STELLAR_AGENT_SECRET_KEY: SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
VAULT_CONTRACT_ID: CDUMMYVAULTCONTRACTID
USDC_TOKEN_ADDRESS: CDUMMYUSDC
ANTHROPIC_API_KEY: smoke-anthropic-key
JWT_SEED: smoke-jwt-seed
ANTHROPIC_API_KEY: sk-ant-smoke-key
JWT_SEED: smoke-jwt-seed-0123456789abcdef0123456789
JWT_SESSION_TTL_HOURS: '24'
JWT_NONCE_TTL_MS: '300000'
JWT_CLEANUP_INTERVAL_MS: '86400000'
Expand All @@ -55,7 +55,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

# Install dependencies
Expand Down Expand Up @@ -103,7 +103,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

- name: Install dependencies
Expand All @@ -115,10 +115,13 @@ jobs:

- name: License check (block GPL/AGPL/LGPL)
# Prevents copyleft licences from entering the dependency tree.
# Allowed: MIT, ISC, Apache-2.0, BSD-2-Clause, BSD-3-Clause, 0BSD, BlueOak-1.0.0, CC0-1.0
# Allowed: MIT, ISC, Apache-2.0, BSD-2-Clause, BSD-3-Clause, 0BSD, BlueOak-1.0.0, CC0-1.0,
# Python-2.0 (PSF — permissive, transitive argparse@2),
# CC-BY-4.0 (attribution-only, transitive caniuse-lite data),
# Unlicense (public-domain dedication, transitive fast-sha256)
run: |
npx license-checker --onlyAllow \
'MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause;0BSD;BlueOak-1.0.0;CC0-1.0' \
'MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause;0BSD;BlueOak-1.0.0;CC0-1.0;Python-2.0;CC-BY-4.0;Unlicense' \
--excludePrivatePackages

# ── Issue #100: migration smoke gate ──────────────────────────────────────
Expand All @@ -145,19 +148,26 @@ jobs:
--health-retries=5
env:
NODE_ENV: test
# scripts/smoke-health.sh probes port 3001 — the server must listen there.
PORT: '3001'
DATABASE_URL: postgresql://smoke_user:smoke_pass@localhost:5433/smoke_db
# Non-secret stubs required by env.ts at module-load time
STELLAR_NETWORK: testnet
STELLAR_RPC_URL: https://rpc.example.com
# This job boots the real server; startEventListener() calls
# getLatestLedger() with no client timeout before app.listen(), so the
# endpoint must resolve or startup stalls.
STELLAR_RPC_URL: https://soroban-testnet.stellar.org
STELLAR_AGENT_SECRET_KEY: SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
VAULT_CONTRACT_ID: CDUMMYVAULTCONTRACTID
USDC_TOKEN_ADDRESS: CDUMMYUSDC
ANTHROPIC_API_KEY: smoke-anthropic-key
JWT_SEED: smoke-jwt-seed
ANTHROPIC_API_KEY: sk-ant-smoke-key
JWT_SEED: smoke-jwt-seed-0123456789abcdef0123456789
JWT_SESSION_TTL_HOURS: '24'
JWT_NONCE_TTL_MS: '300000'
JWT_CLEANUP_INTERVAL_MS: '86400000'
WALLET_ENCRYPTION_KEY: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
# Required by env.ts and asserted again by initServices().
TWILIO_AUTH_TOKEN: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

steps:
- name: Checkout repository
Expand All @@ -166,7 +176,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

- name: Install dependencies
Expand All @@ -175,8 +185,11 @@ jobs:
- name: Prisma generate
run: npx prisma generate

- name: Check migration status (staging gate)
run: npx prisma migrate status
# Informational only: on a fresh smoke database every migration is
# pending, and `migrate status` exits non-zero whenever that is true.
# The real gate is "Confirm no pending migrations after deploy" below.
- name: Check migration status (pre-deploy, informational)
run: npx prisma migrate status || true

- name: Apply migrations to smoke DB
run: npx prisma migrate deploy
Expand All @@ -189,6 +202,10 @@ jobs:
exit 1
fi

# smoke-health.sh runs the compiled server, so the artifact must exist.
- name: Build project
run: npm run build

- name: Run smoke test
run: npm run smoke

Expand All @@ -205,7 +222,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'

- name: Install markdown-link-check
run: npm install -g markdown-link-check
Expand All @@ -229,31 +246,18 @@ jobs:
for file in docs/*.md; do
if [ -f "$file" ]; then
# Count opening and closing code fences
open=$(grep -c '^\`\`\`' "$file" || true)
open=$(grep -c '^```' "$file" || true)
if [ $((open % 2)) -ne 0 ]; then
echo "::error file=$file::Unclosed code fence detected"
exit 1
fi
fi
done

- name: Install kubectl
uses: azure/setup-kubectl@v4
with:
version: 'v1.29.0'

- name: Validate Kubernetes manifests
run: |
# Validate all YAML manifests in deploy/k8s
for manifest in deploy/k8s/*.yaml; do
if [ -f "$manifest" ]; then
echo "Validating $manifest"
kubectl apply --dry-run=client -f "$manifest" || {
echo "::error file=$manifest::Kubernetes manifest validation failed"
exit 1
}
fi
done
# Kubernetes manifest validation lives in k8s-validate.yml (kubeconform,
# fully offline) and triggers whenever deploy/k8s changes. kubectl's
# client dry-run needs a live cluster for API discovery, so it can never
# pass on a bare runner and was removed from this job.

- name: Check referenced file paths in docs
run: |
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/production-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ jobs:
STELLAR_AGENT_SECRET_KEY: SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
VAULT_CONTRACT_ID: CDUMMYVAULTCONTRACTID
USDC_TOKEN_ADDRESS: CDUMMYUSDC
ANTHROPIC_API_KEY: smoke-anthropic-key
ANTHROPIC_API_KEY: sk-ant-smoke-key
JWT_SEED: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
JWT_SESSION_TTL_HOURS: '24'
JWT_NONCE_TTL_MS: '300000'
JWT_CLEANUP_INTERVAL_MS: '86400000'
WALLET_ENCRYPTION_KEY: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
TWILIO_AUTH_TOKEN: smoke-twilio-token
TWILIO_AUTH_TOKEN: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

steps:
- name: Checkout repository
Expand All @@ -56,7 +56,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: npm

- name: Install dependencies (build)
Expand All @@ -78,4 +78,4 @@ jobs:
run: npx prisma@5.22.0 generate

- name: Run startup smoke check (/health)
run: npm run smoke:health
run: npm run smoke
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ coverage/
.agents/
skills-lock.json
CLAUDE.md
plan.md
plan.md
tasks/
56 changes: 56 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,62 @@ Response 404:
"error": "User not found"
}

### GET /api/portfolio/:userId/tax-report

- Auth: required (requireAuth + enforceUserAccess)
- Path params:
- userId: uuid string
- Query params:
- year: integer (2000–2100), required — calendar year, UTC boundaries
- format: enum(json, csv), default json
- Request body: none

Realized gain/loss report computed with FIFO cost-basis lot accounting over
confirmed on-chain withdrawals. Money fields are decimal strings; null means
"unpriced" (excluded from totals), never zero. See docs/TAX_REPORT.md for
methodology and known limitations.

Example request:
GET /api/portfolio/550e8400-e29b-41d4-a716-446655440001/tax-report?year=2026

Response 200 (json):
{
"userId": "550e8400-e29b-41d4-a716-446655440001",
"year": 2026,
"method": "FIFO",
"disposals": [
{
"disposedAt": "2026-06-15T00:00:00.000Z",
"assetSymbol": "USDC",
"amount": "40",
"withdrawalTxHash": "c1d2...",
"acquiredAt": "2026-01-15T00:00:00.000Z",
"acquisitionTxHash": "a1b2...",
"acquisitionPrice": "1",
"disposalPrice": "1",
"costBasis": "40",
"proceeds": "40",
"realizedGain": "0",
"priced": true
}
],
"totals": { "proceeds": "40", "costBasis": "40", "realizedGain": "0", "pricedDisposalCount": 1 },
"caveats": { "unpricedDisposalCount": 0, "unpricedAssets": [], "stablecoinAssumption": "...", "rebalancesNotIncluded": "..." }
}

Response 200 (format=csv): text/csv attachment (tax-report-2026.csv), header
row plus one row per disposal, formula-injection-safe cells.

Response 401:
{
"error": "Unauthorized"
}

Response 404:
{
"error": "User not found"
}

---

## Transactions
Expand Down
1 change: 1 addition & 0 deletions docs/DOCUMENTATION_INDEX.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Documentation Index - Issue #23: Vault Events Persistence

## Quick Navigation
Expand All @@ -8,6 +8,7 @@
- **[CODE_STRUCTURE.md](CODE_STRUCTURE.md)** - Understand code organization and design
- **[IMPLEMENTATION_DETAILS.md](IMPLEMENTATION_DETAILS.md)** - Deep dive into implementation
- **[API_REFERENCE.md](API_REFERENCE.md)** - Complete backend endpoint reference
- **[TAX_REPORT.md](TAX_REPORT.md)** - Tax reporting & FIFO cost-basis lot tracking (#284)

### For DevOps/Deployment

Expand Down
Loading
Loading