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
5,728 changes: 437 additions & 5,291 deletions docs/openapi.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 12 additions & 27 deletions prisma/migrations/20260302221454_init/rollback.sql
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
-- Rollback for 20260302221454_init
-- Reverses the initial schema: drops foreign keys, tables, and enums.
-- WARNING: This drops every core table and DESTROYS ALL DATA. Take a backup first.
-- rollback.sql — reverse of 20260302221454_init/migration.sql
-- Drops all tables created by the initial schema migration.
-- WARNING: This is a full database wipe. Only run in a non-production
-- environment or after a verified database snapshot backup.

-- DropForeignKey
ALTER TABLE "agent_logs" DROP CONSTRAINT IF EXISTS "agent_logs_userId_fkey";
ALTER TABLE "yield_snapshots" DROP CONSTRAINT IF EXISTS "yield_snapshots_positionId_fkey";
ALTER TABLE "transactions" DROP CONSTRAINT IF EXISTS "transactions_positionId_fkey";
ALTER TABLE "transactions" DROP CONSTRAINT IF EXISTS "transactions_userId_fkey";
ALTER TABLE "positions" DROP CONSTRAINT IF EXISTS "positions_userId_fkey";
ALTER TABLE "sessions" DROP CONSTRAINT IF EXISTS "sessions_userId_fkey";

-- DropTable (indexes are dropped with their table)
DROP TABLE IF EXISTS "agent_logs";
DROP TABLE IF EXISTS "protocol_rates";
DROP TABLE IF EXISTS "yield_snapshots";
DROP TABLE IF EXISTS "transactions";
DROP TABLE IF EXISTS "positions";
DROP TABLE IF EXISTS "sessions";
DROP TABLE IF EXISTS "users";

-- DropEnum
DROP TYPE IF EXISTS "AgentStatus";
DROP TYPE IF EXISTS "AgentAction";
DROP TYPE IF EXISTS "PositionStatus";
DROP TYPE IF EXISTS "TransactionStatus";
DROP TYPE IF EXISTS "TransactionType";
DROP TYPE IF EXISTS "Network";
-- Drop foreign keys and tables in dependency order
DROP TABLE IF EXISTS "agent_logs" CASCADE;
DROP TABLE IF EXISTS "yield_snapshots" CASCADE;
DROP TABLE IF EXISTS "transactions" CASCADE;
DROP TABLE IF EXISTS "protocol_rates" CASCADE;
DROP TABLE IF EXISTS "positions" CASCADE;
DROP TABLE IF EXISTS "sessions" CASCADE;
DROP TABLE IF EXISTS "users" CASCADE;
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Rollback for 20260326152030_add_event_tracking
-- Drops the event-tracking tables (indexes drop with their table).
-- WARNING: Destroys event cursor state and the processed-event dedupe log.
-- rollback.sql — reverse of 20260326152030_add_event_tracking/migration.sql
-- Drops the event_cursors and processed_events tables.

DROP TABLE IF EXISTS "processed_events";
DROP TABLE IF EXISTS "event_cursors";
DROP TABLE IF EXISTS "processed_events" CASCADE;
DROP TABLE IF EXISTS "event_cursors" CASCADE;
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
-- Rollback for 20260425140000_add_performance_indexes
-- Drops the indexes the forward migration created and recreates the three it dropped.
-- Index-only changes: no data is affected.
-- rollback.sql — reverse of 20260425140000_add_performance_indexes/migration.sql
-- Drops new indexes and restores original indexes that were dropped by the migration.

-- Recreate indexes that the forward migration dropped
CREATE INDEX IF NOT EXISTS "users_walletAddress_idx" ON "users"("walletAddress");
CREATE INDEX IF NOT EXISTS "sessions_token_idx" ON "sessions"("token");
CREATE INDEX IF NOT EXISTS "transactions_txHash_idx" ON "transactions"("txHash");

-- Drop indexes the forward migration created
DROP INDEX IF EXISTS "sessions_expiresAt_idx";
DROP INDEX IF EXISTS "sessions_userId_expiresAt_idx";
DROP INDEX IF EXISTS "positions_status_idx";
DROP INDEX IF EXISTS "positions_userId_status_idx";
DROP INDEX IF EXISTS "positions_protocolName_assetSymbol_idx";
DROP INDEX IF EXISTS "positions_assetSymbol_idx";
DROP INDEX IF EXISTS "transactions_type_idx";
DROP INDEX IF EXISTS "transactions_status_idx";
DROP INDEX IF EXISTS "transactions_createdAt_idx";
DROP INDEX IF EXISTS "transactions_userId_createdAt_idx";
DROP INDEX IF EXISTS "agent_logs_status_idx";
-- Drop indexes added by the migration
DROP INDEX IF EXISTS "agent_logs_userId_status_idx";
DROP INDEX IF EXISTS "agent_logs_status_idx";
DROP INDEX IF EXISTS "transactions_userId_createdAt_idx";
DROP INDEX IF EXISTS "transactions_createdAt_idx";
DROP INDEX IF EXISTS "transactions_status_idx";
DROP INDEX IF EXISTS "transactions_type_idx";
DROP INDEX IF EXISTS "positions_assetSymbol_idx";
DROP INDEX IF EXISTS "positions_protocolName_assetSymbol_idx";
DROP INDEX IF EXISTS "positions_userId_status_idx";
DROP INDEX IF EXISTS "positions_status_idx";
DROP INDEX IF EXISTS "sessions_userId_expiresAt_idx";
DROP INDEX IF EXISTS "sessions_expiresAt_idx";
DROP INDEX IF EXISTS "processed_events_ledger_idx";

-- Restore indexes that the migration dropped
CREATE INDEX "users_walletAddress_idx" ON "users"("walletAddress");
CREATE INDEX "sessions_token_idx" ON "sessions"("token");
CREATE INDEX "transactions_txHash_idx" ON "transactions"("txHash");
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
-- Rollback for 20260528_add_dead_letter_events
-- Drops the dead-letter queue table and its enum.
-- WARNING: Destroys any queued/failed events awaiting retry.
-- rollback.sql — reverse of 20260528_add_dead_letter_events/migration.sql
-- Drops the dead_letter_events table and the DeadLetterEventStatus enum.

DROP TABLE IF EXISTS "dead_letter_events";
DROP TABLE IF EXISTS "dead_letter_events" CASCADE;
DROP TYPE IF EXISTS "DeadLetterEventStatus";
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
-- Rollback for 20260529000001_add_custodial_wallets
-- Drops the custodial wallets table (indexes drop with it).
-- WARNING: Destroys encrypted custodial wallet secrets. Ensure keys are backed up.
-- rollback.sql — reverse of 20260529000001_add_custodial_wallets/migration.sql
-- Drops the custodial_wallets table and all its indexes.

DROP TABLE IF EXISTS "custodial_wallets";
DROP TABLE IF EXISTS "custodial_wallets" CASCADE;
7 changes: 3 additions & 4 deletions prisma/migrations/20260529000002_add_auth_nonces/rollback.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
-- Rollback for 20260529000002_add_auth_nonces
-- Drops the auth nonce table (indexes drop with it).
-- Safe: nonces are short-lived challenge values, not durable state.
-- rollback.sql — reverse of 20260529000002_add_auth_nonces/migration.sql
-- Drops the auth_nonces table and all its indexes.

DROP TABLE IF EXISTS "auth_nonces";
DROP TABLE IF EXISTS "auth_nonces" CASCADE;
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
-- Rollback for 20260617000000_fix_agent_log_attribution
-- Reverses the agent_logs attribution changes.
-- rollback.sql — reverse of 20260617000000_fix_agent_log_attribution/migration.sql
--
-- PARTIALLY IRREVERSIBLE: the forward migration relaxed agent_logs.userId to
-- nullable so system-level scans could log without a user. Restoring NOT NULL
-- will FAIL if any rows have userId IS NULL. Reassign or delete those rows
-- before running this rollback, e.g.:
-- DELETE FROM "agent_logs" WHERE "userId" IS NULL;
-- Restores agent_logs.userId to NOT NULL and drops positionId.
-- NOTE: If any existing rows have userId=NULL (system-level logs written after
-- the migration), they must be reassigned or deleted before re-adding the
-- NOT NULL constraint. Document any such rows before running this rollback.
--
-- Run with: psql $DATABASE_URL -f prisma/migrations/20260617000000_fix_agent_log_attribution/rollback.sql

-- Remove indexes added by the migration
DROP INDEX IF EXISTS "agent_logs_userId_createdAt_idx";
DROP INDEX IF EXISTS "agent_logs_positionId_idx";

-- Remove the positionId column
ALTER TABLE "agent_logs" DROP COLUMN IF EXISTS "positionId";

-- Restore userId NOT NULL (fails if any null rows exist — resolve before running)
ALTER TABLE "agent_logs" ALTER COLUMN "userId" SET NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Migration: add_portfolio_risk_aggregates
-- Creates the portfolio_risk_aggregates table for precomputed per-user
-- risk metrics (VaR, CVaR, Sortino, drawdown, volatility).
-- Written by src/jobs/portfolioRisk.ts on a configurable schedule.

CREATE TABLE "portfolio_risk_aggregates" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"window" TEXT NOT NULL,
"insufficientHistory" BOOLEAN NOT NULL DEFAULT false,
"sampleCount" INTEGER NOT NULL DEFAULT 0,
"annualisedVolatility" DECIMAL(20, 8),
"sortinoRatio" DECIMAL(20, 8),
"downsideDeviation" DECIMAL(20, 8),
"maxDrawdown" DECIMAL(20, 8),
"maxDrawdownDuration" INTEGER,
"varHistorical95" DECIMAL(20, 8),
"varHistorical99" DECIMAL(20, 8),
"varParametric95" DECIMAL(20, 8),
"varParametric99" DECIMAL(20, 8),
"cvarHistorical95" DECIMAL(20, 8),
"cvarHistorical99" DECIMAL(20, 8),
"beta" DECIMAL(20, 8),
"dataFrom" TIMESTAMP(3),
"dataTo" TIMESTAMP(3),
"computedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "portfolio_risk_aggregates_pkey" PRIMARY KEY ("id")
);

-- Foreign key to users
ALTER TABLE "portfolio_risk_aggregates"
ADD CONSTRAINT "portfolio_risk_aggregates_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Unique constraint: one row per (userId, window) — upserted on each compute run
CREATE UNIQUE INDEX "portfolio_risk_aggregates_userId_window_key"
ON "portfolio_risk_aggregates"("userId", "window");

-- Indexes for efficient lookups and leaderboard ORDER BY
CREATE INDEX "portfolio_risk_aggregates_userId_idx" ON "portfolio_risk_aggregates"("userId");
CREATE INDEX "portfolio_risk_aggregates_window_idx" ON "portfolio_risk_aggregates"("window");
CREATE INDEX "portfolio_risk_aggregates_computedAt_idx" ON "portfolio_risk_aggregates"("computedAt");
CREATE INDEX "portfolio_risk_aggregates_sortinoRatio_idx" ON "portfolio_risk_aggregates"("sortinoRatio");
CREATE INDEX "portfolio_risk_aggregates_annualisedVol_idx" ON "portfolio_risk_aggregates"("annualisedVolatility");
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- rollback.sql — reverse of 20260820000000_add_portfolio_risk_aggregates/migration.sql
--
-- Drops the portfolio_risk_aggregates table and all its indexes.
-- Safe to run multiple times (IF EXISTS guards).
-- Run with: psql $DATABASE_URL -f prisma/migrations/20260820000000_add_portfolio_risk_aggregates/rollback.sql

-- Drop indexes first (dropped implicitly with the table, listed explicitly for clarity)
DROP INDEX IF EXISTS "portfolio_risk_aggregates_annualisedVol_idx";
DROP INDEX IF EXISTS "portfolio_risk_aggregates_sortinoRatio_idx";
DROP INDEX IF EXISTS "portfolio_risk_aggregates_computedAt_idx";
DROP INDEX IF EXISTS "portfolio_risk_aggregates_window_idx";
DROP INDEX IF EXISTS "portfolio_risk_aggregates_userId_idx";
DROP INDEX IF EXISTS "portfolio_risk_aggregates_userId_window_key";

-- Drop the table (cascade removes foreign-key constraint automatically)
DROP TABLE IF EXISTS "portfolio_risk_aggregates" CASCADE;
109 changes: 73 additions & 36 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -199,26 +199,27 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

sessions Session[]
positions Position[]
transactions Transaction[]
agentLogs AgentLog[]
webhookSubscriptions WebhookSubscription[]
fiatOrders FiatOrder[]
fiatQuoteLocks FiatQuoteLock[]
referralCode ReferralCode?
referralConversion ReferralConversion?
recurringDepositPlans RecurringDepositPlan[]
alertRules AlertRule[]
costBasisLots CostBasisLot[]
lotDisposals LotDisposal[]
savingsGoals SavingsGoal[]
publishedStrategy PublishedStrategy?
strategyFollows StrategyFollow[]
parentSubAccounts SubAccount[] @relation("ParentOf")
childSubAccounts SubAccount[] @relation("ChildOf")
allocationSuggestions AllocationSuggestion[]
portfolioAttributions PortfolioAttribution[]
sessions Session[]
positions Position[]
transactions Transaction[]
agentLogs AgentLog[]
webhookSubscriptions WebhookSubscription[]
fiatOrders FiatOrder[]
fiatQuoteLocks FiatQuoteLock[]
referralCode ReferralCode?
referralConversion ReferralConversion?
recurringDepositPlans RecurringDepositPlan[]
alertRules AlertRule[]
costBasisLots CostBasisLot[]
lotDisposals LotDisposal[]
savingsGoals SavingsGoal[]
publishedStrategy PublishedStrategy?
strategyFollows StrategyFollow[]
parentSubAccounts SubAccount[] @relation("ParentOf")
childSubAccounts SubAccount[] @relation("ChildOf")
allocationSuggestions AllocationSuggestion[]
portfolioAttributions PortfolioAttribution[]
portfolioRiskAggregates PortfolioRiskAggregate[]

@@map("users")
}
Expand Down Expand Up @@ -501,6 +502,7 @@ model DeadLetterEvent {
// This is the single choke point every write to the vault contract must pass
// through — src/outbox/executors.ts is the only module besides
// src/stellar/contract.ts itself allowed to call the raw write functions

// (enforced by tests/unit/outbox/structural.test.ts).
model OutboxOp {
id String @id @default(uuid())
Expand Down Expand Up @@ -585,13 +587,13 @@ enum KeyStatus {
/// without the registry ever holding anything an attacker could decrypt
/// with. See src/keys/registry.ts.
model WalletEncryptionKey {
id String @id @default(uuid())
keyLabel String @unique
hash String @unique
status KeyStatus @default(ACTIVE)
rotatedFromId String?
createdAt DateTime @default(now())
retiredAt DateTime?
id String @id @default(uuid())
keyLabel String @unique
hash String @unique
status KeyStatus @default(ACTIVE)
rotatedFromId String?
createdAt DateTime @default(now())
retiredAt DateTime?

rotatedFrom WalletEncryptionKey? @relation("KeyRotation", fields: [rotatedFromId], references: [id])
rotatedTo WalletEncryptionKey[] @relation("KeyRotation")
Expand Down Expand Up @@ -732,15 +734,15 @@ model FiatOrder {
// cryptoAmount is never an unexplained number; settledRate/settledCryptoAmount
// are populated at on-chain settlement so the quoted-vs-settled delta is
// always inspectable on the order itself.
quoteRate Decimal? @db.Decimal(36, 18)
quotedCryptoAmount Decimal? @db.Decimal(36, 18)
fees Json? // structured FeeBreakdown; null means the provider could not price it (unpriced, never assumed 0)
providerQuoteId String?
rateLockExpiresAt DateTime?
settledRate Decimal? @db.Decimal(36, 18)
settledCryptoAmount Decimal? @db.Decimal(36, 18)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
quoteRate Decimal? @db.Decimal(36, 18)
quotedCryptoAmount Decimal? @db.Decimal(36, 18)
fees Json? // structured FeeBreakdown; null means the provider could not price it (unpriced, never assumed 0)
providerQuoteId String?
rateLockExpiresAt DateTime?
settledRate Decimal? @db.Decimal(36, 18)
settledCryptoAmount Decimal? @db.Decimal(36, 18)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
transaction Transaction? @relation(fields: [transactionId], references: [id])
Expand Down Expand Up @@ -1140,3 +1142,38 @@ model AllocationSuggestion {
@@index([userId, inputHash])
@@map("allocation_suggestions")
}

/// Precomputed risk analytics aggregate for a user (#225).
/// Recomputed on a schedule from YieldSnapshot history.
model PortfolioRiskAggregate {
id String @id @default(uuid())
userId String
window String
insufficientHistory Boolean @default(false)
sampleCount Int @default(0)
annualisedVolatility Float?
sortinoRatio Float?
downsideDeviation Float?
maxDrawdown Float?
maxDrawdownDuration Int?
varHistorical95 Float?
varHistorical99 Float?
varParametric95 Float?
varParametric99 Float?
cvarHistorical95 Float?
cvarHistorical99 Float?
beta Float?
dataFrom DateTime?
dataTo DateTime?
computedAt DateTime @default(now())

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([userId, window])
@@index([userId])
@@index([window])
@@index([computedAt])
@@index([sortinoRatio])
@@index([annualisedVolatility])
@@map("portfolio_risk_aggregates")
}
Loading
Loading