Skip to content
Open
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
66 changes: 66 additions & 0 deletions .github/workflows/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Dashboard QA

on:
push:
branches: ["**"]
paths:
- "dashboard/**"
- ".github/workflows/dashboard.yml"
pull_request:
branches: [main]
paths:
- "dashboard/**"
- ".github/workflows/dashboard.yml"

jobs:
lint-and-build:
name: Lint and Build
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: "package-lock.json"

- name: Install dependencies
run: npm ci

- name: Lint dashboard
run: npm run lint -w dashboard

- name: Build dashboard
run: npm run build -w dashboard

browser-smoke:
name: Cross-Browser Smoke (${{ matrix.browser }})
runs-on: ubuntu-latest
needs: lint-and-build

strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: "package-lock.json"

- name: Install dependencies
run: npm ci

- name: Install Playwright browser
run: npm exec --workspace dashboard -- playwright install --with-deps ${{ matrix.browser }}

- name: Run browser smoke test
run: npm run test:browser -w dashboard -- --project=${{ matrix.browser }}
10 changes: 5 additions & 5 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,17 @@
"@aws-sdk/client-kms": "^3.500.0",
"@prisma/client": "^6.19.2",
"@stellar/stellar-sdk": "^14.6.1",
"cron-parser": "^4.9.0",
"@types/node-cron": "^3.0.11",
"@types/pdfkit": "^0.17.6",
"cors": "^2.8.5",
"cron-parser": "^4.9.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^7.1.0",
"ioredis": "^5.3.0",
"jsonwebtoken": "^9.0.3",
"node-cron": "^3.0.3",
"nodemailer": "^6.10.1",
"node-cron": "^4.2.1",
"nodemailer": "^6.10.1",
"pdfkit": "^0.18.0",
"prom-client": "^15.1.0",
"rate-limit-redis": "^4.0.0",
Expand All @@ -55,11 +54,11 @@
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.25",
"@types/node-cron": "^3.0.11",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.10",
"@types/nodemailer": "^6.4.17",
"@types/node": "^20.11.30",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
Expand All @@ -70,6 +69,7 @@
"eslint": "^8.57.0",
"fast-check": "^3.23.2",
"jest": "^29.7.0",
"jest-util": "^29.7.0",
"nodemon": "^3.1.0",
"prisma": "^6.19.2",
"supertest": "^6.3.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "phone" TEXT;

-- CreateTable
CREATE TABLE "Quote" (
"id" TEXT NOT NULL PRIMARY KEY,
"sourceAsset" TEXT NOT NULL,
"sourceAmount" TEXT NOT NULL,
"destinationAsset" TEXT NOT NULL,
"destinationAmount" TEXT,
"price" TEXT,
"expiresAt" DATETIME NOT NULL,
"context" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateTable
CREATE TABLE "NotificationPreference" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"emailEnabled" BOOLEAN NOT NULL DEFAULT true,
"smsEnabled" BOOLEAN NOT NULL DEFAULT false,
"pushEnabled" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "NotificationPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"transactionId" TEXT,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"message" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Notification_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "ContractJob" (
"id" TEXT NOT NULL PRIMARY KEY,
"jobId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"priority" TEXT NOT NULL DEFAULT 'NORMAL',
"status" TEXT NOT NULL DEFAULT 'PENDING',
"contractId" TEXT,
"functionName" TEXT,
"parameters" JSONB,
"result" JSONB,
"error" TEXT,
"errorCategory" TEXT,
"errorSeverity" TEXT,
"errorCode" TEXT,
"userMessage" TEXT,
"suggestedAction" TEXT,
"retryable" BOOLEAN NOT NULL DEFAULT false,
"attempts" INTEGER NOT NULL DEFAULT 0,
"maxAttempts" INTEGER NOT NULL DEFAULT 3,
"createdBy" TEXT,
"metadata" JSONB,
"startedAt" DATETIME,
"completedAt" DATETIME,
"failedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateIndex
CREATE INDEX "Quote_expiresAt_idx" ON "Quote"("expiresAt");

-- CreateIndex
CREATE UNIQUE INDEX "NotificationPreference_userId_key" ON "NotificationPreference"("userId");

-- CreateIndex
CREATE INDEX "NotificationPreference_userId_idx" ON "NotificationPreference"("userId");

-- CreateIndex
CREATE INDEX "Notification_userId_idx" ON "Notification"("userId");

-- CreateIndex
CREATE INDEX "Notification_transactionId_idx" ON "Notification"("transactionId");

-- CreateIndex
CREATE UNIQUE INDEX "ContractJob_jobId_key" ON "ContractJob"("jobId");

-- CreateIndex
CREATE INDEX "ContractJob_jobId_idx" ON "ContractJob"("jobId");

-- CreateIndex
CREATE INDEX "ContractJob_status_idx" ON "ContractJob"("status");

-- CreateIndex
CREATE INDEX "ContractJob_createdBy_idx" ON "ContractJob"("createdBy");

-- CreateIndex
CREATE INDEX "ContractJob_type_idx" ON "ContractJob"("type");
31 changes: 17 additions & 14 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,7 @@ model User {
kycCustomer KycCustomer?
apiKeys ApiKey[]
recurringPaymentSchedules RecurringPaymentSchedule[]
id String @id @default(uuid())
publicKey String @unique
email String?
phone String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
transactions Transaction[]
kycCustomer KycCustomer?
apiKeys ApiKey[]
notificationPreference NotificationPreference?

@@index([publicKey])
Expand Down Expand Up @@ -138,12 +130,6 @@ model Transaction {
stellarTxId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type String // DEPOSIT | WITHDRAW | SEP31
status String // PENDING, COMPLETED, FAILED
externalId String? @unique
stellarTxId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notifications Notification[]

// Fee tracking
Expand Down Expand Up @@ -184,6 +170,21 @@ model FeeReport {
@@index([endDate])
}

model Quote {
id String @id @default(uuid())
sourceAsset String
sourceAmount String
destinationAsset String
destinationAmount String?
price String?
expiresAt DateTime
context String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([expiresAt])
}

model NotificationPreference {
id String @id @default(uuid())
userId String @unique
Expand Down Expand Up @@ -254,6 +255,8 @@ model SystemConfig {

@@index([isActive])
@@index([createdAt])
}

enum JobStatus {
PENDING
ACTIVE
Expand Down
8 changes: 4 additions & 4 deletions backend/src/api/routes/sep24.route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('SEP-24 Routes', () => {

expect(res.statusCode).toBe(400);
expect(res.body.error).toContain('Asset DOGE is not supported');
expect(res.body.error).toContain('Supported assets: USDC, USD, BTC, ETH');
expect(res.body.error).toContain('Supported assets: USDC, USD');
});

it('returns an interactive URL for supported assets (with optional params)', async () => {
Expand Down Expand Up @@ -100,22 +100,22 @@ describe('SEP-24 Routes', () => {

expect(res.statusCode).toBe(400);
expect(res.body.error).toContain('Asset DOGE is not supported');
expect(res.body.error).toContain('Supported assets: USDC, USD, BTC, ETH');
expect(res.body.error).toContain('Supported assets: USDC, USD');
});

it('returns an interactive URL for supported assets', async () => {
const res = await request(app)
.post('/transactions/withdraw/interactive')
.send({
asset_code: 'BTC',
asset_code: 'USDC',
account: 'GACCOUNT',
amount: '1'
});

expect(res.statusCode).toBe(200);
const parsed = new URL(res.body.url);
expect(parsed.pathname).toBe('/kyc-withdraw');
expect(parsed.searchParams.get('asset_code')).toBe('BTC');
expect(parsed.searchParams.get('asset_code')).toBe('USDC');
expect(parsed.searchParams.get('account')).toBe('GACCOUNT');
expect(parsed.searchParams.get('amount')).toBe('1');
});
Expand Down
2 changes: 0 additions & 2 deletions backend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,11 @@ const envSchema = z.object({
.pipe(z.number().int().min(0)),
STELLAR_NETWORK: z.enum(['testnet', 'public', 'futurenet']).default('testnet'),
RECURRING_PAYMENTS_WORKER_CRON: z.string().default('*/1 * * * *'),
STELLAR_NETWORK: z.enum(['testnet', 'public']).default('testnet'),
STELLAR_NETWORK_PASSPHRASE: z
.string()
.default('Test SDF Network ; September 2015'),
STELLAR_HORIZON_URL: z.string().url().default('https://horizon-testnet.stellar.org'),
HORIZON_URL: z.string().url().default('https://horizon-testnet.stellar.org'),
STELLAR_NETWORK_PASSPHRASE: z.string().default('Test SDF Network ; September 2015'),
STELLAR_FEE_BUMP_SECRET: z.string().optional(),
STELLAR_DISTRIBUTION_SECRET: z.string().optional(),
STELLAR_BASE_FEE: z.string().default('100'),
Expand Down
3 changes: 0 additions & 3 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import feeReportRouter from './api/routes/fee-report.route';
import { feeReportScheduler } from './workers/fee-report.scheduler';
import eventRouter from './api/routes/event.route';
import notificationsRouter from './api/routes/notifications.route';
import { errorHandler } from './api/middleware/error.middleware';
import { metricsMiddleware, connectionTracker } from './api/middleware/metrics.middleware';
import { publicLimiter } from './api/middleware/rate-limit.middleware';
import { notificationService } from './services/notification.service';
import { ConsoleEmailProvider, ConsoleSmsProvider, ConsolePushProvider } from './lib/notifications/providers';
Expand Down Expand Up @@ -141,7 +139,6 @@ app.use('/sep40', sep40Router);
app.use('/info', infoRouter);

// SEP-24 routes
app.use('/sep24', sep24Router);
// Public endpoints with shared Redis-backed rate limit state
app.use('/sep38', publicLimiter, sep38Router);
app.use('/info', publicLimiter, infoRouter);
Expand Down
6 changes: 6 additions & 0 deletions backend/src/lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
if (!process.env.DATABASE_URL) {
process.env.DATABASE_URL = process.env.NODE_ENV === 'test'
? 'file:./prisma/test.db'
: 'file:./prisma/dev.db';
}

import { PrismaClient } from '@prisma/client';
import { metricsService } from '../services/metrics.service';

Expand Down
Loading