Initial commit
This commit is contained in:
53
src/__tests__/e2e/fx-trading-flow.test.ts
Normal file
53
src/__tests__/e2e/fx-trading-flow.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// FX Trading Flow End-to-End Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { createAuthHeaders } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('FX Trading Flow E2E', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
it('should complete full FX trading workflow', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
// Step 1: Submit FX order
|
||||
const orderResponse = await request(app)
|
||||
.post('/api/fx/orders')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }))
|
||||
.send({
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: 'market',
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(orderResponse.status);
|
||||
const tradeId = orderResponse.body.data?.tradeId;
|
||||
|
||||
if (tradeId) {
|
||||
// Step 2: Execute trade
|
||||
const executeResponse = await request(app)
|
||||
.post(`/api/fx/trades/${tradeId}/execute`)
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }));
|
||||
|
||||
expect([200, 201]).toContain(executeResponse.status);
|
||||
|
||||
// Step 3: Check trade status
|
||||
const statusResponse = await request(app)
|
||||
.get(`/api/fx/trades/${tradeId}`)
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }));
|
||||
|
||||
expect(statusResponse.status).toBe(200);
|
||||
expect(['executed', 'settled']).toContain(statusResponse.body.data?.status);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
57
src/__tests__/e2e/payment-flow.test.ts
Normal file
57
src/__tests__/e2e/payment-flow.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// Payment Flow End-to-End Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { createAuthHeaders } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('Payment Flow E2E', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
it('should complete a full payment flow', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
// Note: This test assumes payment routes are implemented
|
||||
// Adjust based on actual API structure
|
||||
const response = await request(app)
|
||||
.post('/api/payments')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }))
|
||||
.send({
|
||||
sourceAccountId: sourceAccount.id,
|
||||
destinationAccountId: destAccount.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
paymentType: 'transfer',
|
||||
});
|
||||
|
||||
// Verify response
|
||||
expect([200, 201]).toContain(response.status);
|
||||
|
||||
// Verify balances updated
|
||||
const updatedSource = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: sourceAccount.id },
|
||||
});
|
||||
const updatedDest = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: destAccount.id },
|
||||
});
|
||||
|
||||
expect(updatedSource?.balance.toString()).toBe('900.00');
|
||||
expect(updatedDest?.balance.toString()).toBe('600.00');
|
||||
});
|
||||
});
|
||||
|
||||
69
src/__tests__/e2e/settlement-workflow.test.ts
Normal file
69
src/__tests__/e2e/settlement-workflow.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
// Settlement Workflow End-to-End Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { createAuthHeaders } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('Settlement Workflow E2E', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
it('should complete full settlement workflow', async () => {
|
||||
const bank1 = await createTestSovereignBank({ sovereignCode: 'BANK1' });
|
||||
const bank2 = await createTestSovereignBank({ sovereignCode: 'BANK2' });
|
||||
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank1.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank2.id,
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
// Step 1: Initiate payment
|
||||
const paymentResponse = await request(app)
|
||||
.post('/api/payments')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank1.id }))
|
||||
.send({
|
||||
debtorAccount: account1.accountNumber,
|
||||
creditorAccount: account2.accountNumber,
|
||||
amount: '100.00',
|
||||
currency: 'USD',
|
||||
priority: 'NORMAL',
|
||||
assetType: 'fiat',
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(paymentResponse.status);
|
||||
const paymentId = paymentResponse.body.data?.paymentId;
|
||||
|
||||
if (paymentId) {
|
||||
// Step 2: Check payment status
|
||||
const statusResponse = await request(app)
|
||||
.get(`/api/payments/${paymentId}`)
|
||||
.set(createAuthHeaders({ sovereignBankId: bank1.id }));
|
||||
|
||||
expect(statusResponse.status).toBe(200);
|
||||
expect(statusResponse.body.data.status).toBe('settled');
|
||||
}
|
||||
|
||||
// Step 3: Verify balances
|
||||
const updatedAccount1 = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: account1.id },
|
||||
});
|
||||
const updatedAccount2 = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: account2.id },
|
||||
});
|
||||
|
||||
expect(updatedAccount1?.balance.toString()).toBe('900.00');
|
||||
expect(updatedAccount2?.balance.toString()).toBe('600.00');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Authentication Middleware Integration Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createAuthHeaders, createTestToken } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('Authentication Middleware', () => {
|
||||
describe('zeroTrustAuthMiddleware', () => {
|
||||
it('should reject requests without token', async () => {
|
||||
const response = await request(app).get('/api/health').expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error.code).toBe('UNAUTHORIZED');
|
||||
});
|
||||
|
||||
it('should reject requests with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/health')
|
||||
.set('authorization', 'SOV-TOKEN invalid-token')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept requests with valid token', async () => {
|
||||
// Note: This test may need adjustment based on actual route implementation
|
||||
// Health endpoint should not require auth
|
||||
const response = await request(app).get('/health').expect(200);
|
||||
|
||||
expect(response.body.status).toBe('healthy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Signature Verification', () => {
|
||||
it('should require signature headers', async () => {
|
||||
const token = createTestToken({ sovereignBankId: 'test-bank' });
|
||||
const response = await request(app)
|
||||
.get('/api/health')
|
||||
.set('authorization', `SOV-TOKEN ${token}`)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.error.message).toContain('signature');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
47
src/__tests__/integration/api/accounts.routes.test.ts
Normal file
47
src/__tests__/integration/api/accounts.routes.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Accounts API Integration Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { createAuthHeaders } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('Accounts API Integration', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('POST /api/accounts', () => {
|
||||
it('should create a new account', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/accounts')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }))
|
||||
.send({
|
||||
accountType: 'commercial',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(response.status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/accounts/:id', () => {
|
||||
it('should retrieve account details', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/accounts/test-id')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }));
|
||||
|
||||
expect([200, 404]).toContain(response.status);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
48
src/__tests__/integration/api/fx.routes.test.ts
Normal file
48
src/__tests__/integration/api/fx.routes.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// FX API Integration Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { createAuthHeaders } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('FX API Integration', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('POST /api/fx/orders', () => {
|
||||
it('should submit an FX order', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/fx/orders')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }))
|
||||
.send({
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: 'market',
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(response.status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/fx/trades/:id', () => {
|
||||
it('should retrieve FX trade details', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/fx/trades/test-id')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }));
|
||||
|
||||
expect([200, 404]).toContain(response.status);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
62
src/__tests__/integration/api/ledger.routes.test.ts
Normal file
62
src/__tests__/integration/api/ledger.routes.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Ledger API Integration Tests
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/integration/api-gateway/app';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { createAuthHeaders } from '@/__tests__/utils/test-auth';
|
||||
|
||||
describe('Ledger API Integration', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('POST /api/ledger/entries', () => {
|
||||
it('should create a ledger entry', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const debitAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const creditAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/ledger/entries')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }))
|
||||
.send({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId: debitAccount.id,
|
||||
creditAccountId: creditAccount.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: 'TEST-REF-001',
|
||||
});
|
||||
|
||||
expect([200, 201]).toContain(response.status);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/ledger/entries/:id', () => {
|
||||
it('should retrieve a ledger entry', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
// Note: This test assumes the route exists
|
||||
// Adjust based on actual API structure
|
||||
const response = await request(app)
|
||||
.get('/api/ledger/entries/test-id')
|
||||
.set(createAuthHeaders({ sovereignBankId: bank.id }));
|
||||
|
||||
expect([200, 404]).toContain(response.status);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
22
src/__tests__/setup.ts
Normal file
22
src/__tests__/setup.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Jest test setup
|
||||
// Runs before all tests
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
|
||||
// Set test environment
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL || 'postgresql://test:test@localhost:5432/dbis_test';
|
||||
process.env.JWT_SECRET = 'test-jwt-secret-minimum-32-characters-long-for-testing';
|
||||
process.env.ALLOWED_ORIGINS = 'http://localhost:3000';
|
||||
process.env.LOG_LEVEL = 'error'; // Reduce log noise in tests
|
||||
|
||||
// Global test timeout
|
||||
jest.setTimeout(10000);
|
||||
|
||||
// Cleanup after all tests
|
||||
afterAll(async () => {
|
||||
// Close any open connections
|
||||
const prisma = new PrismaClient();
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
35
src/__tests__/unit/core/compliance/aml.test.ts
Normal file
35
src/__tests__/unit/core/compliance/aml.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// AML Compliance Unit Tests
|
||||
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
|
||||
describe('AML Compliance', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('Sanctions Screening', () => {
|
||||
it('should screen transactions against sanctions list', async () => {
|
||||
// This would test the actual AML service when implemented
|
||||
// For now, this is a placeholder test structure
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should flag high-risk transactions', async () => {
|
||||
// Placeholder for AML risk scoring tests
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PEP Detection', () => {
|
||||
it('should detect politically exposed persons', async () => {
|
||||
// Placeholder for PEP detection tests
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
116
src/__tests__/unit/core/compliance/compliance-complete.test.ts
Normal file
116
src/__tests__/unit/core/compliance/compliance-complete.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// Complete Compliance Test Suite
|
||||
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { amlService } from '@/core/compliance/aml.service';
|
||||
import { ComplianceRecordType } from '@/shared/types';
|
||||
|
||||
describe('Compliance - Complete Test Suite', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('AML Screening', () => {
|
||||
it('should screen transactions for AML violations', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const result = await amlService.screenTransaction({
|
||||
transactionId: 'TEST-TX-001',
|
||||
sourceAccountId: 'test-account-1',
|
||||
destinationAccountId: 'test-account-2',
|
||||
amount: '10000.00',
|
||||
currencyCode: 'USD',
|
||||
transactionType: 'transfer',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.riskScore).toBeGreaterThanOrEqual(0);
|
||||
expect(result.riskScore).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('should flag high-risk transactions', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
// Large amount that should trigger AML
|
||||
const result = await amlService.screenTransaction({
|
||||
transactionId: 'TEST-TX-002',
|
||||
sourceAccountId: 'test-account-1',
|
||||
destinationAccountId: 'test-account-2',
|
||||
amount: '1000000.00',
|
||||
currencyCode: 'USD',
|
||||
transactionType: 'transfer',
|
||||
});
|
||||
|
||||
expect(result.riskScore).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sanctions Checking', () => {
|
||||
it('should check entities against sanctions list', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const result = await amlService.checkSanctions({
|
||||
entityId: 'test-entity',
|
||||
entityType: 'account',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.isSanctioned).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PEP Detection', () => {
|
||||
it('should detect politically exposed persons', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const result = await amlService.checkPEP({
|
||||
entityId: 'test-entity',
|
||||
entityType: 'account',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.isPEP).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Risk Scoring', () => {
|
||||
it('should calculate comprehensive risk score', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const riskScore = await amlService.calculateRiskScore({
|
||||
transactionId: 'TEST-TX-003',
|
||||
sourceAccountId: 'test-account-1',
|
||||
destinationAccountId: 'test-account-2',
|
||||
amount: '50000.00',
|
||||
currencyCode: 'USD',
|
||||
});
|
||||
|
||||
expect(riskScore).toBeGreaterThanOrEqual(0);
|
||||
expect(riskScore).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Compliance Record Creation', () => {
|
||||
it('should create compliance record for flagged transactions', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const record = await amlService.createComplianceRecord({
|
||||
entityId: 'test-entity',
|
||||
entityType: 'transaction',
|
||||
recordType: ComplianceRecordType.AML_ALERT,
|
||||
riskScore: 75,
|
||||
details: {
|
||||
reason: 'High-value transaction',
|
||||
},
|
||||
});
|
||||
|
||||
expect(record.id).toBeDefined();
|
||||
expect(record.status).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
124
src/__tests__/unit/core/fx/fx-edge-cases.test.ts
Normal file
124
src/__tests__/unit/core/fx/fx-edge-cases.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
// FX Service Edge Cases Unit Tests
|
||||
|
||||
import { fxService } from '@/core/fx/fx.service';
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { FxOrderType, FxTradeType } from '@/shared/types';
|
||||
|
||||
describe('FxService - Edge Cases', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('Market Volatility', () => {
|
||||
it('should handle rapid price changes', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const order1 = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
// Simulate rapid price change scenario
|
||||
const order2 = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect(order1.tradeId).toBeDefined();
|
||||
expect(order2.tradeId).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Order Matching', () => {
|
||||
it('should match limit orders at correct price', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const order = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.LIMIT,
|
||||
limitPrice: '0.85',
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect(order.tradeId).toBeDefined();
|
||||
expect(order.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should not match limit orders below market price', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
// This test would need market price mocking
|
||||
const order = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.LIMIT,
|
||||
limitPrice: '0.50', // Unrealistically low
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect(order.status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Settlement Failures', () => {
|
||||
it('should handle settlement failure gracefully', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const order = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
// Attempt to execute
|
||||
try {
|
||||
await fxService.executeTrade(order.tradeId);
|
||||
} catch (error) {
|
||||
// Settlement failure should be handled
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid Currency Pairs', () => {
|
||||
it('should reject invalid currency pair format', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
await expect(
|
||||
fxService.submitOrder(bank.id, {
|
||||
pair: 'INVALID',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Large Order Amounts', () => {
|
||||
it('should handle very large FX orders', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const order = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '999999999.99',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect(order.tradeId).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
82
src/__tests__/unit/core/fx/fx.service.test.ts
Normal file
82
src/__tests__/unit/core/fx/fx.service.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// FX Service Unit Tests
|
||||
|
||||
import { fxService } from '@/core/fx/fx.service';
|
||||
import { createTestSovereignBank, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { FxOrderType, FxTradeType } from '@/shared/types';
|
||||
|
||||
describe('FxService', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('submitOrder', () => {
|
||||
it('should submit a market order', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const result = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect(result.tradeId).toBeDefined();
|
||||
expect(result.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should submit a limit order with price', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const result = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.LIMIT,
|
||||
limitPrice: '0.85',
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
expect(result.tradeId).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject limit order without price', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
await expect(
|
||||
fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.LIMIT,
|
||||
settlement: 'RTGS',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeTrade', () => {
|
||||
it('should execute a pending trade', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
|
||||
const orderResult = await fxService.submitOrder(bank.id, {
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: FxOrderType.MARKET,
|
||||
settlement: 'RTGS',
|
||||
});
|
||||
|
||||
const trade = await fxService.executeTrade(orderResult.tradeId);
|
||||
|
||||
expect(trade.status).toBe('executed');
|
||||
expect(trade.executedAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject executing non-existent trade', async () => {
|
||||
await expect(fxService.executeTrade('NON-EXISTENT')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
148
src/__tests__/unit/core/ledger/ledger.service.test.ts
Normal file
148
src/__tests__/unit/core/ledger/ledger.service.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// Ledger Service Unit Tests
|
||||
|
||||
import { ledgerService } from '@/core/ledger/ledger.service';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
describe('LedgerService', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('postEntry', () => {
|
||||
it('should create a double-entry ledger entry', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const debitAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
accountNumber: 'DEBIT-001',
|
||||
balance: '1000.00',
|
||||
});
|
||||
const creditAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
accountNumber: 'CREDIT-001',
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
const result = await ledgerService.postEntry({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId: debitAccount.id,
|
||||
creditAccountId: creditAccount.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: 'TEST-REF-001',
|
||||
});
|
||||
|
||||
expect(result.status).toBe('posted');
|
||||
expect(result.entryIds).toHaveLength(1);
|
||||
|
||||
// Verify balances updated
|
||||
const updatedDebit = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: debitAccount.id },
|
||||
});
|
||||
const updatedCredit = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: creditAccount.id },
|
||||
});
|
||||
|
||||
expect(updatedDebit?.balance.toString()).toBe('900.00');
|
||||
expect(updatedCredit?.balance.toString()).toBe('600.00');
|
||||
});
|
||||
|
||||
it('should validate debits equal credits', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const debitAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
const creditAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
ledgerService.postEntry({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId: debitAccount.id,
|
||||
creditAccountId: creditAccount.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: 'TEST-REF-002',
|
||||
})
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject entry if debit account has insufficient balance', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const debitAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '50.00',
|
||||
});
|
||||
const creditAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
ledgerService.postEntry({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId: debitAccount.id,
|
||||
creditAccountId: creditAccount.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: 'TEST-REF-003',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashChaining', () => {
|
||||
it('should chain ledger entries with previous hash', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const debitAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
const creditAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
const entry1 = await ledgerService.postEntry({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId: debitAccount.id,
|
||||
creditAccountId: creditAccount.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: 'TEST-REF-004',
|
||||
});
|
||||
|
||||
const entry2 = await ledgerService.postEntry({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId: debitAccount.id,
|
||||
creditAccountId: creditAccount.id,
|
||||
amount: '50.00',
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: 'TEST-REF-005',
|
||||
});
|
||||
|
||||
const ledgerEntry1 = await testPrisma.ledgerEntry.findUnique({
|
||||
where: { id: entry1.entryIds[0] },
|
||||
});
|
||||
const ledgerEntry2 = await testPrisma.ledgerEntry.findUnique({
|
||||
where: { id: entry2.entryIds[0] },
|
||||
});
|
||||
|
||||
expect(ledgerEntry2?.previousHash).toBe(ledgerEntry1?.blockHash);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
177
src/__tests__/unit/core/payments/payment-edge-cases.test.ts
Normal file
177
src/__tests__/unit/core/payments/payment-edge-cases.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// Payment Service Edge Cases Unit Tests
|
||||
|
||||
import { paymentService } from '@/core/payments/payment.service';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { PaymentPriority } from '@/shared/types';
|
||||
|
||||
describe('PaymentService - Edge Cases', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('Currency Mismatch Handling', () => {
|
||||
it('should reject payment when currencies do not match', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
currencyCode: 'USD',
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
currencyCode: 'EUR',
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount.accountNumber,
|
||||
amount: '100.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Large Amount Handling', () => {
|
||||
it('should handle very large payment amounts', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '999999999.99',
|
||||
});
|
||||
const destAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
const result = await paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount.accountNumber,
|
||||
amount: '999999999.99',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.RTGS,
|
||||
assetType: 'fiat',
|
||||
});
|
||||
|
||||
expect(result.status).toBe('settled');
|
||||
});
|
||||
|
||||
it('should reject payment exceeding account balance', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount.accountNumber,
|
||||
amount: '1001.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
})
|
||||
).rejects.toThrow('Insufficient');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Payments', () => {
|
||||
it('should handle multiple concurrent payments from same account', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destAccount1 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
const destAccount2 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
// Simulate concurrent payments
|
||||
const payment1 = paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount1.accountNumber,
|
||||
amount: '300.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
});
|
||||
|
||||
const payment2 = paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount2.accountNumber,
|
||||
amount: '300.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled([payment1, payment2]);
|
||||
|
||||
// At least one should succeed, but both might fail if balance is insufficient
|
||||
const succeeded = results.filter((r) => r.status === 'fulfilled').length;
|
||||
expect(succeeded).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zero and Negative Amounts', () => {
|
||||
it('should reject zero amount payments', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount.accountNumber,
|
||||
amount: '0.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should reject negative amount payments', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destAccount.accountNumber,
|
||||
amount: '-100.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
103
src/__tests__/unit/core/payments/payment.service.test.ts
Normal file
103
src/__tests__/unit/core/payments/payment.service.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// Payment Service Unit Tests
|
||||
|
||||
import { paymentService } from '@/core/payments/payment.service';
|
||||
import { PaymentPriority } from '@/shared/types';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
describe('PaymentService', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('processPayment', () => {
|
||||
it('should process a valid payment', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
accountNumber: 'SOURCE-001',
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destinationAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
accountNumber: 'DEST-001',
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
const result = await paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destinationAccount.accountNumber,
|
||||
amount: '100.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
});
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.paymentId).toBeDefined();
|
||||
|
||||
// Verify balances
|
||||
const updatedSource = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: sourceAccount.id },
|
||||
});
|
||||
const updatedDest = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: destinationAccount.id },
|
||||
});
|
||||
|
||||
expect(updatedSource?.balance.toString()).toBe('900.00');
|
||||
expect(updatedDest?.balance.toString()).toBe('600.00');
|
||||
});
|
||||
|
||||
it('should reject payment with insufficient balance', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '50.00',
|
||||
});
|
||||
const destinationAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destinationAccount.accountNumber,
|
||||
amount: '100.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should validate currency codes match', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const sourceAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
currencyCode: 'USD',
|
||||
balance: '1000.00',
|
||||
});
|
||||
const destinationAccount = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
currencyCode: 'EUR',
|
||||
});
|
||||
|
||||
await expect(
|
||||
paymentService.initiatePayment({
|
||||
debtorAccount: sourceAccount.accountNumber,
|
||||
creditorAccount: destinationAccount.accountNumber,
|
||||
amount: '100.00',
|
||||
currency: 'USD',
|
||||
priority: PaymentPriority.NORMAL,
|
||||
assetType: 'fiat',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
78
src/__tests__/unit/core/settlement/atomic-settlement.test.ts
Normal file
78
src/__tests__/unit/core/settlement/atomic-settlement.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Atomic Settlement Service Unit Tests
|
||||
|
||||
import { atomicSettlementService } from '@/core/settlement/isn/atomic-settlement.service';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
|
||||
describe('AtomicSettlementService', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('settleAtomically', () => {
|
||||
it('should settle multiple transactions atomically', async () => {
|
||||
const bank1 = await createTestSovereignBank({ sovereignCode: 'BANK1' });
|
||||
const bank2 = await createTestSovereignBank({ sovereignCode: 'BANK2' });
|
||||
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank1.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank2.id,
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
const result = await atomicSettlementService.settleAtomically({
|
||||
transactions: [
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account2.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
],
|
||||
referenceId: 'ATOMIC-001',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.settlementTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should rollback all transactions if any fails', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '50.00', // Insufficient for 100.00 transfer
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
await expect(
|
||||
atomicSettlementService.settleAtomically({
|
||||
transactions: [
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account2.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
],
|
||||
referenceId: 'ATOMIC-002',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
||||
// Verify no changes were made
|
||||
const updatedAccount1 = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: account1.id },
|
||||
});
|
||||
expect(updatedAccount1?.balance.toString()).toBe('50.00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// Comprehensive Settlement Tests
|
||||
|
||||
import { atomicSettlementService } from '@/core/settlement/isn/atomic-settlement.service';
|
||||
import { createTestSovereignBank, createTestBankAccount, cleanDatabase } from '@/__tests__/utils/test-db';
|
||||
import { testPrisma } from '@/__tests__/utils/test-db';
|
||||
|
||||
describe('Settlement - Comprehensive Tests', () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testPrisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('Cross-Chain Settlement', () => {
|
||||
it('should settle transactions across different asset chains', async () => {
|
||||
const bank1 = await createTestSovereignBank({ sovereignCode: 'BANK1' });
|
||||
const bank2 = await createTestSovereignBank({ sovereignCode: 'BANK2' });
|
||||
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank1.id,
|
||||
assetType: 'fiat',
|
||||
balance: '1000.00',
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank2.id,
|
||||
assetType: 'cbdc',
|
||||
balance: '500.00',
|
||||
});
|
||||
|
||||
const result = await atomicSettlementService.settleAtomically({
|
||||
transactions: [
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account2.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
],
|
||||
referenceId: 'CROSS-CHAIN-001',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Atomic Rollback', () => {
|
||||
it('should rollback all transactions if any fails', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '50.00',
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
const account3 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
// First transaction should succeed, second should fail
|
||||
await expect(
|
||||
atomicSettlementService.settleAtomically({
|
||||
transactions: [
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account2.id,
|
||||
amount: '30.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account3.id,
|
||||
amount: '30.00', // This will fail due to insufficient balance
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
],
|
||||
referenceId: 'ROLLBACK-001',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
||||
// Verify no changes were made (rollback worked)
|
||||
const updatedAccount1 = await testPrisma.bankAccount.findUnique({
|
||||
where: { id: account1.id },
|
||||
});
|
||||
expect(updatedAccount1?.balance.toString()).toBe('50.00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-Leg Transactions', () => {
|
||||
it('should settle multi-legged transactions atomically', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '500.00',
|
||||
});
|
||||
const account3 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '300.00',
|
||||
});
|
||||
|
||||
const result = await atomicSettlementService.settleAtomically({
|
||||
transactions: [
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account2.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
{
|
||||
sourceAccountId: account2.id,
|
||||
destinationAccountId: account3.id,
|
||||
amount: '50.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
],
|
||||
referenceId: 'MULTI-LEG-001',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.entryIds.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Settlement Time Performance', () => {
|
||||
it('should complete settlement within acceptable time', async () => {
|
||||
const bank = await createTestSovereignBank();
|
||||
const account1 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
balance: '1000.00',
|
||||
});
|
||||
const account2 = await createTestBankAccount({
|
||||
sovereignBankId: bank.id,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await atomicSettlementService.settleAtomically({
|
||||
transactions: [
|
||||
{
|
||||
sourceAccountId: account1.id,
|
||||
destinationAccountId: account2.id,
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
],
|
||||
referenceId: 'PERF-001',
|
||||
});
|
||||
const endTime = Date.now();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.settlementTime).toBeLessThan(1000); // Should complete in < 1 second
|
||||
expect(endTime - startTime).toBeLessThan(2000); // Actual time < 2 seconds
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
37
src/__tests__/utils/test-auth.ts
Normal file
37
src/__tests__/utils/test-auth.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Test Authentication Utilities
|
||||
// Helpers for authentication in tests
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { JwtPayload } from '@/shared/types';
|
||||
|
||||
const TEST_JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret-minimum-32-characters-long-for-testing';
|
||||
|
||||
/**
|
||||
* Create a test JWT token
|
||||
*/
|
||||
export function createTestToken(payload: Partial<JwtPayload>): string {
|
||||
const defaultPayload: JwtPayload = {
|
||||
sovereignBankId: 'test-bank-id',
|
||||
identityType: 'API',
|
||||
apiRole: 'user',
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
|
||||
...payload,
|
||||
};
|
||||
|
||||
return jwt.sign(defaultPayload, TEST_JWT_SECRET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test request headers with authentication
|
||||
*/
|
||||
export function createAuthHeaders(payload?: Partial<JwtPayload>): Record<string, string> {
|
||||
const token = createTestToken(payload || {});
|
||||
return {
|
||||
authorization: `SOV-TOKEN ${token}`,
|
||||
'x-sov-signature': 'test-signature',
|
||||
'x-sov-timestamp': Date.now().toString(),
|
||||
'x-sov-nonce': 'test-nonce',
|
||||
};
|
||||
}
|
||||
|
||||
86
src/__tests__/utils/test-db.ts
Normal file
86
src/__tests__/utils/test-db.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// Test Database Utilities
|
||||
// Helpers for database operations in tests
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
const testPrisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: process.env.TEST_DATABASE_URL || 'postgresql://test:test@localhost:5432/dbis_test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Clean database before each test
|
||||
*/
|
||||
export async function cleanDatabase(): Promise<void> {
|
||||
// Delete in reverse order of dependencies
|
||||
const tables = [
|
||||
'LedgerEntry',
|
||||
'BankAccount',
|
||||
'SovereignIdentity',
|
||||
'SovereignBank',
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
await (testPrisma as any)[table].deleteMany({});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test sovereign bank
|
||||
*/
|
||||
export async function createTestSovereignBank(data?: {
|
||||
sovereignCode?: string;
|
||||
name?: string;
|
||||
bic?: string;
|
||||
}) {
|
||||
return await testPrisma.sovereignBank.create({
|
||||
data: {
|
||||
sovereignCode: data?.sovereignCode || 'TEST',
|
||||
name: data?.name || 'Test Bank',
|
||||
bic: data?.bic || 'TESTXXXX',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test bank account
|
||||
*/
|
||||
export async function createTestBankAccount(data: {
|
||||
sovereignBankId: string;
|
||||
accountNumber?: string;
|
||||
accountType?: string;
|
||||
currencyCode?: string;
|
||||
balance?: string;
|
||||
}) {
|
||||
return await testPrisma.bankAccount.create({
|
||||
data: {
|
||||
accountNumber: data.accountNumber || `ACC-${Date.now()}`,
|
||||
sovereignBankId: data.sovereignBankId,
|
||||
accountType: data.accountType || 'commercial',
|
||||
currencyCode: data.currencyCode || 'USD',
|
||||
balance: new Decimal(data.balance || '0'),
|
||||
availableBalance: new Decimal(data.balance || '0'),
|
||||
reservedBalance: new Decimal('0'),
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback transaction wrapper for tests
|
||||
*/
|
||||
export async function withTransaction<T>(
|
||||
callback: (tx: PrismaClient) => Promise<T>
|
||||
): Promise<T> {
|
||||
return await testPrisma.$transaction(async (tx) => {
|
||||
return await callback(tx as PrismaClient);
|
||||
});
|
||||
}
|
||||
|
||||
export { testPrisma };
|
||||
|
||||
78
src/__tests__/utils/test-factories.ts
Normal file
78
src/__tests__/utils/test-factories.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Test Data Factories
|
||||
// Generate test data for various entities
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
export const testFactories = {
|
||||
/**
|
||||
* Create test sovereign bank data
|
||||
*/
|
||||
sovereignBank: (overrides?: Record<string, unknown>) => ({
|
||||
sovereignCode: `TEST-${uuidv4().substring(0, 8)}`,
|
||||
name: 'Test Sovereign Bank',
|
||||
bic: `TEST${uuidv4().substring(0, 4)}`,
|
||||
status: 'active',
|
||||
...overrides,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create test bank account data
|
||||
*/
|
||||
bankAccount: (sovereignBankId: string, overrides?: Record<string, unknown>) => ({
|
||||
accountNumber: `ACC-${uuidv4()}`,
|
||||
sovereignBankId,
|
||||
accountType: 'commercial',
|
||||
currencyCode: 'USD',
|
||||
balance: new Decimal('1000.00'),
|
||||
availableBalance: new Decimal('1000.00'),
|
||||
reservedBalance: new Decimal('0'),
|
||||
status: 'active',
|
||||
...overrides,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create test ledger entry data
|
||||
*/
|
||||
ledgerEntry: (
|
||||
debitAccountId: string,
|
||||
creditAccountId: string,
|
||||
overrides?: Record<string, unknown>
|
||||
) => ({
|
||||
ledgerId: 'MASTER',
|
||||
debitAccountId,
|
||||
creditAccountId,
|
||||
amount: new Decimal('100.00'),
|
||||
currencyCode: 'USD',
|
||||
assetType: 'fiat',
|
||||
transactionType: 'TYPE_A',
|
||||
referenceId: `REF-${uuidv4()}`,
|
||||
blockHash: `HASH-${uuidv4()}`,
|
||||
status: 'pending',
|
||||
...overrides,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create test payment data
|
||||
*/
|
||||
payment: (overrides?: Record<string, unknown>) => ({
|
||||
sourceAccountId: uuidv4(),
|
||||
destinationAccountId: uuidv4(),
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD',
|
||||
paymentType: 'transfer',
|
||||
...overrides,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create test FX order data
|
||||
*/
|
||||
fxOrder: (overrides?: Record<string, unknown>) => ({
|
||||
pair: 'USD/EUR',
|
||||
amount: '1000.00',
|
||||
orderType: 'market',
|
||||
settlement: 'RTGS',
|
||||
...overrides,
|
||||
}),
|
||||
};
|
||||
|
||||
206
src/core/accounting/accounting-standards.service.ts
Normal file
206
src/core/accounting/accounting-standards.service.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// DBIS Accounting Standards Service
|
||||
// Fair value marking, commodity feed integration
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
|
||||
export interface ValuationData {
|
||||
assetType: string;
|
||||
assetId: string;
|
||||
fairValue: number;
|
||||
currencyCode: string;
|
||||
valuationDate: Date;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export class AccountingStandardsService {
|
||||
/**
|
||||
* Get valuation rule for asset type
|
||||
*/
|
||||
async getValuationRule(assetType: string) {
|
||||
return await prisma.valuationRule.findFirst({
|
||||
where: {
|
||||
assetType,
|
||||
status: 'active',
|
||||
effectiveDate: {
|
||||
lte: new Date(),
|
||||
},
|
||||
OR: [
|
||||
{ expiryDate: null },
|
||||
{ expiryDate: { gte: new Date() } },
|
||||
],
|
||||
},
|
||||
orderBy: { effectiveDate: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark asset to fair value
|
||||
*/
|
||||
async markToFairValue(assetId: string, assetType: string, fairValue: number, currencyCode: string) {
|
||||
const rule = await this.getValuationRule(assetType);
|
||||
|
||||
if (!rule) {
|
||||
throw new Error(`No valuation rule found for asset type: ${assetType}`);
|
||||
}
|
||||
|
||||
// Update asset value based on type
|
||||
switch (assetType) {
|
||||
case 'commodity':
|
||||
await this.updateCommodityValue(assetId, fairValue);
|
||||
break;
|
||||
case 'security':
|
||||
await this.updateSecurityValue(assetId, fairValue);
|
||||
break;
|
||||
case 'fiat':
|
||||
case 'cbdc':
|
||||
// Fiat and CBDC are already at fair value
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported asset type for fair value marking: ${assetType}`);
|
||||
}
|
||||
|
||||
// Log valuation
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
eventType: 'valuation',
|
||||
entityType: assetType,
|
||||
entityId: assetId,
|
||||
action: 'mark_to_fair_value',
|
||||
details: {
|
||||
fairValue,
|
||||
currencyCode,
|
||||
valuationMethod: rule.valuationMethod,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update commodity value
|
||||
*/
|
||||
private async updateCommodityValue(commodityId: string, fairValue: number) {
|
||||
// In production, would update commodity price
|
||||
// For now, update commodity spot price if exists
|
||||
const commodity = await prisma.commodity.findFirst({
|
||||
where: {
|
||||
id: commodityId,
|
||||
},
|
||||
});
|
||||
|
||||
if (commodity) {
|
||||
await prisma.commodity.update({
|
||||
where: { id: commodityId },
|
||||
data: {
|
||||
spotPrice: new Decimal(fairValue),
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update security value
|
||||
*/
|
||||
private async updateSecurityValue(securityId: string, fairValue: number) {
|
||||
const security = await prisma.security.findFirst({
|
||||
where: {
|
||||
securityId,
|
||||
},
|
||||
});
|
||||
|
||||
if (security) {
|
||||
await prisma.security.update({
|
||||
where: { id: security.id },
|
||||
data: {
|
||||
price: new Decimal(fairValue),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commodity feed price
|
||||
*/
|
||||
async getCommodityFeedPrice(commodityType: string, unit: string): Promise<number | null> {
|
||||
const commodity = await prisma.commodity.findUnique({
|
||||
where: {
|
||||
commodityType_unit: {
|
||||
commodityType,
|
||||
unit,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!commodity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseFloat(commodity.spotPrice.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FX reference rate
|
||||
*/
|
||||
async getFXReferenceRate(baseCurrency: string, quoteCurrency: string): Promise<number | null> {
|
||||
const fxPair = await prisma.fxPair.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
baseCurrency,
|
||||
quoteCurrency,
|
||||
},
|
||||
{
|
||||
baseCurrency: quoteCurrency,
|
||||
quoteCurrency: baseCurrency,
|
||||
},
|
||||
],
|
||||
status: 'active',
|
||||
},
|
||||
include: {
|
||||
trades: {
|
||||
where: {
|
||||
status: 'settled',
|
||||
},
|
||||
orderBy: { timestampUtc: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!fxPair || fxPair.trades.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseFloat(fxPair.trades[0].price.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create valuation rule
|
||||
*/
|
||||
async createValuationRule(
|
||||
assetType: string,
|
||||
valuationMethod: string,
|
||||
feedSource?: string,
|
||||
updateFrequency: string = 'real_time'
|
||||
) {
|
||||
return await prisma.valuationRule.create({
|
||||
data: {
|
||||
id: require('uuid').v4(),
|
||||
ruleId: require('uuid').v4(),
|
||||
assetType,
|
||||
valuationMethod,
|
||||
feedSource,
|
||||
updateFrequency,
|
||||
status: 'active',
|
||||
effectiveDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const accountingStandardsService = new AccountingStandardsService();
|
||||
|
||||
422
src/core/accounting/reporting-engine.service.ts
Normal file
422
src/core/accounting/reporting-engine.service.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
// DBIS Reporting Engine Service
|
||||
// Generate consolidated statements, SCB reports
|
||||
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { accountService } from '@/core/accounts/account.service';
|
||||
import { treasuryService } from '@/core/treasury/treasury.service';
|
||||
import prisma from '@/shared/database/prisma';
|
||||
|
||||
export interface ConsolidatedStatementData {
|
||||
statementType: string;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
}
|
||||
|
||||
export interface SovereignReportData {
|
||||
sovereignBankId: string;
|
||||
reportType: string;
|
||||
reportPeriod: string;
|
||||
reportDate: Date;
|
||||
}
|
||||
|
||||
export class ReportingEngineService {
|
||||
/**
|
||||
* Generate Consolidated Sovereign Liquidity Report (CSLR)
|
||||
*/
|
||||
async generateCSLR(periodStart: Date, periodEnd: Date) {
|
||||
const banks = await prisma.sovereignBank.findMany({
|
||||
where: { status: 'active' },
|
||||
include: {
|
||||
liquidityPools: true,
|
||||
accounts: true,
|
||||
},
|
||||
});
|
||||
|
||||
const consolidatedData: Record<string, unknown> = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
reportDate: new Date(),
|
||||
totalBanks: banks.length,
|
||||
liquidityByCurrency: {},
|
||||
totalLiquidity: 0,
|
||||
bankDetails: [],
|
||||
};
|
||||
|
||||
for (const bank of banks) {
|
||||
const bankLiquidity = bank.liquidityPools.reduce(
|
||||
(sum, pool) => sum + parseFloat(pool.totalLiquidity.toString()),
|
||||
0
|
||||
);
|
||||
|
||||
const lcr = await treasuryService.calculateLCR(bank.id);
|
||||
const nsfr = await treasuryService.calculateNSFR(bank.id);
|
||||
|
||||
consolidatedData.bankDetails.push({
|
||||
sovereignBankId: bank.id,
|
||||
sovereignCode: bank.sovereignCode,
|
||||
name: bank.name,
|
||||
totalLiquidity: bankLiquidity,
|
||||
lcr,
|
||||
nsfr,
|
||||
});
|
||||
|
||||
// Aggregate by currency
|
||||
for (const pool of bank.liquidityPools) {
|
||||
const currency = pool.currencyCode;
|
||||
if (!consolidatedData.liquidityByCurrency[currency]) {
|
||||
consolidatedData.liquidityByCurrency[currency] = 0;
|
||||
}
|
||||
consolidatedData.liquidityByCurrency[currency] += parseFloat(pool.totalLiquidity.toString());
|
||||
}
|
||||
|
||||
consolidatedData.totalLiquidity += bankLiquidity;
|
||||
}
|
||||
|
||||
const statement = await prisma.consolidatedStatement.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
statementId: uuidv4(),
|
||||
statementType: 'CSLR',
|
||||
reportDate: new Date(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
status: 'final',
|
||||
statementData: consolidatedData,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Cross-Border Settlement Exposures Report
|
||||
*/
|
||||
async generateCrossBorderExposureReport(periodStart: Date, periodEnd: Date) {
|
||||
const settlements = await prisma.ledgerEntry.findMany({
|
||||
where: {
|
||||
timestampUtc: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
status: 'settled',
|
||||
},
|
||||
include: {
|
||||
debitAccount: {
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
},
|
||||
creditAccount: {
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const exposures: Record<string, unknown> = {};
|
||||
const bankPairs: Record<string, number> = {};
|
||||
|
||||
for (const settlement of settlements) {
|
||||
const debitBank = settlement.debitAccount.sovereignBank.sovereignCode;
|
||||
const creditBank = settlement.creditAccount.sovereignBank.sovereignCode;
|
||||
|
||||
if (debitBank !== creditBank) {
|
||||
const pairKey = `${debitBank}_${creditBank}`;
|
||||
const amount = parseFloat(settlement.amount.toString());
|
||||
|
||||
if (!bankPairs[pairKey]) {
|
||||
bankPairs[pairKey] = 0;
|
||||
}
|
||||
bankPairs[pairKey] += amount;
|
||||
|
||||
// Track exposure by bank
|
||||
if (!exposures[debitBank]) {
|
||||
exposures[debitBank] = { outbound: 0, inbound: 0 };
|
||||
}
|
||||
if (!exposures[creditBank]) {
|
||||
exposures[creditBank] = { outbound: 0, inbound: 0 };
|
||||
}
|
||||
|
||||
exposures[debitBank].outbound += amount;
|
||||
exposures[creditBank].inbound += amount;
|
||||
}
|
||||
}
|
||||
|
||||
const reportData = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
reportDate: new Date(),
|
||||
totalCrossBorderSettlements: settlements.filter(
|
||||
(s) => s.debitAccount.sovereignBankId !== s.creditAccount.sovereignBankId
|
||||
).length,
|
||||
exposures,
|
||||
bankPairs,
|
||||
};
|
||||
|
||||
const statement = await prisma.consolidatedStatement.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
statementId: uuidv4(),
|
||||
statementType: 'CrossBorderExposure',
|
||||
reportDate: new Date(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
status: 'final',
|
||||
statementData: reportData,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CBDC Reserve Adequacy Statement
|
||||
*/
|
||||
async generateCBDCReserveAdequacy(periodStart: Date, periodEnd: Date) {
|
||||
const cbdcIssuances = await prisma.cbdcIssuance.findMany({
|
||||
where: {
|
||||
timestampUtc: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
});
|
||||
|
||||
const adequacyData: Record<string, unknown> = {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
reportDate: new Date(),
|
||||
totalIssuances: cbdcIssuances.length,
|
||||
bankAdequacy: [],
|
||||
totalCBDCIssued: 0,
|
||||
totalReserveBacking: 0,
|
||||
};
|
||||
|
||||
for (const issuance of cbdcIssuances) {
|
||||
const bankIssuances = cbdcIssuances.filter(
|
||||
(i) => i.sovereignBankId === issuance.sovereignBankId
|
||||
);
|
||||
|
||||
const totalIssued = bankIssuances.reduce(
|
||||
(sum, i) => sum + parseFloat(i.amountMinted.toString()),
|
||||
0
|
||||
);
|
||||
const totalBacking = bankIssuances.reduce(
|
||||
(sum, i) => sum + parseFloat(i.reserveBacking?.toString() || '0'),
|
||||
0
|
||||
);
|
||||
|
||||
adequacyData.bankAdequacy.push({
|
||||
sovereignBankId: issuance.sovereignBankId,
|
||||
sovereignCode: issuance.sovereignBank.sovereignCode,
|
||||
totalCBDCIssued: totalIssued,
|
||||
totalReserveBacking: totalBacking,
|
||||
adequacyRatio: totalBacking > 0 ? totalIssued / totalBacking : 0,
|
||||
});
|
||||
|
||||
adequacyData.totalCBDCIssued += totalIssued;
|
||||
adequacyData.totalReserveBacking += totalBacking;
|
||||
}
|
||||
|
||||
const statement = await prisma.consolidatedStatement.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
statementId: uuidv4(),
|
||||
statementType: 'CBDCReserveAdequacy',
|
||||
reportDate: new Date(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
status: 'final',
|
||||
statementData: adequacyData,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB daily liquidity window report
|
||||
*/
|
||||
async generateDailyLiquidityReport(sovereignBankId: string, reportDate: Date) {
|
||||
const lcr = await treasuryService.calculateLCR(sovereignBankId);
|
||||
const nsfr = await treasuryService.calculateNSFR(sovereignBankId);
|
||||
const accounts = await accountService.getAccountsBySovereign(sovereignBankId);
|
||||
|
||||
const liquidityData = {
|
||||
reportDate,
|
||||
lcr,
|
||||
nsfr,
|
||||
totalAccounts: accounts.length,
|
||||
totalBalance: accounts.reduce((sum, acc) => sum + parseFloat(acc.balance), 0),
|
||||
availableBalance: accounts.reduce((sum, acc) => sum + parseFloat(acc.availableBalance), 0),
|
||||
reservedBalance: accounts.reduce((sum, acc) => sum + parseFloat(acc.reservedBalance), 0),
|
||||
};
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'daily_liquidity',
|
||||
reportPeriod: 'daily',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getTime() + 24 * 60 * 60 * 1000), // Next day
|
||||
status: 'submitted',
|
||||
reportData: liquidityData,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB weekly FX reserve update
|
||||
*/
|
||||
async generateWeeklyFXReserveReport(sovereignBankId: string, reportDate: Date) {
|
||||
const accounts = await accountService.getAccountsBySovereign(sovereignBankId);
|
||||
const fxReserves: Record<string, number> = {};
|
||||
|
||||
for (const account of accounts) {
|
||||
if (account.assetType === 'fiat' || account.assetType === 'cbdc') {
|
||||
if (!fxReserves[account.currencyCode]) {
|
||||
fxReserves[account.currencyCode] = 0;
|
||||
}
|
||||
fxReserves[account.currencyCode] += parseFloat(account.balance);
|
||||
}
|
||||
}
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'weekly_fx_reserve',
|
||||
reportPeriod: 'weekly',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getTime() + 7 * 24 * 60 * 60 * 1000), // Next week
|
||||
status: 'submitted',
|
||||
reportData: {
|
||||
reportDate,
|
||||
fxReserves,
|
||||
totalReserves: Object.values(fxReserves).reduce((sum, val) => sum + val, 0),
|
||||
},
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB monthly AML compliance results
|
||||
*/
|
||||
async generateMonthlyAMLComplianceReport(sovereignBankId: string, reportDate: Date) {
|
||||
const monthStart = new Date(reportDate.getFullYear(), reportDate.getMonth(), 1);
|
||||
const monthEnd = new Date(reportDate.getFullYear(), reportDate.getMonth() + 1, 0);
|
||||
|
||||
const complianceRecords = await prisma.complianceRecord.findMany({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
createdAt: {
|
||||
gte: monthStart,
|
||||
lte: monthEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reportData = {
|
||||
reportDate,
|
||||
monthStart,
|
||||
monthEnd,
|
||||
totalChecks: complianceRecords.length,
|
||||
clearCount: complianceRecords.filter((r) => r.status === 'clear').length,
|
||||
flaggedCount: complianceRecords.filter((r) => r.status === 'flagged').length,
|
||||
blockedCount: complianceRecords.filter((r) => r.status === 'blocked').length,
|
||||
averageRiskScore: complianceRecords.length > 0
|
||||
? complianceRecords.reduce((sum, r) => sum + r.riskScore, 0) / complianceRecords.length
|
||||
: 0,
|
||||
};
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'monthly_aml_compliance',
|
||||
reportPeriod: 'monthly',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getFullYear(), reportDate.getMonth() + 1, 15), // 15th of next month
|
||||
status: 'submitted',
|
||||
reportData,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SCB quarterly CBDC issuance audit
|
||||
*/
|
||||
async generateQuarterlyCBDCAudit(sovereignBankId: string, reportDate: Date) {
|
||||
const quarterStart = new Date(reportDate.getFullYear(), Math.floor(reportDate.getMonth() / 3) * 3, 1);
|
||||
const quarterEnd = new Date(reportDate.getFullYear(), Math.floor(reportDate.getMonth() / 3) * 3 + 3, 0);
|
||||
|
||||
const issuances = await prisma.cbdcIssuance.findMany({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
timestampUtc: {
|
||||
gte: quarterStart,
|
||||
lte: quarterEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reportData = {
|
||||
reportDate,
|
||||
quarterStart,
|
||||
quarterEnd,
|
||||
totalIssuances: issuances.length,
|
||||
totalMinted: issuances.reduce((sum, i) => sum + parseFloat(i.amountMinted.toString()), 0),
|
||||
totalBurned: issuances.reduce((sum, i) => sum + parseFloat(i.amountBurned.toString()), 0),
|
||||
netChange: issuances.reduce((sum, i) => sum + parseFloat(i.netChange.toString()), 0),
|
||||
issuances: issuances.map((i) => ({
|
||||
recordId: i.recordId,
|
||||
operationType: i.operationType,
|
||||
amountMinted: parseFloat(i.amountMinted.toString()),
|
||||
amountBurned: parseFloat(i.amountBurned.toString()),
|
||||
reserveBacking: i.reserveBacking ? parseFloat(i.reserveBacking.toString()) : null,
|
||||
timestampUtc: i.timestampUtc,
|
||||
})),
|
||||
};
|
||||
|
||||
const report = await prisma.sovereignReport.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
sovereignBankId,
|
||||
reportId: uuidv4(),
|
||||
reportType: 'quarterly_cbdc_audit',
|
||||
reportPeriod: 'quarterly',
|
||||
reportDate,
|
||||
dueDate: new Date(reportDate.getFullYear(), Math.floor(reportDate.getMonth() / 3) * 3 + 3, 15),
|
||||
status: 'submitted',
|
||||
reportData,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
export const reportingEngineService = new ReportingEngineService();
|
||||
|
||||
192
src/core/accounting/valuation.service.ts
Normal file
192
src/core/accounting/valuation.service.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Valuation Service
|
||||
// Real-time fair value calculation
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { accountingStandardsService } from './accounting-standards.service';
|
||||
|
||||
|
||||
export class ValuationService {
|
||||
/**
|
||||
* Calculate real-time fair value for an asset
|
||||
*/
|
||||
async calculateFairValue(assetType: string, assetId: string, currencyCode: string): Promise<number> {
|
||||
const rule = await accountingStandardsService.getValuationRule(assetType);
|
||||
|
||||
if (!rule) {
|
||||
throw new Error(`No valuation rule found for asset type: ${assetType}`);
|
||||
}
|
||||
|
||||
switch (rule.valuationMethod) {
|
||||
case 'fair_value':
|
||||
return await this.calculateFairValueDirect(assetType, assetId, currencyCode);
|
||||
case 'commodity_feed':
|
||||
return await this.calculateFromCommodityFeed(assetType, assetId, currencyCode);
|
||||
case 'fx_reference_rate':
|
||||
return await this.calculateFromFXRate(assetType, assetId, currencyCode);
|
||||
default:
|
||||
throw new Error(`Unsupported valuation method: ${rule.valuationMethod}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fair value directly (for fiat, CBDC)
|
||||
*/
|
||||
private async calculateFairValueDirect(
|
||||
assetType: string,
|
||||
assetId: string,
|
||||
currencyCode: string
|
||||
): Promise<number> {
|
||||
if (assetType === 'fiat' || assetType === 'cbdc') {
|
||||
// Fiat and CBDC are already at fair value (1:1)
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: assetId },
|
||||
});
|
||||
|
||||
if (account) {
|
||||
return parseFloat(account.balance.toString());
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Cannot calculate fair value directly for asset type: ${assetType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate from commodity feed
|
||||
*/
|
||||
private async calculateFromCommodityFeed(
|
||||
assetType: string,
|
||||
assetId: string,
|
||||
currencyCode: string
|
||||
): Promise<number> {
|
||||
if (assetType !== 'commodity') {
|
||||
throw new Error('Commodity feed valuation only applies to commodities');
|
||||
}
|
||||
|
||||
// Get commodity
|
||||
const commodity = await prisma.commodity.findFirst({
|
||||
where: { id: assetId },
|
||||
});
|
||||
|
||||
if (!commodity) {
|
||||
throw new Error(`Commodity not found: ${assetId}`);
|
||||
}
|
||||
|
||||
// Get current price from feed
|
||||
const price = await accountingStandardsService.getCommodityFeedPrice(
|
||||
commodity.commodityType,
|
||||
commodity.unit
|
||||
);
|
||||
|
||||
if (!price) {
|
||||
throw new Error(`No price feed available for commodity: ${commodity.commodityType}`);
|
||||
}
|
||||
|
||||
// Get quantity from account or sub-ledger
|
||||
const account = await prisma.bankAccount.findFirst({
|
||||
where: {
|
||||
assetType: 'commodity',
|
||||
currencyCode: commodity.commodityType,
|
||||
},
|
||||
});
|
||||
|
||||
const quantity = account ? parseFloat(account.balance.toString()) : 0;
|
||||
|
||||
return price * quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate from FX reference rate
|
||||
*/
|
||||
private async calculateFromFXRate(
|
||||
assetType: string,
|
||||
assetId: string,
|
||||
currencyCode: string
|
||||
): Promise<number> {
|
||||
// Get account
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: assetId },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new Error(`Account not found: ${assetId}`);
|
||||
}
|
||||
|
||||
const baseAmount = parseFloat(account.balance.toString());
|
||||
|
||||
// If account currency matches target currency, no conversion needed
|
||||
if (account.currencyCode === currencyCode) {
|
||||
return baseAmount;
|
||||
}
|
||||
|
||||
// Get FX rate
|
||||
const fxRate = await accountingStandardsService.getFXReferenceRate(
|
||||
account.currencyCode,
|
||||
currencyCode
|
||||
);
|
||||
|
||||
if (!fxRate) {
|
||||
throw new Error(
|
||||
`No FX rate available for ${account.currencyCode}/${currencyCode}`
|
||||
);
|
||||
}
|
||||
|
||||
return baseAmount * fxRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all assets to fair value (batch operation)
|
||||
*/
|
||||
async markAllToFairValue(sovereignBankId?: string) {
|
||||
const where: { assetType?: string; sovereignBankId?: string } = {};
|
||||
|
||||
if (sovereignBankId) {
|
||||
where.sovereignBankId = sovereignBankId;
|
||||
}
|
||||
|
||||
const accounts = await prisma.bankAccount.findMany({
|
||||
where,
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
},
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const account of accounts) {
|
||||
try {
|
||||
const fairValue = await this.calculateFairValue(
|
||||
account.assetType,
|
||||
account.id,
|
||||
account.currencyCode
|
||||
);
|
||||
|
||||
await accountingStandardsService.markToFairValue(
|
||||
account.id,
|
||||
account.assetType,
|
||||
fairValue,
|
||||
account.currencyCode
|
||||
);
|
||||
|
||||
results.push({
|
||||
accountId: account.id,
|
||||
assetType: account.assetType,
|
||||
fairValue,
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
accountId: account.id,
|
||||
assetType: account.assetType,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export const valuationService = new ValuationService();
|
||||
|
||||
117
src/core/accounts/account.routes.ts
Normal file
117
src/core/accounts/account.routes.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Accounts
|
||||
* description: Bank Account Management
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { zeroTrustAuthMiddleware } from '@/integration/api-gateway/middleware/auth.middleware';
|
||||
import { accountService } from './account.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts:
|
||||
* post:
|
||||
* summary: Create a new bank account
|
||||
* description: Create a new account for a sovereign bank
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - SovereignToken: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - accountType
|
||||
* - currencyCode
|
||||
* properties:
|
||||
* accountType:
|
||||
* type: string
|
||||
* enum: [sovereign, treasury, commercial, correspondent, settlement]
|
||||
* currencyCode:
|
||||
* type: string
|
||||
* description: ISO 4217 currency code
|
||||
* example: "USD"
|
||||
* assetType:
|
||||
* type: string
|
||||
* enum: [fiat, cbdc, commodity, security]
|
||||
* default: fiat
|
||||
* reserveRequirement:
|
||||
* type: string
|
||||
* description: Reserve requirement percentage
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Account created successfully
|
||||
* 400:
|
||||
* description: Validation error
|
||||
*/
|
||||
router.post('/', zeroTrustAuthMiddleware, async (req, res, next) => {
|
||||
try {
|
||||
const sovereignBankId = (req as any).sovereignBankId;
|
||||
const account = await accountService.createAccount(
|
||||
sovereignBankId,
|
||||
req.body.accountType,
|
||||
req.body.currencyCode,
|
||||
req.body.assetType,
|
||||
req.body.reserveRequirement
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: account,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts/{id}:
|
||||
* get:
|
||||
* summary: Get account by ID
|
||||
* description: Retrieve account details
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - SovereignToken: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Account retrieved
|
||||
* 404:
|
||||
* description: Account not found
|
||||
*/
|
||||
router.get('/:id', zeroTrustAuthMiddleware, async (req, res, next) => {
|
||||
try {
|
||||
const account = await accountService.getAccount(req.params.id);
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: { code: 'NOT_FOUND', message: 'Account not found' },
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: account,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
205
src/core/accounts/account.service.ts
Normal file
205
src/core/accounts/account.service.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
// Account Management Engine
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { AccountType, AssetType, BankAccount } from '@/shared/types';
|
||||
import { DbisError, ErrorCode } from '@/shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class AccountService {
|
||||
/**
|
||||
* Create a new bank account
|
||||
*/
|
||||
async createAccount(
|
||||
sovereignBankId: string,
|
||||
accountType: AccountType,
|
||||
currencyCode: string,
|
||||
assetType: AssetType = AssetType.FIAT,
|
||||
reserveRequirement?: string
|
||||
): Promise<BankAccount> {
|
||||
const accountNumber = this.generateAccountNumber(sovereignBankId, accountType);
|
||||
|
||||
const account = await prisma.bankAccount.create({
|
||||
data: {
|
||||
accountNumber,
|
||||
sovereignBankId,
|
||||
accountType,
|
||||
currencyCode,
|
||||
assetType,
|
||||
balance: new Decimal(0),
|
||||
availableBalance: new Decimal(0),
|
||||
reservedBalance: new Decimal(0),
|
||||
reserveRequirement: reserveRequirement ? new Decimal(reserveRequirement) : null,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToBankAccount(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account by ID
|
||||
*/
|
||||
async getAccount(accountId: string): Promise<BankAccount | null> {
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
});
|
||||
|
||||
return account ? this.mapToBankAccount(account) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account by account number
|
||||
*/
|
||||
async getAccountByNumber(accountNumber: string): Promise<BankAccount | null> {
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { accountNumber },
|
||||
});
|
||||
|
||||
return account ? this.mapToBankAccount(account) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accounts for a sovereign bank
|
||||
*/
|
||||
async getAccountsBySovereign(
|
||||
sovereignBankId: string,
|
||||
accountType?: AccountType
|
||||
): Promise<BankAccount[]> {
|
||||
const where: any = { sovereignBankId };
|
||||
if (accountType) {
|
||||
where.accountType = accountType;
|
||||
}
|
||||
|
||||
const accounts = await prisma.bankAccount.findMany({
|
||||
where,
|
||||
});
|
||||
|
||||
return accounts.map(this.mapToBankAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate account balance
|
||||
*/
|
||||
async calculateBalance(accountId: string): Promise<{
|
||||
balance: string;
|
||||
availableBalance: string;
|
||||
reservedBalance: string;
|
||||
}> {
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new DbisError(ErrorCode.NOT_FOUND, 'Account not found');
|
||||
}
|
||||
|
||||
return {
|
||||
balance: account.balance.toString(),
|
||||
availableBalance: account.availableBalance.toString(),
|
||||
reservedBalance: account.reservedBalance.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserve balance
|
||||
*/
|
||||
async reserveBalance(accountId: string, amount: string): Promise<void> {
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new DbisError(ErrorCode.NOT_FOUND, 'Account not found');
|
||||
}
|
||||
|
||||
const amountDecimal = new Decimal(amount);
|
||||
if (account.availableBalance.lt(amountDecimal)) {
|
||||
throw new DbisError(ErrorCode.VALIDATION_ERROR, 'Insufficient available balance');
|
||||
}
|
||||
|
||||
await prisma.bankAccount.update({
|
||||
where: { id: accountId },
|
||||
data: {
|
||||
availableBalance: account.availableBalance.minus(amountDecimal),
|
||||
reservedBalance: account.reservedBalance.plus(amountDecimal),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Release reserved balance
|
||||
*/
|
||||
async releaseReservedBalance(accountId: string, amount: string): Promise<void> {
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new DbisError(ErrorCode.NOT_FOUND, 'Account not found');
|
||||
}
|
||||
|
||||
const amountDecimal = new Decimal(amount);
|
||||
if (account.reservedBalance.lt(amountDecimal)) {
|
||||
throw new DbisError(ErrorCode.VALIDATION_ERROR, 'Insufficient reserved balance');
|
||||
}
|
||||
|
||||
await prisma.bankAccount.update({
|
||||
where: { id: accountId },
|
||||
data: {
|
||||
availableBalance: account.availableBalance.plus(amountDecimal),
|
||||
reservedBalance: account.reservedBalance.minus(amountDecimal),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check reserve requirements
|
||||
*/
|
||||
async checkReserveRequirements(accountId: string): Promise<boolean> {
|
||||
const account = await prisma.bankAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
});
|
||||
|
||||
if (!account || !account.reserveRequirement) {
|
||||
return true; // No requirement
|
||||
}
|
||||
|
||||
const requiredReserve = account.balance.times(account.reserveRequirement);
|
||||
const actualReserve = account.reservedBalance;
|
||||
|
||||
return actualReserve.gte(requiredReserve);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate account number
|
||||
*/
|
||||
private generateAccountNumber(sovereignBankId: string, accountType: AccountType): string {
|
||||
const prefix = accountType.substring(0, 3).toUpperCase();
|
||||
const timestamp = Date.now().toString(36).toUpperCase();
|
||||
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
||||
return `${prefix}-${sovereignBankId.substring(0, 4)}-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Prisma model to BankAccount type
|
||||
*/
|
||||
private mapToBankAccount(account: any): BankAccount {
|
||||
return {
|
||||
id: account.id,
|
||||
accountNumber: account.accountNumber,
|
||||
sovereignBankId: account.sovereignBankId,
|
||||
accountType: account.accountType as AccountType,
|
||||
currencyCode: account.currencyCode,
|
||||
assetType: account.assetType as AssetType,
|
||||
balance: account.balance.toString(),
|
||||
availableBalance: account.availableBalance.toString(),
|
||||
reservedBalance: account.reservedBalance.toString(),
|
||||
reserveRequirement: account.reserveRequirement?.toString(),
|
||||
status: account.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const accountService = new AccountService();
|
||||
|
||||
127
src/core/admin/dbis-admin/controls/corridor-controls.service.ts
Normal file
127
src/core/admin/dbis-admin/controls/corridor-controls.service.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// DBIS Admin Console - Corridor Controls Service
|
||||
// Corridor caps, throttling, enable/disable
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { adminAuditService } from '@/core/admin/shared/admin-audit.service';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface CorridorCapUpdate {
|
||||
routeId: string;
|
||||
newCap: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CorridorThrottleRequest {
|
||||
routeId: string;
|
||||
throttleRate: number; // 0-1, where 1 = no throttling, 0.5 = 50% throttling
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CorridorEnableDisable {
|
||||
routeId: string;
|
||||
action: 'enable' | 'disable';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export class CorridorControlsService {
|
||||
/**
|
||||
* Adjust corridor caps
|
||||
*/
|
||||
async adjustCorridorCaps(
|
||||
employeeId: string,
|
||||
update: CorridorCapUpdate
|
||||
): Promise<{ success: boolean }> {
|
||||
const route = await prisma.settlementRoute.findUnique({
|
||||
where: { routeId: update.routeId },
|
||||
});
|
||||
|
||||
if (!route) {
|
||||
throw new Error('Route not found');
|
||||
}
|
||||
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'adjust_corridor_caps',
|
||||
permission: AdminPermission.CORRIDOR_ADJUST_CAPS,
|
||||
resourceType: 'settlement_route',
|
||||
resourceId: update.routeId,
|
||||
beforeState: { cap: route.sireCost?.toString() },
|
||||
afterState: { cap: update.newCap.toString() },
|
||||
metadata: update,
|
||||
});
|
||||
|
||||
// Update route (would need to add cap field to schema or use existing fields)
|
||||
logger.info('Corridor cap adjusted', {
|
||||
employeeId,
|
||||
update,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle corridor
|
||||
*/
|
||||
async throttleCorridor(
|
||||
employeeId: string,
|
||||
request: CorridorThrottleRequest
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'throttle_corridor',
|
||||
permission: AdminPermission.CORRIDOR_THROTTLE,
|
||||
resourceType: 'settlement_route',
|
||||
resourceId: request.routeId,
|
||||
metadata: request,
|
||||
});
|
||||
|
||||
// Update route status or add throttling config
|
||||
logger.info('Corridor throttled', {
|
||||
employeeId,
|
||||
request,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable corridor
|
||||
*/
|
||||
async enableDisableCorridor(
|
||||
employeeId: string,
|
||||
request: CorridorEnableDisable
|
||||
): Promise<{ success: boolean }> {
|
||||
const route = await prisma.settlementRoute.findUnique({
|
||||
where: { routeId: request.routeId },
|
||||
});
|
||||
|
||||
if (!route) {
|
||||
throw new Error('Route not found');
|
||||
}
|
||||
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: request.action === 'enable' ? 'enable_corridor' : 'disable_corridor',
|
||||
permission: AdminPermission.CORRIDOR_ENABLE_DISABLE,
|
||||
resourceType: 'settlement_route',
|
||||
resourceId: request.routeId,
|
||||
beforeState: { status: route.status },
|
||||
afterState: { status: request.action === 'enable' ? 'active' : 'inactive' },
|
||||
metadata: request,
|
||||
});
|
||||
|
||||
await prisma.settlementRoute.update({
|
||||
where: { routeId: request.routeId },
|
||||
data: {
|
||||
status: request.action === 'enable' ? 'active' : 'inactive',
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const corridorControlsService = new CorridorControlsService();
|
||||
|
||||
183
src/core/admin/dbis-admin/controls/gru-controls.service.ts
Normal file
183
src/core/admin/dbis-admin/controls/gru-controls.service.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
// DBIS Admin Console - GRU Controls Service
|
||||
// GRU issuance, locks, circuit breakers, bond issuance windows
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { adminAuditService } from '@/core/admin/shared/admin-audit.service';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
|
||||
export interface GRUIssuanceProposal {
|
||||
gruClass: string; // M00, M0, M1, SR-1, SR-2, SR-3
|
||||
amount: number;
|
||||
reason: string;
|
||||
effectiveDate?: Date;
|
||||
}
|
||||
|
||||
export interface GRULockRequest {
|
||||
gruClass: string;
|
||||
action: 'lock' | 'unlock';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface GRUCircuitBreakerConfig {
|
||||
indexId: string;
|
||||
maxIntradayMove: number; // e.g., 0.05 for 5%
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface GRUBondIssuanceWindow {
|
||||
bondId: string;
|
||||
action: 'open' | 'close';
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class GRUControlsService {
|
||||
/**
|
||||
* Create GRU issuance proposal
|
||||
*/
|
||||
async createIssuanceProposal(
|
||||
employeeId: string,
|
||||
proposal: GRUIssuanceProposal
|
||||
): Promise<{ proposalId: string; status: string }> {
|
||||
// Log action
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'create_gru_issuance_proposal',
|
||||
permission: AdminPermission.GRU_ISSUANCE_PROPOSAL,
|
||||
resourceType: 'gru_issuance',
|
||||
metadata: proposal,
|
||||
});
|
||||
|
||||
// Create proposal (would go through governance workflow)
|
||||
const proposalId = uuidv4();
|
||||
|
||||
logger.info('GRU issuance proposal created', {
|
||||
proposalId,
|
||||
employeeId,
|
||||
proposal,
|
||||
});
|
||||
|
||||
return {
|
||||
proposalId,
|
||||
status: 'pending_governance_approval',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock or unlock GRU class
|
||||
*/
|
||||
async lockUnlockGRUClass(
|
||||
employeeId: string,
|
||||
request: GRULockRequest
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: request.action === 'lock' ? 'lock_gru_class' : 'unlock_gru_class',
|
||||
permission: AdminPermission.GRU_LOCK_UNLOCK,
|
||||
resourceType: 'gru_class',
|
||||
resourceId: request.gruClass,
|
||||
metadata: request,
|
||||
});
|
||||
|
||||
// Update GRU unit status (placeholder - would need proper implementation)
|
||||
logger.info('GRU class lock/unlock', {
|
||||
employeeId,
|
||||
request,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `GRU class ${request.gruClass} ${request.action === 'lock' ? 'locked' : 'unlocked'}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set circuit breakers for GRU index
|
||||
*/
|
||||
async setCircuitBreakers(
|
||||
employeeId: string,
|
||||
config: GRUCircuitBreakerConfig
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'set_gru_circuit_breakers',
|
||||
permission: AdminPermission.GRU_CIRCUIT_BREAKERS,
|
||||
resourceType: 'gru_index',
|
||||
resourceId: config.indexId,
|
||||
beforeState: {},
|
||||
afterState: config,
|
||||
});
|
||||
|
||||
// Update GRU index
|
||||
await prisma.gruIndex.updateMany({
|
||||
where: { indexId: config.indexId },
|
||||
data: {
|
||||
circuitBreakerEnabled: config.enabled,
|
||||
circuitBreakerThreshold: config.maxIntradayMove,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or close GRU bond issuance window
|
||||
*/
|
||||
async manageBondIssuanceWindow(
|
||||
employeeId: string,
|
||||
request: GRUBondIssuanceWindow
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: request.action === 'open' ? 'open_bond_issuance_window' : 'close_bond_issuance_window',
|
||||
permission: AdminPermission.GRU_BOND_ISSUANCE_WINDOW,
|
||||
resourceType: 'gru_bond',
|
||||
resourceId: request.bondId,
|
||||
metadata: request,
|
||||
});
|
||||
|
||||
// Update bond
|
||||
await prisma.gruBond.updateMany({
|
||||
where: { bondId: request.bondId },
|
||||
data: {
|
||||
issuanceWindowOpen: request.action === 'open',
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger emergency buy-back
|
||||
*/
|
||||
async triggerEmergencyBuyback(
|
||||
employeeId: string,
|
||||
bondId: string,
|
||||
amount: number
|
||||
): Promise<{ success: boolean; transactionId: string }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'trigger_emergency_buyback',
|
||||
permission: AdminPermission.GRU_BOND_BUYBACK,
|
||||
resourceType: 'gru_bond',
|
||||
resourceId: bondId,
|
||||
metadata: { amount },
|
||||
});
|
||||
|
||||
// Would require multi-sig/governance confirmation in production
|
||||
const transactionId = uuidv4();
|
||||
|
||||
logger.warn('Emergency buy-back triggered', {
|
||||
employeeId,
|
||||
bondId,
|
||||
amount,
|
||||
transactionId,
|
||||
});
|
||||
|
||||
return { success: true, transactionId };
|
||||
}
|
||||
}
|
||||
|
||||
export const gruControlsService = new GRUControlsService();
|
||||
|
||||
110
src/core/admin/dbis-admin/controls/network-controls.service.ts
Normal file
110
src/core/admin/dbis-admin/controls/network-controls.service.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// DBIS Admin Console - Network Controls Service
|
||||
// Subsystem quiesce, kill-switches, incident escalation
|
||||
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { adminAuditService } from '@/core/admin/shared/admin-audit.service';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
|
||||
export interface SubsystemQuiesceRequest {
|
||||
subsystem: string; // GAS, QPS, Ω-Layer, GPN, GRU Engine, Metaverse MEN, 6G Edge Grid
|
||||
action: 'quiesce' | 'resume';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface KillSwitchRequest {
|
||||
scope: 'global' | 'scb' | 'corridor';
|
||||
targetId?: string; // SCB ID or corridor ID if scope is not global
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface IncidentEscalation {
|
||||
incidentId: string;
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
assignTo?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class NetworkControlsService {
|
||||
/**
|
||||
* Quiesce or resume subsystem
|
||||
*/
|
||||
async quiesceSubsystem(
|
||||
employeeId: string,
|
||||
request: SubsystemQuiesceRequest
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: request.action === 'quiesce' ? 'quiesce_subsystem' : 'resume_subsystem',
|
||||
permission: AdminPermission.NETWORK_QUIESCE_SUBSYSTEM,
|
||||
resourceType: 'network_subsystem',
|
||||
resourceId: request.subsystem,
|
||||
metadata: request,
|
||||
});
|
||||
|
||||
// Would integrate with actual subsystem control
|
||||
logger.warn('Subsystem quiesce/resume', {
|
||||
employeeId,
|
||||
request,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Subsystem ${request.subsystem} ${request.action === 'quiesce' ? 'quiesced' : 'resumed'}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate kill switch
|
||||
*/
|
||||
async activateKillSwitch(
|
||||
employeeId: string,
|
||||
request: KillSwitchRequest
|
||||
): Promise<{ success: boolean; killSwitchId: string }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'activate_kill_switch',
|
||||
permission: AdminPermission.NETWORK_KILL_SWITCH,
|
||||
resourceType: 'network',
|
||||
resourceId: request.targetId || 'global',
|
||||
metadata: request,
|
||||
});
|
||||
|
||||
// Critical action - would require additional confirmation in production
|
||||
logger.error('KILL SWITCH ACTIVATED', {
|
||||
employeeId,
|
||||
request,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
killSwitchId: `killswitch-${Date.now()}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escalate incident
|
||||
*/
|
||||
async escalateIncident(
|
||||
employeeId: string,
|
||||
escalation: IncidentEscalation
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'escalate_incident',
|
||||
permission: AdminPermission.NETWORK_ESCALATE_INCIDENT,
|
||||
resourceType: 'incident',
|
||||
resourceId: escalation.incidentId,
|
||||
metadata: escalation,
|
||||
});
|
||||
|
||||
logger.info('Incident escalated', {
|
||||
employeeId,
|
||||
escalation,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const networkControlsService = new NetworkControlsService();
|
||||
|
||||
225
src/core/admin/dbis-admin/dashboards/cbdc-fx.service.ts
Normal file
225
src/core/admin/dbis-admin/dashboards/cbdc-fx.service.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
// DBIS Admin Console - CBDC & FX Service
|
||||
// CBDC wallet schemas, FX/GRU/SSU routing
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface CBDCSchema {
|
||||
scbId: string;
|
||||
scbName: string;
|
||||
cbdcTypes: Array<{
|
||||
type: string; // rCBDC, wCBDC, iCBDC
|
||||
status: 'active' | 'pending' | 'suspended';
|
||||
inCirculation: number;
|
||||
}>;
|
||||
crossBorderCorridors: Array<{
|
||||
targetSCB: string;
|
||||
settlementAsset: string; // SSU, GRU
|
||||
status: 'active' | 'pending';
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface FXRouting {
|
||||
corridors: Array<{
|
||||
sourceSCB: string;
|
||||
destinationSCB: string;
|
||||
preferredSettlementAsset: string;
|
||||
volume24h: number;
|
||||
spreads: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
fees: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
circuitBreakers: {
|
||||
enabled: boolean;
|
||||
volatilityThreshold: number;
|
||||
};
|
||||
}>;
|
||||
ssuUsage: {
|
||||
totalVolume: number;
|
||||
activeCorridors: number;
|
||||
};
|
||||
gruBridgeUsage: {
|
||||
totalVolume: number;
|
||||
activeCorridors: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CBDCFXDashboard {
|
||||
cbdcSchemas: CBDCSchema[];
|
||||
fxRouting: FXRouting;
|
||||
}
|
||||
|
||||
export class CBDCFXService {
|
||||
/**
|
||||
* Get CBDC & FX dashboard
|
||||
*/
|
||||
async getCBDCFXDashboard(): Promise<CBDCFXDashboard> {
|
||||
const [cbdcSchemas, fxRouting] = await Promise.all([
|
||||
this.getCBDCSchemas(),
|
||||
this.getFXRouting(),
|
||||
]);
|
||||
|
||||
return { cbdcSchemas, fxRouting };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CBDC schemas across all SCBs
|
||||
*/
|
||||
async getCBDCSchemas(): Promise<CBDCSchema[]> {
|
||||
const scbs = await prisma.sovereignBank.findMany({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
const schemas: CBDCSchema[] = [];
|
||||
|
||||
for (const scb of scbs) {
|
||||
// Get CBDC issuances
|
||||
const issuances = await prisma.cbdcIssuance.findMany({
|
||||
where: { sovereignBankId: scb.id },
|
||||
});
|
||||
|
||||
// Get CBDC wallets
|
||||
const wallets = await prisma.cbdcWallet.findMany({
|
||||
where: { sovereignBankId: scb.id },
|
||||
});
|
||||
|
||||
// Group by type
|
||||
const cbdcTypes = [
|
||||
{
|
||||
type: 'rCBDC',
|
||||
status: 'active' as const,
|
||||
inCirculation: wallets
|
||||
.filter((w) => w.walletType === 'retail')
|
||||
.reduce((sum, w) => sum.plus(w.balance), new Decimal(0))
|
||||
.toNumber(),
|
||||
},
|
||||
{
|
||||
type: 'wCBDC',
|
||||
status: 'active' as const,
|
||||
inCirculation: wallets
|
||||
.filter((w) => w.walletType === 'wholesale')
|
||||
.reduce((sum, w) => sum.plus(w.balance), new Decimal(0))
|
||||
.toNumber(),
|
||||
},
|
||||
{
|
||||
type: 'iCBDC',
|
||||
status: 'active' as const,
|
||||
inCirculation: wallets
|
||||
.filter((w) => w.walletType === 'institutional')
|
||||
.reduce((sum, w) => sum.plus(w.balance), new Decimal(0))
|
||||
.toNumber(),
|
||||
},
|
||||
];
|
||||
|
||||
// Get cross-border corridors
|
||||
const routes = await prisma.settlementRoute.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scb.id }, { destinationBankId: scb.id }],
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
const crossBorderCorridors = routes.map((route) => ({
|
||||
targetSCB: route.sourceBankId === scb.id ? route.destinationBankId : route.sourceBankId,
|
||||
settlementAsset: 'SSU', // Default, would check actual usage
|
||||
status: 'active' as const,
|
||||
}));
|
||||
|
||||
schemas.push({
|
||||
scbId: scb.id,
|
||||
scbName: scb.name,
|
||||
cbdcTypes,
|
||||
crossBorderCorridors,
|
||||
});
|
||||
}
|
||||
|
||||
return schemas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FX routing information
|
||||
*/
|
||||
async getFXRouting(): Promise<FXRouting> {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// Get all active routes
|
||||
const routes = await prisma.settlementRoute.findMany({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
// Get FX trades
|
||||
const fxTrades = await prisma.fxTrade.findMany({
|
||||
where: {
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
});
|
||||
|
||||
// Get SSU transactions
|
||||
const ssuTransactions = await prisma.ssuTransaction.findMany({
|
||||
where: {
|
||||
createdAt: { gte: oneDayAgo },
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
|
||||
const ssuVolume = ssuTransactions.reduce(
|
||||
(sum, t) => sum.plus(t.amount),
|
||||
new Decimal(0)
|
||||
).toNumber();
|
||||
|
||||
// Build corridors
|
||||
const corridors = routes.map((route) => {
|
||||
const routeTrades = fxTrades.filter(
|
||||
(t) =>
|
||||
(t.sovereignBankId === route.sourceBankId ||
|
||||
t.sovereignBankId === route.destinationBankId) &&
|
||||
t.baseCurrency === route.currencyCode
|
||||
);
|
||||
|
||||
const volume24h = routeTrades
|
||||
.filter((t) => t.status === 'executed')
|
||||
.reduce((sum, t) => sum.plus(t.quantity), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
return {
|
||||
sourceSCB: route.sourceBankId,
|
||||
destinationSCB: route.destinationBankId,
|
||||
preferredSettlementAsset: 'SSU', // Default
|
||||
volume24h,
|
||||
spreads: {
|
||||
min: 0.0001,
|
||||
max: 0.01,
|
||||
},
|
||||
fees: {
|
||||
min: 0.001,
|
||||
max: 0.05,
|
||||
},
|
||||
circuitBreakers: {
|
||||
enabled: true,
|
||||
volatilityThreshold: 0.05, // 5%
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
corridors,
|
||||
ssuUsage: {
|
||||
totalVolume: ssuVolume,
|
||||
activeCorridors: new Set(routes.map((r) => `${r.sourceBankId}-${r.destinationBankId}`))
|
||||
.size,
|
||||
},
|
||||
gruBridgeUsage: {
|
||||
totalVolume: 0, // Would calculate from GRU transactions
|
||||
activeCorridors: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcFxService = new CBDCFXService();
|
||||
|
||||
176
src/core/admin/dbis-admin/dashboards/gas-qps.service.ts
Normal file
176
src/core/admin/dbis-admin/dashboards/gas-qps.service.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
// DBIS Admin Console - GAS & QPS Control Service
|
||||
// GAS atomic settlement control, QPS legacy rails management
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface GASMetrics {
|
||||
atomicSettlementSuccessRate: number;
|
||||
averageLatency: number;
|
||||
perAssetBreakdown: {
|
||||
currency: { successRate: number; volume: number };
|
||||
cbdc: { successRate: number; volume: number };
|
||||
commodity: { successRate: number; volume: number };
|
||||
security: { successRate: number; volume: number };
|
||||
};
|
||||
assetLevelLimits: Array<{
|
||||
assetClass: string;
|
||||
maxNotionalPerSCB: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface QPSMetrics {
|
||||
legacyRails: Array<{
|
||||
railType: string; // SWIFT, ISO20022, RTGS
|
||||
enabled: boolean;
|
||||
volume24h: number;
|
||||
errorRate: number;
|
||||
}>;
|
||||
mappingProfiles: Array<{
|
||||
profileId: string;
|
||||
profileName: string;
|
||||
acceptedMessages: string[];
|
||||
validationLevel: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GASQPSDashboard {
|
||||
gas: GASMetrics;
|
||||
qps: QPSMetrics;
|
||||
}
|
||||
|
||||
export class GASQPSService {
|
||||
/**
|
||||
* Get GAS & QPS dashboard
|
||||
*/
|
||||
async getGASQPSDashboard(): Promise<GASQPSDashboard> {
|
||||
const [gas, qps] = await Promise.all([this.getGASMetrics(), this.getQPSMetrics()]);
|
||||
|
||||
return { gas, qps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GAS metrics
|
||||
*/
|
||||
async getGASMetrics(): Promise<GASMetrics> {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// Get all atomic settlements in last 24 hours
|
||||
const settlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
});
|
||||
|
||||
const total = settlements.length;
|
||||
const successful = settlements.filter((s) => s.status === 'settled').length;
|
||||
const successRate = total > 0 ? successful / total : 1.0;
|
||||
|
||||
// Calculate average latency
|
||||
const settledSettlements = settlements.filter((s) => s.status === 'settled' && s.settlementTime);
|
||||
const avgLatency =
|
||||
settledSettlements.length > 0
|
||||
? settledSettlements.reduce((sum, s) => sum + (s.settlementTime || 0), 0) /
|
||||
settledSettlements.length
|
||||
: 0;
|
||||
|
||||
// Per asset breakdown
|
||||
const perAssetBreakdown = {
|
||||
currency: { successRate: 0, volume: 0 },
|
||||
cbdc: { successRate: 0, volume: 0 },
|
||||
commodity: { successRate: 0, volume: 0 },
|
||||
security: { successRate: 0, volume: 0 },
|
||||
};
|
||||
|
||||
['currency', 'cbdc', 'commodity', 'security'].forEach((assetType) => {
|
||||
const assetSettlements = settlements.filter((s) => s.assetType === assetType);
|
||||
const assetSuccessful = assetSettlements.filter((s) => s.status === 'settled').length;
|
||||
const assetVolume = assetSettlements
|
||||
.filter((s) => s.status === 'settled')
|
||||
.reduce((sum, s) => sum.plus(s.amount), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
perAssetBreakdown[assetType as keyof typeof perAssetBreakdown] = {
|
||||
successRate: assetSettlements.length > 0 ? assetSuccessful / assetSettlements.length : 0,
|
||||
volume: assetVolume,
|
||||
};
|
||||
});
|
||||
|
||||
// Asset level limits (placeholder - would come from configuration)
|
||||
const assetLevelLimits = [
|
||||
{ assetClass: 'currency', maxNotionalPerSCB: 1000000000 },
|
||||
{ assetClass: 'cbdc', maxNotionalPerSCB: 500000000 },
|
||||
{ assetClass: 'commodity', maxNotionalPerSCB: 200000000 },
|
||||
{ assetClass: 'security', maxNotionalPerSCB: 1000000000 },
|
||||
];
|
||||
|
||||
return {
|
||||
atomicSettlementSuccessRate: successRate,
|
||||
averageLatency: avgLatency,
|
||||
perAssetBreakdown,
|
||||
assetLevelLimits,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get QPS metrics
|
||||
*/
|
||||
async getQPSMetrics(): Promise<QPSMetrics> {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// Get ISO 20022 messages (QPS)
|
||||
const isoMessages = await prisma.isoMessage.findMany({
|
||||
where: {
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
});
|
||||
|
||||
// Group by message type
|
||||
const messageTypes = new Map<string, number>();
|
||||
isoMessages.forEach((msg) => {
|
||||
const count = messageTypes.get(msg.messageType) || 0;
|
||||
messageTypes.set(msg.messageType, count + 1);
|
||||
});
|
||||
|
||||
// Legacy rails (placeholder - would need actual QPS integration)
|
||||
const legacyRails = [
|
||||
{
|
||||
railType: 'SWIFT',
|
||||
enabled: true,
|
||||
volume24h: isoMessages.filter((m) => m.messageType.includes('SWIFT')).length,
|
||||
errorRate: 0.01,
|
||||
},
|
||||
{
|
||||
railType: 'ISO20022',
|
||||
enabled: true,
|
||||
volume24h: isoMessages.length,
|
||||
errorRate: 0.005,
|
||||
},
|
||||
{
|
||||
railType: 'RTGS',
|
||||
enabled: true,
|
||||
volume24h: 0,
|
||||
errorRate: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// Mapping profiles (placeholder)
|
||||
const mappingProfiles = [
|
||||
{
|
||||
profileId: 'default',
|
||||
profileName: 'Default ISO 20022 Profile',
|
||||
acceptedMessages: Array.from(messageTypes.keys()),
|
||||
validationLevel: 'standard',
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
legacyRails,
|
||||
mappingProfiles,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const gasQpsService = new GASQPSService();
|
||||
|
||||
356
src/core/admin/dbis-admin/dashboards/global-overview.service.ts
Normal file
356
src/core/admin/dbis-admin/dashboards/global-overview.service.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
// DBIS Admin Console - Global Overview Dashboard Service
|
||||
// Network health, settlement throughput, GRU & liquidity, risk flags, SCB status
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { dashboardService } from '@/core/compliance/regtech/dashboard.service';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface NetworkHealthStatus {
|
||||
subsystem: string;
|
||||
status: 'healthy' | 'degraded' | 'down';
|
||||
lastHeartbeat?: Date;
|
||||
latency?: number;
|
||||
errorRate?: number;
|
||||
}
|
||||
|
||||
export interface SettlementThroughput {
|
||||
txPerSecond: number;
|
||||
dailyVolume: number;
|
||||
byAssetType: {
|
||||
fiat: number;
|
||||
cbdc: number;
|
||||
gru: number;
|
||||
ssu: number;
|
||||
commodities: number;
|
||||
};
|
||||
heatmap: Array<{
|
||||
sourceSCB: string;
|
||||
destinationSCB: string;
|
||||
volume: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GRULiquidityMetrics {
|
||||
currentPrice: number;
|
||||
volatility: number;
|
||||
inCirculation: {
|
||||
m00: number;
|
||||
m0: number;
|
||||
m1: number;
|
||||
sr1: number;
|
||||
sr2: number;
|
||||
sr3: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RiskFlags {
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
alerts: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SCBStatus {
|
||||
scbId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bic?: string;
|
||||
status: string;
|
||||
connectivity: 'connected' | 'degraded' | 'disconnected';
|
||||
latency?: number;
|
||||
errorRate?: number;
|
||||
openIncidents: number;
|
||||
}
|
||||
|
||||
export interface GlobalOverviewDashboard {
|
||||
networkHealth: NetworkHealthStatus[];
|
||||
settlementThroughput: SettlementThroughput;
|
||||
gruLiquidity: GRULiquidityMetrics;
|
||||
riskFlags: RiskFlags;
|
||||
scbStatus: SCBStatus[];
|
||||
}
|
||||
|
||||
export class GlobalOverviewService {
|
||||
/**
|
||||
* Get global overview dashboard
|
||||
*/
|
||||
async getGlobalOverview(): Promise<GlobalOverviewDashboard> {
|
||||
const [networkHealth, settlementThroughput, gruLiquidity, riskFlags, scbStatus] =
|
||||
await Promise.all([
|
||||
this.getNetworkHealth(),
|
||||
this.getSettlementThroughput(),
|
||||
this.getGRULiquidity(),
|
||||
this.getRiskFlags(),
|
||||
this.getSCBStatus(),
|
||||
]);
|
||||
|
||||
return {
|
||||
networkHealth,
|
||||
settlementThroughput,
|
||||
gruLiquidity,
|
||||
riskFlags,
|
||||
scbStatus,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network health status for all subsystems
|
||||
*/
|
||||
async getNetworkHealth(): Promise<NetworkHealthStatus[]> {
|
||||
const subsystems: NetworkHealthStatus[] = [];
|
||||
|
||||
// GAS (Global Atomic Settlement)
|
||||
try {
|
||||
const gasSettlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 5 * 60 * 1000), // Last 5 minutes
|
||||
},
|
||||
},
|
||||
take: 100,
|
||||
});
|
||||
|
||||
const successRate =
|
||||
gasSettlements.length > 0
|
||||
? gasSettlements.filter((s) => s.status === 'settled').length /
|
||||
gasSettlements.length
|
||||
: 1.0;
|
||||
|
||||
subsystems.push({
|
||||
subsystem: 'GAS',
|
||||
status: successRate > 0.95 ? 'healthy' : successRate > 0.8 ? 'degraded' : 'down',
|
||||
errorRate: 1 - successRate,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting GAS health', { error });
|
||||
subsystems.push({ subsystem: 'GAS', status: 'down' });
|
||||
}
|
||||
|
||||
// QPS (Quantum Payment System) - placeholder
|
||||
subsystems.push({
|
||||
subsystem: 'QPS',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
// Ω-Layer (Omega Layer) - placeholder
|
||||
subsystems.push({
|
||||
subsystem: 'Ω-Layer',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
// GPN (Global Payment Network) - placeholder
|
||||
subsystems.push({
|
||||
subsystem: 'GPN',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
// GRU Engine - placeholder
|
||||
subsystems.push({
|
||||
subsystem: 'GRU Engine',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
// Metaverse MEN - placeholder
|
||||
subsystems.push({
|
||||
subsystem: 'Metaverse MEN',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
// 6G Edge Grid - placeholder
|
||||
subsystems.push({
|
||||
subsystem: '6G Edge Grid',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
return subsystems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settlement throughput metrics
|
||||
*/
|
||||
async getSettlementThroughput(): Promise<SettlementThroughput> {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
|
||||
|
||||
// Get all settlements in last 24 hours
|
||||
const settlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: oneDayAgo,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get settlements in last minute for tx/sec
|
||||
const recentSettlements = settlements.filter(
|
||||
(s) => s.createdAt >= oneMinuteAgo
|
||||
);
|
||||
|
||||
const txPerSecond = recentSettlements.length / 60;
|
||||
|
||||
// Calculate daily volume
|
||||
const dailyVolume = settlements
|
||||
.filter((s) => s.status === 'settled')
|
||||
.reduce((sum, s) => sum.plus(s.amount), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
// Group by asset type
|
||||
const byAssetType = {
|
||||
fiat: 0,
|
||||
cbdc: 0,
|
||||
gru: 0,
|
||||
ssu: 0,
|
||||
commodities: 0,
|
||||
};
|
||||
|
||||
settlements.forEach((s) => {
|
||||
if (s.assetType === 'currency') byAssetType.fiat += parseFloat(s.amount.toString());
|
||||
else if (s.assetType === 'cbdc') byAssetType.cbdc += parseFloat(s.amount.toString());
|
||||
else if (s.assetType === 'commodity') byAssetType.commodities += parseFloat(s.amount.toString());
|
||||
// GRU and SSU would need additional queries
|
||||
});
|
||||
|
||||
// Heatmap: top corridors by volume
|
||||
const corridorMap = new Map<string, number>();
|
||||
settlements.forEach((s) => {
|
||||
if (s.status === 'settled') {
|
||||
const key = `${s.sourceBankId}-${s.destinationBankId}`;
|
||||
const current = corridorMap.get(key) || 0;
|
||||
corridorMap.set(key, current + parseFloat(s.amount.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
const heatmap = Array.from(corridorMap.entries())
|
||||
.map(([key, volume]) => {
|
||||
const [source, dest] = key.split('-');
|
||||
return { sourceSCB: source, destinationSCB: dest, volume };
|
||||
})
|
||||
.sort((a, b) => b.volume - a.volume)
|
||||
.slice(0, 20); // Top 20
|
||||
|
||||
return {
|
||||
txPerSecond,
|
||||
dailyVolume,
|
||||
byAssetType,
|
||||
heatmap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GRU & liquidity metrics
|
||||
*/
|
||||
async getGRULiquidity(): Promise<GRULiquidityMetrics> {
|
||||
// Get GRU units
|
||||
const gruUnits = await prisma.gruUnit.findMany({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
// Calculate in circulation by class
|
||||
const inCirculation = {
|
||||
m00: 0,
|
||||
m0: 0,
|
||||
m1: 0,
|
||||
sr1: 0,
|
||||
sr2: 0,
|
||||
sr3: 0,
|
||||
};
|
||||
|
||||
// Get GRU indexes for price
|
||||
const indexes = await prisma.gruIndex.findMany({
|
||||
where: { status: 'active' },
|
||||
include: { priceHistory: { orderBy: { timestamp: 'desc' }, take: 2 } },
|
||||
});
|
||||
|
||||
let currentPrice = 1.0; // Default
|
||||
let volatility = 0.0;
|
||||
|
||||
if (indexes.length > 0 && indexes[0].priceHistory.length >= 2) {
|
||||
const [latest, previous] = indexes[0].priceHistory;
|
||||
currentPrice = parseFloat(latest.price.toString());
|
||||
const prevPrice = parseFloat(previous.price.toString());
|
||||
volatility = Math.abs((currentPrice - prevPrice) / prevPrice);
|
||||
}
|
||||
|
||||
return {
|
||||
currentPrice,
|
||||
volatility,
|
||||
inCirculation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get risk flags and alerts
|
||||
*/
|
||||
async getRiskFlags(): Promise<RiskFlags> {
|
||||
const dashboard = await dashboardService.getIncidentAlertsDashboard();
|
||||
|
||||
const alerts = dashboard.incidentAlerts || [];
|
||||
const high = alerts.filter((a) => a.severity === 'critical' || a.severity === 'high').length;
|
||||
const medium = alerts.filter((a) => a.severity === 'medium').length;
|
||||
const low = alerts.filter((a) => a.severity === 'low').length;
|
||||
|
||||
return {
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
alerts: alerts.slice(0, 10), // Top 10
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SCB status table
|
||||
*/
|
||||
async getSCBStatus(): Promise<SCBStatus[]> {
|
||||
const scbs = await prisma.sovereignBank.findMany({
|
||||
where: { status: { in: ['active', 'suspended'] } },
|
||||
});
|
||||
|
||||
const scbStatus: SCBStatus[] = [];
|
||||
|
||||
for (const scb of scbs) {
|
||||
// Get recent settlements to determine connectivity
|
||||
const recentSettlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scb.id }, { destinationBankId: scb.id }],
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 5 * 60 * 1000), // Last 5 minutes
|
||||
},
|
||||
},
|
||||
take: 10,
|
||||
});
|
||||
|
||||
const connectivity =
|
||||
recentSettlements.length > 0 ? 'connected' : 'degraded';
|
||||
|
||||
// Get open incidents (SRI enforcements)
|
||||
const openIncidents = await prisma.sRIEnforcement.count({
|
||||
where: {
|
||||
sovereignBankId: scb.id,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
scbStatus.push({
|
||||
scbId: scb.id,
|
||||
name: scb.name,
|
||||
country: scb.sovereignCode,
|
||||
bic: scb.bic || undefined,
|
||||
status: scb.status,
|
||||
connectivity,
|
||||
openIncidents,
|
||||
});
|
||||
}
|
||||
|
||||
return scbStatus;
|
||||
}
|
||||
}
|
||||
|
||||
export const globalOverviewService = new GlobalOverviewService();
|
||||
|
||||
224
src/core/admin/dbis-admin/dashboards/gru-command.service.ts
Normal file
224
src/core/admin/dbis-admin/dashboards/gru-command.service.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
// DBIS Admin Console - GRU Command Center Service
|
||||
// GRU Monetary, Indexes, Bonds, Supranational Pools
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface GRUMonetaryMetrics {
|
||||
supply: {
|
||||
m00: number;
|
||||
m0: number;
|
||||
m1: number;
|
||||
sr1: number;
|
||||
sr2: number;
|
||||
sr3: number;
|
||||
};
|
||||
locked: {
|
||||
m00: boolean;
|
||||
m0: boolean;
|
||||
m1: boolean;
|
||||
sr1: boolean;
|
||||
sr2: boolean;
|
||||
sr3: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GRUIndexInfo {
|
||||
indexId: string;
|
||||
indexName: string;
|
||||
indexCode: string; // LiXAU, LiPMG, LiBMG1-3
|
||||
currentPrice: number;
|
||||
components: Array<{
|
||||
asset: string;
|
||||
weight: number;
|
||||
}>;
|
||||
priceHistory: Array<{
|
||||
timestamp: Date;
|
||||
price: number;
|
||||
}>;
|
||||
circuitBreakers: {
|
||||
maxIntradayMove: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GRUBondInfo {
|
||||
bondId: string;
|
||||
bondName: string;
|
||||
bondCode: string; // Li99PpOsB10, Li99PpAvB10
|
||||
outstanding: number;
|
||||
buyers: number;
|
||||
yield: number;
|
||||
issuanceWindow: 'open' | 'closed';
|
||||
primaryMarketParams: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GRUSupranationalPool {
|
||||
poolId: string;
|
||||
poolName: string;
|
||||
totalReserves: number;
|
||||
allocations: Array<{
|
||||
reserveClass: string;
|
||||
amount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GRUCommandDashboard {
|
||||
monetary: GRUMonetaryMetrics;
|
||||
indexes: GRUIndexInfo[];
|
||||
bonds: GRUBondInfo[];
|
||||
supranationalPools: GRUSupranationalPool[];
|
||||
}
|
||||
|
||||
export class GRUCommandService {
|
||||
/**
|
||||
* Get GRU command center dashboard
|
||||
*/
|
||||
async getGRUCommandDashboard(): Promise<GRUCommandDashboard> {
|
||||
const [monetary, indexes, bonds, supranationalPools] = await Promise.all([
|
||||
this.getGRUMonetary(),
|
||||
this.getGRUIndexes(),
|
||||
this.getGRUBonds(),
|
||||
this.getGRUSupranationalPools(),
|
||||
]);
|
||||
|
||||
return {
|
||||
monetary,
|
||||
indexes,
|
||||
bonds,
|
||||
supranationalPools,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GRU monetary metrics
|
||||
*/
|
||||
async getGRUMonetary(): Promise<GRUMonetaryMetrics> {
|
||||
// Get all GRU units
|
||||
const gruUnits = await prisma.gruUnit.findMany({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
// Calculate supply by class (placeholder - would need proper aggregation)
|
||||
const supply = {
|
||||
m00: 0,
|
||||
m0: 0,
|
||||
m1: 0,
|
||||
sr1: 0,
|
||||
sr2: 0,
|
||||
sr3: 0,
|
||||
};
|
||||
|
||||
// Check locked status (placeholder)
|
||||
const locked = {
|
||||
m00: false,
|
||||
m0: false,
|
||||
m1: false,
|
||||
sr1: false,
|
||||
sr2: false,
|
||||
sr3: false,
|
||||
};
|
||||
|
||||
return { supply, locked };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GRU indexes
|
||||
*/
|
||||
async getGRUIndexes(): Promise<GRUIndexInfo[]> {
|
||||
const indexes = await prisma.gruIndex.findMany({
|
||||
where: { status: 'active' },
|
||||
include: {
|
||||
priceHistory: {
|
||||
orderBy: { timestamp: 'desc' },
|
||||
take: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return indexes.map((index) => ({
|
||||
indexId: index.indexId,
|
||||
indexName: index.indexName,
|
||||
indexCode: index.indexCode,
|
||||
currentPrice: index.priceHistory.length > 0
|
||||
? parseFloat(index.priceHistory[0].price.toString())
|
||||
: 0,
|
||||
components: (index.components as Array<{ asset: string; weight: number }>) || [],
|
||||
priceHistory: index.priceHistory.map((ph) => ({
|
||||
timestamp: ph.timestamp,
|
||||
price: parseFloat(ph.price.toString()),
|
||||
})),
|
||||
circuitBreakers: {
|
||||
maxIntradayMove: parseFloat(index.circuitBreakerThreshold?.toString() || '0.1'),
|
||||
enabled: index.circuitBreakerEnabled || false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GRU bonds
|
||||
*/
|
||||
async getGRUBonds(): Promise<GRUBondInfo[]> {
|
||||
const bonds = await prisma.gruBond.findMany({
|
||||
where: { status: 'active' },
|
||||
include: {
|
||||
coupons: true,
|
||||
pricing: {
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return bonds.map((bond) => {
|
||||
const latestPricing = bond.pricing[0];
|
||||
const yieldValue = latestPricing
|
||||
? parseFloat(latestPricing.yield.toString())
|
||||
: 0;
|
||||
|
||||
return {
|
||||
bondId: bond.bondId,
|
||||
bondName: bond.bondName,
|
||||
bondCode: bond.bondCode,
|
||||
outstanding: parseFloat(bond.outstandingAmount.toString()),
|
||||
buyers: bond.numberOfHolders || 0,
|
||||
yield: yieldValue,
|
||||
issuanceWindow: bond.issuanceWindowOpen ? 'open' : 'closed',
|
||||
primaryMarketParams: {
|
||||
minPurchase: parseFloat(bond.minPurchaseAmount?.toString() || '0'),
|
||||
maxPurchase: parseFloat(bond.maxPurchaseAmount?.toString() || '0'),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GRU supranational pools
|
||||
*/
|
||||
async getGRUSupranationalPools(): Promise<GRUSupranationalPool[]> {
|
||||
const pools = await prisma.gruReservePool.findMany({
|
||||
where: { status: 'active' },
|
||||
include: {
|
||||
allocations: {
|
||||
include: {
|
||||
reserveClass: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return pools.map((pool) => ({
|
||||
poolId: pool.poolId,
|
||||
poolName: pool.poolName,
|
||||
totalReserves: parseFloat(pool.totalReserves.toString()),
|
||||
allocations: pool.allocations.map((alloc) => ({
|
||||
reserveClass: alloc.reserveClass.className,
|
||||
amount: parseFloat(alloc.amount.toString()),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const gruCommandService = new GRUCommandService();
|
||||
|
||||
119
src/core/admin/dbis-admin/dashboards/metaverse-edge.service.ts
Normal file
119
src/core/admin/dbis-admin/dashboards/metaverse-edge.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// DBIS Admin Console - Metaverse & Edge Service
|
||||
// Metaverse Nodes (MEN), 6G Edge GPU Grid
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
export interface MetaverseNode {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
location: string;
|
||||
status: 'active' | 'degraded' | 'down';
|
||||
activeVolumes: number;
|
||||
onRamps: Array<{
|
||||
scbId: string;
|
||||
enabled: boolean;
|
||||
volume24h: number;
|
||||
}>;
|
||||
kycEnforcementLevel: 'low' | 'medium' | 'high';
|
||||
limits: {
|
||||
dailyLimit: number;
|
||||
perTransactionLimit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EdgeNode {
|
||||
regionId: string;
|
||||
regionName: string;
|
||||
nodeCount: number;
|
||||
occupancy: number; // 0-100
|
||||
latency: number;
|
||||
priority: 'settlement' | 'rendering' | 'balanced';
|
||||
status: 'active' | 'quarantined' | 'draining';
|
||||
}
|
||||
|
||||
export interface MetaverseEdgeDashboard {
|
||||
metaverseNodes: MetaverseNode[];
|
||||
edgeNodes: EdgeNode[];
|
||||
}
|
||||
|
||||
export class MetaverseEdgeService {
|
||||
/**
|
||||
* Get Metaverse & Edge dashboard
|
||||
*/
|
||||
async getMetaverseEdgeDashboard(): Promise<MetaverseEdgeDashboard> {
|
||||
const [metaverseNodes, edgeNodes] = await Promise.all([
|
||||
this.getMetaverseNodes(),
|
||||
this.getEdgeNodes(),
|
||||
]);
|
||||
|
||||
return { metaverseNodes, edgeNodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Metaverse nodes
|
||||
*/
|
||||
async getMetaverseNodes(): Promise<MetaverseNode[]> {
|
||||
// Placeholder - would integrate with actual metaverse system
|
||||
const nodes: MetaverseNode[] = [
|
||||
{
|
||||
nodeId: 'metaverse-dubai',
|
||||
nodeName: 'MetaverseDubai',
|
||||
location: 'Dubai',
|
||||
status: 'active',
|
||||
activeVolumes: 0,
|
||||
onRamps: [],
|
||||
kycEnforcementLevel: 'medium',
|
||||
limits: {
|
||||
dailyLimit: 1000000,
|
||||
perTransactionLimit: 10000,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Get SCBs to populate on-ramps
|
||||
const scbs = await prisma.sovereignBank.findMany({
|
||||
where: { status: 'active' },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
nodes.forEach((node) => {
|
||||
node.onRamps = scbs.map((scb) => ({
|
||||
scbId: scb.id,
|
||||
enabled: true,
|
||||
volume24h: 0, // Would calculate from actual transactions
|
||||
}));
|
||||
});
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 6G Edge GPU Grid nodes
|
||||
*/
|
||||
async getEdgeNodes(): Promise<EdgeNode[]> {
|
||||
// Placeholder - would integrate with actual edge compute system
|
||||
// 325 region nodes as mentioned in spec
|
||||
const regions = [
|
||||
{ id: 'region-001', name: 'North America East', nodeCount: 25 },
|
||||
{ id: 'region-002', name: 'North America West', nodeCount: 20 },
|
||||
{ id: 'region-003', name: 'Europe Central', nodeCount: 30 },
|
||||
{ id: 'region-004', name: 'Asia Pacific', nodeCount: 40 },
|
||||
{ id: 'region-005', name: 'Middle East', nodeCount: 15 },
|
||||
// ... would have 325 total
|
||||
];
|
||||
|
||||
return regions.map((region) => ({
|
||||
regionId: region.id,
|
||||
regionName: region.name,
|
||||
nodeCount: region.nodeCount,
|
||||
occupancy: Math.random() * 100, // Placeholder
|
||||
latency: Math.random() * 100, // Placeholder
|
||||
priority: 'balanced' as const,
|
||||
status: 'active' as const,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const metaverseEdgeService = new MetaverseEdgeService();
|
||||
|
||||
218
src/core/admin/dbis-admin/dashboards/participants.service.ts
Normal file
218
src/core/admin/dbis-admin/dashboards/participants.service.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
// DBIS Admin Console - Participants & Jurisdictions Service
|
||||
// SCB directory, jurisdiction settings, corridor management
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface ParticipantInfo {
|
||||
scbId: string;
|
||||
name: string;
|
||||
country: string;
|
||||
bic?: string;
|
||||
lei?: string;
|
||||
status: string;
|
||||
connectivity: 'connected' | 'degraded' | 'disconnected';
|
||||
lastHeartbeat?: Date;
|
||||
latency?: number;
|
||||
errorRate?: number;
|
||||
}
|
||||
|
||||
export interface JurisdictionSettings {
|
||||
scbId: string;
|
||||
allowedAssetClasses: string[];
|
||||
corridorRules: Array<{
|
||||
targetSCB: string;
|
||||
caps: number;
|
||||
allowedSettlementAssets: string[];
|
||||
}>;
|
||||
regulatoryProfiles: {
|
||||
amlStrictness: 'low' | 'medium' | 'high';
|
||||
sanctionsLists: string[];
|
||||
reportingFrequency: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CorridorInfo {
|
||||
routeId: string;
|
||||
sourceSCB: string;
|
||||
destinationSCB: string;
|
||||
currencyCode: string;
|
||||
status: string;
|
||||
volume24h: number;
|
||||
latency: number;
|
||||
errorRate: number;
|
||||
}
|
||||
|
||||
export class ParticipantsService {
|
||||
/**
|
||||
* Get participant directory
|
||||
*/
|
||||
async getParticipantDirectory(): Promise<ParticipantInfo[]> {
|
||||
const scbs = await prisma.sovereignBank.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
|
||||
const participants: ParticipantInfo[] = [];
|
||||
|
||||
for (const scb of scbs) {
|
||||
// Get recent activity to determine connectivity
|
||||
const recentSettlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scb.id }, { destinationBankId: scb.id }],
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 5 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
take: 10,
|
||||
});
|
||||
|
||||
const connectivity =
|
||||
recentSettlements.length > 0 ? 'connected' : 'degraded';
|
||||
|
||||
participants.push({
|
||||
scbId: scb.id,
|
||||
name: scb.name,
|
||||
country: scb.sovereignCode,
|
||||
bic: scb.bic || undefined,
|
||||
lei: scb.lei || undefined,
|
||||
status: scb.status,
|
||||
connectivity,
|
||||
lastHeartbeat: scb.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return participants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get participant details
|
||||
*/
|
||||
async getParticipantDetails(scbId: string): Promise<ParticipantInfo | null> {
|
||||
const scb = await prisma.sovereignBank.findUnique({
|
||||
where: { id: scbId },
|
||||
});
|
||||
|
||||
if (!scb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recentSettlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scb.id }, { destinationBankId: scb.id }],
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 5 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
take: 100,
|
||||
});
|
||||
|
||||
const successCount = recentSettlements.filter((s) => s.status === 'settled').length;
|
||||
const errorRate = recentSettlements.length > 0 ? 1 - successCount / recentSettlements.length : 0;
|
||||
|
||||
return {
|
||||
scbId: scb.id,
|
||||
name: scb.name,
|
||||
country: scb.sovereignCode,
|
||||
bic: scb.bic || undefined,
|
||||
lei: scb.lei || undefined,
|
||||
status: scb.status,
|
||||
connectivity: recentSettlements.length > 0 ? 'connected' : 'degraded',
|
||||
lastHeartbeat: scb.updatedAt,
|
||||
errorRate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jurisdiction settings for SCB
|
||||
*/
|
||||
async getJurisdictionSettings(scbId: string): Promise<JurisdictionSettings | null> {
|
||||
const scb = await prisma.sovereignBank.findUnique({
|
||||
where: { id: scbId },
|
||||
});
|
||||
|
||||
if (!scb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get corridors for this SCB
|
||||
const routes = await prisma.settlementRoute.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scbId }, { destinationBankId: scbId }],
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
const corridorRules = routes.map((route) => ({
|
||||
targetSCB: route.sourceBankId === scbId ? route.destinationBankId : route.sourceBankId,
|
||||
caps: route.sireCost ? parseFloat(route.sireCost.toString()) : 0,
|
||||
allowedSettlementAssets: [route.currencyCode],
|
||||
}));
|
||||
|
||||
return {
|
||||
scbId,
|
||||
allowedAssetClasses: ['FIAT', 'CBDC', 'GRU', 'SSU', 'commodities'],
|
||||
corridorRules,
|
||||
regulatoryProfiles: {
|
||||
amlStrictness: 'medium',
|
||||
sanctionsLists: ['OFAC', 'EU', 'UN'],
|
||||
reportingFrequency: 'daily',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all corridors
|
||||
*/
|
||||
async getCorridors(): Promise<CorridorInfo[]> {
|
||||
const routes = await prisma.settlementRoute.findMany({
|
||||
where: { status: 'active' },
|
||||
include: {
|
||||
routingDecisions: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const corridors: CorridorInfo[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
// Get 24h volume
|
||||
const settlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
sourceBankId: route.sourceBankId,
|
||||
destinationBankId: route.destinationBankId,
|
||||
currencyCode: route.currencyCode,
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
});
|
||||
|
||||
const volume24h = settlements
|
||||
.filter((s) => s.status === 'settled')
|
||||
.reduce((sum, s) => sum.plus(s.amount), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
const successCount = settlements.filter((s) => s.status === 'settled').length;
|
||||
const errorRate = settlements.length > 0 ? 1 - successCount / settlements.length : 0;
|
||||
|
||||
corridors.push({
|
||||
routeId: route.routeId,
|
||||
sourceSCB: route.sourceBankId,
|
||||
destinationSCB: route.destinationBankId,
|
||||
currencyCode: route.currencyCode,
|
||||
status: route.status,
|
||||
volume24h,
|
||||
latency: route.estimatedLatency || 0,
|
||||
errorRate,
|
||||
});
|
||||
}
|
||||
|
||||
return corridors;
|
||||
}
|
||||
}
|
||||
|
||||
export const participantsService = new ParticipantsService();
|
||||
|
||||
146
src/core/admin/dbis-admin/dashboards/risk-compliance.service.ts
Normal file
146
src/core/admin/dbis-admin/dashboards/risk-compliance.service.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
// DBIS Admin Console - Risk & Compliance Service
|
||||
// SARE, ARI, Ω-Layer consistency
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { dashboardService } from '@/core/compliance/regtech/dashboard.service';
|
||||
|
||||
export interface SARERiskHeatmap {
|
||||
scbId: string;
|
||||
scbName: string;
|
||||
riskScore: number;
|
||||
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
||||
factors: Array<{
|
||||
factor: string;
|
||||
score: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ARIAlert {
|
||||
alertId: string;
|
||||
type: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
description: string;
|
||||
scbId?: string;
|
||||
timestamp: Date;
|
||||
status: 'new' | 'acknowledged' | 'under_investigation' | 'resolved';
|
||||
}
|
||||
|
||||
export interface OmegaLayerIncident {
|
||||
incidentId: string;
|
||||
type: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
affectedSCBs: string[];
|
||||
timestamp: Date;
|
||||
status: 'active' | 'resolved';
|
||||
}
|
||||
|
||||
export interface RiskComplianceDashboard {
|
||||
sareHeatmap: SARERiskHeatmap[];
|
||||
ariAlerts: ARIAlert[];
|
||||
omegaLayerIncidents: OmegaLayerIncident[];
|
||||
}
|
||||
|
||||
export class RiskComplianceService {
|
||||
/**
|
||||
* Get Risk & Compliance dashboard
|
||||
*/
|
||||
async getRiskComplianceDashboard(): Promise<RiskComplianceDashboard> {
|
||||
const [sareHeatmap, ariAlerts, omegaLayerIncidents] = await Promise.all([
|
||||
this.getSAREHeatmap(),
|
||||
this.getARIAlerts(),
|
||||
this.getOmegaLayerIncidents(),
|
||||
]);
|
||||
|
||||
return { sareHeatmap, ariAlerts, omegaLayerIncidents };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SARE sovereign risk heatmap
|
||||
*/
|
||||
async getSAREHeatmap(): Promise<SARERiskHeatmap[]> {
|
||||
const scbs = await prisma.sovereignBank.findMany({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
const heatmap: SARERiskHeatmap[] = [];
|
||||
|
||||
for (const scb of scbs) {
|
||||
// Get latest SRI
|
||||
const sri = await prisma.sovereignRiskIndex.findFirst({
|
||||
where: { sovereignBankId: scb.id },
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
});
|
||||
|
||||
const riskScore = sri ? parseFloat(sri.sriScore.toString()) : 0;
|
||||
let riskLevel: 'low' | 'medium' | 'high' | 'critical' = 'low';
|
||||
if (riskScore >= 60) riskLevel = 'critical';
|
||||
else if (riskScore >= 40) riskLevel = 'high';
|
||||
else if (riskScore >= 20) riskLevel = 'medium';
|
||||
|
||||
heatmap.push({
|
||||
scbId: scb.id,
|
||||
scbName: scb.name,
|
||||
riskScore,
|
||||
riskLevel,
|
||||
factors: [
|
||||
{ factor: 'liquidity', score: riskScore * 0.3 },
|
||||
{ factor: 'fx_stability', score: riskScore * 0.25 },
|
||||
{ factor: 'cbdc_health', score: riskScore * 0.25 },
|
||||
{ factor: 'commodity_exposure', score: riskScore * 0.2 },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return heatmap.sort((a, b) => b.riskScore - a.riskScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ARI regulatory alerts
|
||||
*/
|
||||
async getARIAlerts(): Promise<ARIAlert[]> {
|
||||
// Get incident alerts from dashboard service
|
||||
const dashboard = await dashboardService.getIncidentAlertsDashboard();
|
||||
const alerts = dashboard.incidentAlerts || [];
|
||||
|
||||
return alerts.map((alert, index) => ({
|
||||
alertId: `ari-${index}`,
|
||||
type: alert.type,
|
||||
severity: alert.severity as 'low' | 'medium' | 'high' | 'critical',
|
||||
description: alert.description,
|
||||
timestamp: alert.timestamp,
|
||||
status: 'new' as const,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Ω-Layer consistency incidents
|
||||
*/
|
||||
async getOmegaLayerIncidents(): Promise<OmegaLayerIncident[]> {
|
||||
// Placeholder - would integrate with actual Ω-Layer monitoring
|
||||
// Check for settlement inconsistencies
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const failedSettlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
status: 'failed',
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
take: 10,
|
||||
});
|
||||
|
||||
return failedSettlements.map((settlement) => ({
|
||||
incidentId: settlement.settlementId,
|
||||
type: 'settlement_failure',
|
||||
severity: 'high',
|
||||
description: `Settlement ${settlement.settlementId} failed`,
|
||||
affectedSCBs: [settlement.sourceBankId, settlement.destinationBankId],
|
||||
timestamp: settlement.createdAt,
|
||||
status: 'active' as const,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const riskComplianceService = new RiskComplianceService();
|
||||
|
||||
340
src/core/admin/dbis-admin/dbis-admin.routes.ts
Normal file
340
src/core/admin/dbis-admin/dbis-admin.routes.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
// DBIS Admin Console API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { dbisAdminService } from './dbis-admin.service';
|
||||
import { requireAdminPermission } from '@/integration/api-gateway/middleware/admin-permission.middleware';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Global Overview Dashboard
|
||||
router.get(
|
||||
'/dashboard/overview',
|
||||
requireAdminPermission(AdminPermission.VIEW_GLOBAL_OVERVIEW),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const overview = await dbisAdminService.globalOverview.getGlobalOverview();
|
||||
res.json(overview);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Participants & Jurisdictions
|
||||
router.get(
|
||||
'/participants',
|
||||
requireAdminPermission(AdminPermission.VIEW_PARTICIPANTS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const participants = await dbisAdminService.participants.getParticipantDirectory();
|
||||
res.json(participants);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/participants/:scbId',
|
||||
requireAdminPermission(AdminPermission.VIEW_PARTICIPANTS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const participant = await dbisAdminService.participants.getParticipantDetails(
|
||||
req.params.scbId
|
||||
);
|
||||
if (!participant) {
|
||||
return res.status(404).json({ error: 'Participant not found' });
|
||||
}
|
||||
res.json(participant);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/participants/:scbId/jurisdiction',
|
||||
requireAdminPermission(AdminPermission.SCB_JURISDICTION_SETTINGS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const settings = await dbisAdminService.participants.getJurisdictionSettings(
|
||||
req.params.scbId
|
||||
);
|
||||
if (!settings) {
|
||||
return res.status(404).json({ error: 'Jurisdiction settings not found' });
|
||||
}
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/corridors',
|
||||
requireAdminPermission(AdminPermission.VIEW_PARTICIPANTS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const corridors = await dbisAdminService.participants.getCorridors();
|
||||
res.json(corridors);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GRU Command Center
|
||||
router.get(
|
||||
'/gru/command',
|
||||
requireAdminPermission(AdminPermission.VIEW_GRU_COMMAND),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const dashboard = await dbisAdminService.gruCommand.getGRUCommandDashboard();
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/gru/issuance/proposal',
|
||||
requireAdminPermission(AdminPermission.GRU_ISSUANCE_PROPOSAL),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.gruControls.createIssuanceProposal(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/gru/lock',
|
||||
requireAdminPermission(AdminPermission.GRU_LOCK_UNLOCK),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.gruControls.lockUnlockGRUClass(employeeId, req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/gru/circuit-breakers',
|
||||
requireAdminPermission(AdminPermission.GRU_CIRCUIT_BREAKERS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.gruControls.setCircuitBreakers(employeeId, req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/gru/bonds/window',
|
||||
requireAdminPermission(AdminPermission.GRU_BOND_ISSUANCE_WINDOW),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.gruControls.manageBondIssuanceWindow(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/gru/bonds/buyback',
|
||||
requireAdminPermission(AdminPermission.GRU_BOND_BUYBACK),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const { bondId, amount } = req.body;
|
||||
const result = await dbisAdminService.gruControls.triggerEmergencyBuyback(
|
||||
employeeId,
|
||||
bondId,
|
||||
amount
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GAS & QPS
|
||||
router.get(
|
||||
'/gas-qps',
|
||||
requireAdminPermission(AdminPermission.VIEW_GAS_QPS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const dashboard = await dbisAdminService.gasQps.getGASQPSDashboard();
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// CBDC & FX
|
||||
router.get(
|
||||
'/cbdc-fx',
|
||||
requireAdminPermission(AdminPermission.VIEW_CBDC_FX),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const dashboard = await dbisAdminService.cbdcFx.getCBDCFXDashboard();
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Metaverse & Edge
|
||||
router.get(
|
||||
'/metaverse-edge',
|
||||
requireAdminPermission(AdminPermission.VIEW_METAVERSE_EDGE),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const dashboard = await dbisAdminService.metaverseEdge.getMetaverseEdgeDashboard();
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Risk & Compliance
|
||||
router.get(
|
||||
'/risk-compliance',
|
||||
requireAdminPermission(AdminPermission.VIEW_RISK_COMPLIANCE),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const dashboard = await dbisAdminService.riskCompliance.getRiskComplianceDashboard();
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Corridor Controls
|
||||
router.post(
|
||||
'/corridors/caps',
|
||||
requireAdminPermission(AdminPermission.CORRIDOR_ADJUST_CAPS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.corridorControls.adjustCorridorCaps(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/corridors/throttle',
|
||||
requireAdminPermission(AdminPermission.CORRIDOR_THROTTLE),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.corridorControls.throttleCorridor(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/corridors/enable-disable',
|
||||
requireAdminPermission(AdminPermission.CORRIDOR_ENABLE_DISABLE),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.corridorControls.enableDisableCorridor(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Network Controls
|
||||
router.post(
|
||||
'/network/quiesce',
|
||||
requireAdminPermission(AdminPermission.NETWORK_QUIESCE_SUBSYSTEM),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.networkControls.quiesceSubsystem(employeeId, req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/network/kill-switch',
|
||||
requireAdminPermission(AdminPermission.NETWORK_KILL_SWITCH),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.networkControls.activateKillSwitch(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/network/escalate',
|
||||
requireAdminPermission(AdminPermission.NETWORK_ESCALATE_INCIDENT),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const employeeId = req.headers['x-employee-id'] as string || req.sovereignBankId || '';
|
||||
const result = await dbisAdminService.networkControls.escalateIncident(
|
||||
employeeId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
60
src/core/admin/dbis-admin/dbis-admin.service.ts
Normal file
60
src/core/admin/dbis-admin/dbis-admin.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// DBIS Admin Console - Main Service
|
||||
// Orchestrates all DBIS admin operations
|
||||
|
||||
import { globalOverviewService } from './dashboards/global-overview.service';
|
||||
import { participantsService } from './dashboards/participants.service';
|
||||
import { gruCommandService } from './dashboards/gru-command.service';
|
||||
import { gasQpsService } from './dashboards/gas-qps.service';
|
||||
import { cbdcFxService } from './dashboards/cbdc-fx.service';
|
||||
import { metaverseEdgeService } from './dashboards/metaverse-edge.service';
|
||||
import { riskComplianceService } from './dashboards/risk-compliance.service';
|
||||
import { gruControlsService } from './controls/gru-controls.service';
|
||||
import { corridorControlsService } from './controls/corridor-controls.service';
|
||||
import { networkControlsService } from './controls/network-controls.service';
|
||||
|
||||
export class DBISAdminService {
|
||||
// Dashboard services
|
||||
get globalOverview() {
|
||||
return globalOverviewService;
|
||||
}
|
||||
|
||||
get participants() {
|
||||
return participantsService;
|
||||
}
|
||||
|
||||
get gruCommand() {
|
||||
return gruCommandService;
|
||||
}
|
||||
|
||||
get gasQps() {
|
||||
return gasQpsService;
|
||||
}
|
||||
|
||||
get cbdcFx() {
|
||||
return cbdcFxService;
|
||||
}
|
||||
|
||||
get metaverseEdge() {
|
||||
return metaverseEdgeService;
|
||||
}
|
||||
|
||||
get riskCompliance() {
|
||||
return riskComplianceService;
|
||||
}
|
||||
|
||||
// Control services
|
||||
get gruControls() {
|
||||
return gruControlsService;
|
||||
}
|
||||
|
||||
get corridorControls() {
|
||||
return corridorControlsService;
|
||||
}
|
||||
|
||||
get networkControls() {
|
||||
return networkControlsService;
|
||||
}
|
||||
}
|
||||
|
||||
export const dbisAdminService = new DBISAdminService();
|
||||
|
||||
80
src/core/admin/scb-admin/controls/cbdc-controls.service.ts
Normal file
80
src/core/admin/scb-admin/controls/cbdc-controls.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// SCB Admin Console - CBDC Controls Service
|
||||
// CBDC parameters, GRU policies
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { adminAuditService } from '@/core/admin/shared/admin-audit.service';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
|
||||
export interface CBDCParameterUpdate {
|
||||
cbdcType: 'rCBDC' | 'wCBDC' | 'iCBDC';
|
||||
parameters: {
|
||||
rate?: number;
|
||||
feeSchedule?: Record<string, number>;
|
||||
velocityControls?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GRUPolicyUpdate {
|
||||
policy: 'enable_domestic_interbank' | 'enable_cross_border' | 'commercial_gru_allowed';
|
||||
enabled: boolean;
|
||||
partnerSCBs?: string[]; // For cross-border policy
|
||||
}
|
||||
|
||||
export class CBDCControlsService {
|
||||
/**
|
||||
* Update CBDC parameters
|
||||
*/
|
||||
async updateCBDCParameters(
|
||||
employeeId: string,
|
||||
scbId: string,
|
||||
update: CBDCParameterUpdate
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'update_cbdc_parameters',
|
||||
permission: AdminPermission.CBDC_UPDATE_PARAMETERS,
|
||||
resourceType: 'cbdc',
|
||||
resourceId: `${scbId}-${update.cbdcType}`,
|
||||
metadata: { scbId, ...update },
|
||||
});
|
||||
|
||||
// Would update CBDC configuration
|
||||
logger.info('CBDC parameters updated', {
|
||||
employeeId,
|
||||
scbId,
|
||||
update,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update GRU policy
|
||||
*/
|
||||
async updateGRUPolicy(
|
||||
employeeId: string,
|
||||
scbId: string,
|
||||
update: GRUPolicyUpdate
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'update_gru_policy',
|
||||
permission: AdminPermission.CBDC_UPDATE_PARAMETERS, // Using same permission
|
||||
resourceType: 'gru_policy',
|
||||
resourceId: scbId,
|
||||
metadata: { scbId, ...update },
|
||||
});
|
||||
|
||||
logger.info('GRU policy updated', {
|
||||
employeeId,
|
||||
scbId,
|
||||
update,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcControlsService = new CBDCControlsService();
|
||||
|
||||
112
src/core/admin/scb-admin/controls/fi-controls.service.ts
Normal file
112
src/core/admin/scb-admin/controls/fi-controls.service.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// SCB Admin Console - FI Controls Service
|
||||
// FI approval/suspension, API profiles, limits
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { adminAuditService } from '@/core/admin/shared/admin-audit.service';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
|
||||
export interface FIApprovalRequest {
|
||||
fiId: string;
|
||||
action: 'approve' | 'suspend';
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface FILimitsUpdate {
|
||||
fiId: string;
|
||||
limits: {
|
||||
byAssetType?: Record<string, number>;
|
||||
byCorridor?: Record<string, number>;
|
||||
dailyLimit?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface APIProfileAssignment {
|
||||
fiId: string;
|
||||
profileId: string;
|
||||
endpoints: string[];
|
||||
}
|
||||
|
||||
export class FIControlsService {
|
||||
/**
|
||||
* Approve or suspend FI
|
||||
*/
|
||||
async approveSuspendFI(
|
||||
employeeId: string,
|
||||
scbId: string,
|
||||
request: FIApprovalRequest
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: request.action === 'approve' ? 'approve_fi' : 'suspend_fi',
|
||||
permission: AdminPermission.FI_APPROVE_SUSPEND,
|
||||
resourceType: 'financial_institution',
|
||||
resourceId: request.fiId,
|
||||
metadata: { scbId, ...request },
|
||||
});
|
||||
|
||||
// Would update FI status
|
||||
logger.info('FI approval/suspension', {
|
||||
employeeId,
|
||||
scbId,
|
||||
request,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set FI limits
|
||||
*/
|
||||
async setFILimits(
|
||||
employeeId: string,
|
||||
scbId: string,
|
||||
update: FILimitsUpdate
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'set_fi_limits',
|
||||
permission: AdminPermission.FI_SET_LIMITS,
|
||||
resourceType: 'financial_institution',
|
||||
resourceId: update.fiId,
|
||||
metadata: { scbId, ...update },
|
||||
});
|
||||
|
||||
logger.info('FI limits updated', {
|
||||
employeeId,
|
||||
scbId,
|
||||
update,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign API profile to FI
|
||||
*/
|
||||
async assignAPIProfile(
|
||||
employeeId: string,
|
||||
scbId: string,
|
||||
assignment: APIProfileAssignment
|
||||
): Promise<{ success: boolean }> {
|
||||
await adminAuditService.logAction({
|
||||
employeeId,
|
||||
action: 'assign_api_profile',
|
||||
permission: AdminPermission.FI_API_PROFILES,
|
||||
resourceType: 'financial_institution',
|
||||
resourceId: assignment.fiId,
|
||||
metadata: { scbId, ...assignment },
|
||||
});
|
||||
|
||||
logger.info('API profile assigned', {
|
||||
employeeId,
|
||||
scbId,
|
||||
assignment,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const fiControlsService = new FIControlsService();
|
||||
|
||||
134
src/core/admin/scb-admin/dashboards/corridor-policy.service.ts
Normal file
134
src/core/admin/scb-admin/dashboards/corridor-policy.service.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
// SCB Admin Console - Corridor & FX Policy Service
|
||||
// Corridor management, FX policy, settlement asset preferences
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface CorridorPolicy {
|
||||
routeId: string;
|
||||
targetSCB: string;
|
||||
targetSCBName: string;
|
||||
status: 'active' | 'pending' | 'suspended';
|
||||
limits: {
|
||||
dailyCap: number;
|
||||
perTransactionLimit: number;
|
||||
};
|
||||
preferredSettlementAsset: string; // GRU, SSU, fiat
|
||||
metaverseEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface FXPolicy {
|
||||
corridors: Array<{
|
||||
corridorId: string;
|
||||
baseCurrency: string;
|
||||
quoteCurrency: string;
|
||||
spreads: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
fees: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CorridorPolicyDashboard {
|
||||
corridors: CorridorPolicy[];
|
||||
fxPolicy: FXPolicy;
|
||||
}
|
||||
|
||||
export class CorridorPolicyService {
|
||||
/**
|
||||
* Get corridor & FX policy dashboard
|
||||
*/
|
||||
async getCorridorPolicyDashboard(scbId: string): Promise<CorridorPolicyDashboard> {
|
||||
const [corridors, fxPolicy] = await Promise.all([
|
||||
this.getCorridors(scbId),
|
||||
this.getFXPolicy(scbId),
|
||||
]);
|
||||
|
||||
return { corridors, fxPolicy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get corridors for SCB
|
||||
*/
|
||||
async getCorridors(scbId: string): Promise<CorridorPolicy[]> {
|
||||
const routes = await prisma.settlementRoute.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scbId }, { destinationBankId: scbId }],
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
const corridors: CorridorPolicy[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
const targetSCBId = route.sourceBankId === scbId ? route.destinationBankId : route.sourceBankId;
|
||||
const targetSCB = await prisma.sovereignBank.findUnique({
|
||||
where: { id: targetSCBId },
|
||||
});
|
||||
|
||||
// Get 24h volume for limits
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const settlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
sourceBankId: route.sourceBankId,
|
||||
destinationBankId: route.destinationBankId,
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
});
|
||||
|
||||
const dailyVolume = settlements
|
||||
.filter((s) => s.status === 'settled')
|
||||
.reduce((sum, s) => sum.plus(s.amount), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
corridors.push({
|
||||
routeId: route.routeId,
|
||||
targetSCB: targetSCBId,
|
||||
targetSCBName: targetSCB?.name || 'Unknown',
|
||||
status: route.status === 'active' ? 'active' : 'suspended',
|
||||
limits: {
|
||||
dailyCap: dailyVolume * 1.5, // 150% of current volume
|
||||
perTransactionLimit: 10000000, // Placeholder
|
||||
},
|
||||
preferredSettlementAsset: 'SSU', // Default
|
||||
metaverseEnabled: false, // Placeholder
|
||||
});
|
||||
}
|
||||
|
||||
return corridors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FX policy
|
||||
*/
|
||||
async getFXPolicy(scbId: string): Promise<FXPolicy> {
|
||||
// Get FX pairs
|
||||
const fxPairs = await prisma.fxPair.findMany({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
const corridors = fxPairs.map((pair) => ({
|
||||
corridorId: pair.id,
|
||||
baseCurrency: pair.baseCurrency,
|
||||
quoteCurrency: pair.quoteCurrency,
|
||||
spreads: {
|
||||
min: 0.0001,
|
||||
max: 0.01,
|
||||
},
|
||||
fees: {
|
||||
min: 0.001,
|
||||
max: 0.05,
|
||||
},
|
||||
}));
|
||||
|
||||
return { corridors };
|
||||
}
|
||||
}
|
||||
|
||||
export const corridorPolicyService = new CorridorPolicyService();
|
||||
|
||||
93
src/core/admin/scb-admin/dashboards/fi-management.service.ts
Normal file
93
src/core/admin/scb-admin/dashboards/fi-management.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// SCB Admin Console - FI Management Service
|
||||
// FI directory, Nostro/Vostro accounts
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
export interface FIInfo {
|
||||
fiId: string;
|
||||
fiName: string;
|
||||
bic?: string;
|
||||
lei?: string;
|
||||
regulatoryTier: string;
|
||||
apiEnabled: boolean;
|
||||
status: 'active' | 'suspended' | 'pending';
|
||||
maxDailyLimits: {
|
||||
byAssetType: Record<string, number>;
|
||||
byCorridor: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NostroVostroAccount {
|
||||
accountId: string;
|
||||
accountType: 'nostro' | 'vostro';
|
||||
counterpartyFI: string;
|
||||
currencyCode: string;
|
||||
balance: number;
|
||||
limits: {
|
||||
dailyLimit: number;
|
||||
perTransactionLimit: number;
|
||||
};
|
||||
status: 'active' | 'frozen' | 'closed';
|
||||
}
|
||||
|
||||
export interface FIManagementDashboard {
|
||||
fiDirectory: FIInfo[];
|
||||
nostroVostroAccounts: NostroVostroAccount[];
|
||||
}
|
||||
|
||||
export class FIManagementService {
|
||||
/**
|
||||
* Get FI management dashboard
|
||||
*/
|
||||
async getFIManagementDashboard(scbId: string): Promise<FIManagementDashboard> {
|
||||
const [fiDirectory, nostroVostroAccounts] = await Promise.all([
|
||||
this.getFIDirectory(scbId),
|
||||
this.getNostroVostroAccounts(scbId),
|
||||
]);
|
||||
|
||||
return { fiDirectory, nostroVostroAccounts };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FI directory
|
||||
*/
|
||||
async getFIDirectory(scbId: string): Promise<FIInfo[]> {
|
||||
// Placeholder - would query actual FI table
|
||||
// For now, return empty array as FIs might be stored differently
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Nostro/Vostro accounts
|
||||
*/
|
||||
async getNostroVostroAccounts(scbId: string): Promise<NostroVostroAccount[]> {
|
||||
// Placeholder - would query Nostro/Vostro account table
|
||||
// These might be stored as BankAccount with specific types
|
||||
const accounts = await prisma.bankAccount.findMany({
|
||||
where: {
|
||||
sovereignBankId: scbId,
|
||||
accountType: {
|
||||
in: ['nostro', 'vostro'],
|
||||
},
|
||||
},
|
||||
take: 100,
|
||||
});
|
||||
|
||||
return accounts.map((account) => ({
|
||||
accountId: account.accountId,
|
||||
accountType: account.accountType as 'nostro' | 'vostro',
|
||||
counterpartyFI: account.counterpartyId || 'Unknown',
|
||||
currencyCode: account.currencyCode,
|
||||
balance: parseFloat(account.balance.toString()),
|
||||
limits: {
|
||||
dailyLimit: 1000000, // Placeholder
|
||||
perTransactionLimit: 100000, // Placeholder
|
||||
},
|
||||
status: account.status === 'active' ? 'active' : 'frozen',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const fiManagementService = new FIManagementService();
|
||||
|
||||
289
src/core/admin/scb-admin/dashboards/scb-overview.service.ts
Normal file
289
src/core/admin/scb-admin/dashboards/scb-overview.service.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
// SCB Admin Console - SCB Overview Dashboard Service
|
||||
// Domestic network health, corridor view, local GRU & CBDC, risk & compliance
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { dashboardService } from '@/core/compliance/regtech/dashboard.service';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface DomesticNetworkHealth {
|
||||
fiCount: number;
|
||||
activeFIs: number;
|
||||
paymentRails: Array<{
|
||||
railType: string;
|
||||
status: 'active' | 'degraded' | 'down';
|
||||
volume24h: number;
|
||||
}>;
|
||||
cbdcStatus: {
|
||||
totalInCirculation: number;
|
||||
wallets: {
|
||||
retail: number;
|
||||
wholesale: number;
|
||||
institutional: number;
|
||||
};
|
||||
};
|
||||
nostroVostroStatus: {
|
||||
totalAccounts: number;
|
||||
activeAccounts: number;
|
||||
apiEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CorridorView {
|
||||
corridors: Array<{
|
||||
targetSCB: string;
|
||||
targetSCBName: string;
|
||||
volume24h: number;
|
||||
latency: number;
|
||||
riskFlags: number;
|
||||
preferredSettlementAsset: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LocalGRUCBDC {
|
||||
gruBalances: {
|
||||
sr3: number;
|
||||
m0: number;
|
||||
};
|
||||
cbdcInCirculation: {
|
||||
rCBDC: number;
|
||||
wCBDC: number;
|
||||
iCBDC: number;
|
||||
};
|
||||
walletsByType: {
|
||||
retail: number;
|
||||
wholesale: number;
|
||||
institutional: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocalRiskCompliance {
|
||||
sareView: {
|
||||
sovereignRisk: number;
|
||||
fiLevelExposure: Array<{
|
||||
fiId: string;
|
||||
fiName: string;
|
||||
exposure: number;
|
||||
}>;
|
||||
};
|
||||
ariAlerts: Array<{
|
||||
alertId: string;
|
||||
type: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
}>;
|
||||
suspiciousFlows: Array<{
|
||||
flowId: string;
|
||||
description: string;
|
||||
riskLevel: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SCBOverviewDashboard {
|
||||
domesticNetwork: DomesticNetworkHealth;
|
||||
corridorView: CorridorView;
|
||||
localGRUCBDC: LocalGRUCBDC;
|
||||
riskCompliance: LocalRiskCompliance;
|
||||
}
|
||||
|
||||
export class SCBOverviewService {
|
||||
/**
|
||||
* Get SCB overview dashboard
|
||||
*/
|
||||
async getSCBOverview(scbId: string): Promise<SCBOverviewDashboard> {
|
||||
const [domesticNetwork, corridorView, localGRUCBDC, riskCompliance] = await Promise.all([
|
||||
this.getDomesticNetworkHealth(scbId),
|
||||
this.getCorridorView(scbId),
|
||||
this.getLocalGRUCBDC(scbId),
|
||||
this.getLocalRiskCompliance(scbId),
|
||||
]);
|
||||
|
||||
return {
|
||||
domesticNetwork,
|
||||
corridorView,
|
||||
localGRUCBDC,
|
||||
riskCompliance,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domestic network health
|
||||
*/
|
||||
async getDomesticNetworkHealth(scbId: string): Promise<DomesticNetworkHealth> {
|
||||
// Get CBDC wallets
|
||||
const wallets = await prisma.cbdcWallet.findMany({
|
||||
where: { sovereignBankId: scbId },
|
||||
});
|
||||
|
||||
const cbdcInCirculation = wallets.reduce(
|
||||
(sum, w) => sum.plus(w.balance),
|
||||
new Decimal(0)
|
||||
).toNumber();
|
||||
|
||||
// Get payment rails (placeholder)
|
||||
const paymentRails = [
|
||||
{
|
||||
railType: 'RTGS',
|
||||
status: 'active' as const,
|
||||
volume24h: 0,
|
||||
},
|
||||
{
|
||||
railType: 'CBDC',
|
||||
status: 'active' as const,
|
||||
volume24h: cbdcInCirculation,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
fiCount: 0, // Would query FI table
|
||||
activeFIs: 0,
|
||||
paymentRails,
|
||||
cbdcStatus: {
|
||||
totalInCirculation: cbdcInCirculation,
|
||||
wallets: {
|
||||
retail: wallets.filter((w) => w.walletType === 'retail').length,
|
||||
wholesale: wallets.filter((w) => w.walletType === 'wholesale').length,
|
||||
institutional: wallets.filter((w) => w.walletType === 'institutional').length,
|
||||
},
|
||||
},
|
||||
nostroVostroStatus: {
|
||||
totalAccounts: 0, // Would query Nostro/Vostro accounts
|
||||
activeAccounts: 0,
|
||||
apiEnabled: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get corridor view
|
||||
*/
|
||||
async getCorridorView(scbId: string): Promise<CorridorView> {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// Get routes from this SCB
|
||||
const routes = await prisma.settlementRoute.findMany({
|
||||
where: {
|
||||
OR: [{ sourceBankId: scbId }, { destinationBankId: scbId }],
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
const corridors = [];
|
||||
|
||||
for (const route of routes) {
|
||||
const targetSCBId = route.sourceBankId === scbId ? route.destinationBankId : route.sourceBankId;
|
||||
const targetSCB = await prisma.sovereignBank.findUnique({
|
||||
where: { id: targetSCBId },
|
||||
});
|
||||
|
||||
// Get 24h volume
|
||||
const settlements = await prisma.atomicSettlement.findMany({
|
||||
where: {
|
||||
sourceBankId: route.sourceBankId,
|
||||
destinationBankId: route.destinationBankId,
|
||||
createdAt: { gte: oneDayAgo },
|
||||
},
|
||||
});
|
||||
|
||||
const volume24h = settlements
|
||||
.filter((s) => s.status === 'settled')
|
||||
.reduce((sum, s) => sum.plus(s.amount), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
// Get risk flags (SRI enforcements)
|
||||
const riskFlags = await prisma.sRIEnforcement.count({
|
||||
where: {
|
||||
sovereignBankId: targetSCBId,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
corridors.push({
|
||||
targetSCB: targetSCBId,
|
||||
targetSCBName: targetSCB?.name || 'Unknown',
|
||||
volume24h,
|
||||
latency: route.estimatedLatency || 0,
|
||||
riskFlags,
|
||||
preferredSettlementAsset: route.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
return { corridors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local GRU & CBDC
|
||||
*/
|
||||
async getLocalGRUCBDC(scbId: string): Promise<LocalGRUCBDC> {
|
||||
// Get CBDC wallets
|
||||
const wallets = await prisma.cbdcWallet.findMany({
|
||||
where: { sovereignBankId: scbId },
|
||||
});
|
||||
|
||||
const rCBDC = wallets
|
||||
.filter((w) => w.walletType === 'retail')
|
||||
.reduce((sum, w) => sum.plus(w.balance), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
const wCBDC = wallets
|
||||
.filter((w) => w.walletType === 'wholesale')
|
||||
.reduce((sum, w) => sum.plus(w.balance), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
const iCBDC = wallets
|
||||
.filter((w) => w.walletType === 'institutional')
|
||||
.reduce((sum, w) => sum.plus(w.balance), new Decimal(0))
|
||||
.toNumber();
|
||||
|
||||
return {
|
||||
gruBalances: {
|
||||
sr3: 0, // Would query GRU balances
|
||||
m0: 0,
|
||||
},
|
||||
cbdcInCirculation: {
|
||||
rCBDC,
|
||||
wCBDC,
|
||||
iCBDC,
|
||||
},
|
||||
walletsByType: {
|
||||
retail: wallets.filter((w) => w.walletType === 'retail').length,
|
||||
wholesale: wallets.filter((w) => w.walletType === 'wholesale').length,
|
||||
institutional: wallets.filter((w) => w.walletType === 'institutional').length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local risk & compliance
|
||||
*/
|
||||
async getLocalRiskCompliance(scbId: string): Promise<LocalRiskCompliance> {
|
||||
// Get SRI
|
||||
const sri = await prisma.sovereignRiskIndex.findFirst({
|
||||
where: { sovereignBankId: scbId },
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
});
|
||||
|
||||
// Get ARI alerts
|
||||
const dashboard = await dashboardService.getIncidentAlertsDashboard(scbId);
|
||||
const ariAlerts = (dashboard.incidentAlerts || []).map((alert, index) => ({
|
||||
alertId: `ari-${scbId}-${index}`,
|
||||
type: alert.type,
|
||||
severity: alert.severity,
|
||||
description: alert.description,
|
||||
timestamp: alert.timestamp,
|
||||
}));
|
||||
|
||||
return {
|
||||
sareView: {
|
||||
sovereignRisk: sri ? parseFloat(sri.sriScore.toString()) : 0,
|
||||
fiLevelExposure: [], // Would query FI exposures
|
||||
},
|
||||
ariAlerts,
|
||||
suspiciousFlows: [], // Would query suspicious transaction patterns
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const scbOverviewService = new SCBOverviewService();
|
||||
|
||||
172
src/core/admin/scb-admin/scb-admin.routes.ts
Normal file
172
src/core/admin/scb-admin/scb-admin.routes.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
// SCB Admin Console API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { scbAdminService } from './scb-admin.service';
|
||||
import { requireAdminPermission, requireSCBAccess } from '@/integration/api-gateway/middleware/admin-permission.middleware';
|
||||
import { AdminPermission } from '@/core/admin/shared/permissions.constants';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// SCB Overview Dashboard
|
||||
router.get(
|
||||
'/dashboard/overview',
|
||||
requireAdminPermission(AdminPermission.VIEW_SCB_OVERVIEW),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const overview = await scbAdminService.overview.getSCBOverview(scbId);
|
||||
res.json(overview);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// FI Management
|
||||
router.get(
|
||||
'/fi',
|
||||
requireAdminPermission(AdminPermission.VIEW_FI_MANAGEMENT),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const dashboard = await scbAdminService.fiManagement.getFIManagementDashboard(scbId);
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/fi/approve-suspend',
|
||||
requireAdminPermission(AdminPermission.FI_APPROVE_SUSPEND),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const employeeId = req.headers['x-employee-id'] as string || scbId;
|
||||
const result = await scbAdminService.fiControls.approveSuspendFI(
|
||||
employeeId,
|
||||
scbId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/fi/limits',
|
||||
requireAdminPermission(AdminPermission.FI_SET_LIMITS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const employeeId = req.headers['x-employee-id'] as string || scbId;
|
||||
const result = await scbAdminService.fiControls.setFILimits(employeeId, scbId, req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/fi/api-profile',
|
||||
requireAdminPermission(AdminPermission.FI_API_PROFILES),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const employeeId = req.headers['x-employee-id'] as string || scbId;
|
||||
const result = await scbAdminService.fiControls.assignAPIProfile(
|
||||
employeeId,
|
||||
scbId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Corridor & FX Policy
|
||||
router.get(
|
||||
'/corridors',
|
||||
requireAdminPermission(AdminPermission.VIEW_CORRIDOR_POLICY),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const dashboard = await scbAdminService.corridorPolicy.getCorridorPolicyDashboard(scbId);
|
||||
res.json(dashboard);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// CBDC Controls
|
||||
router.post(
|
||||
'/cbdc/parameters',
|
||||
requireAdminPermission(AdminPermission.CBDC_UPDATE_PARAMETERS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const employeeId = req.headers['x-employee-id'] as string || scbId;
|
||||
const result = await scbAdminService.cbdcControls.updateCBDCParameters(
|
||||
employeeId,
|
||||
scbId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/gru/policy',
|
||||
requireAdminPermission(AdminPermission.CBDC_UPDATE_PARAMETERS),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const scbId = req.sovereignBankId;
|
||||
if (!scbId) {
|
||||
return res.status(400).json({ error: 'Sovereign Bank ID required' });
|
||||
}
|
||||
const employeeId = req.headers['x-employee-id'] as string || scbId;
|
||||
const result = await scbAdminService.cbdcControls.updateGRUPolicy(
|
||||
employeeId,
|
||||
scbId,
|
||||
req.body
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
35
src/core/admin/scb-admin/scb-admin.service.ts
Normal file
35
src/core/admin/scb-admin/scb-admin.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// SCB Admin Console - Main Service
|
||||
// Orchestrates all SCB admin operations
|
||||
|
||||
import { scbOverviewService } from './dashboards/scb-overview.service';
|
||||
import { fiManagementService } from './dashboards/fi-management.service';
|
||||
import { corridorPolicyService } from './dashboards/corridor-policy.service';
|
||||
import { fiControlsService } from './controls/fi-controls.service';
|
||||
import { cbdcControlsService } from './controls/cbdc-controls.service';
|
||||
|
||||
export class SCBAdminService {
|
||||
// Dashboard services
|
||||
get overview() {
|
||||
return scbOverviewService;
|
||||
}
|
||||
|
||||
get fiManagement() {
|
||||
return fiManagementService;
|
||||
}
|
||||
|
||||
get corridorPolicy() {
|
||||
return corridorPolicyService;
|
||||
}
|
||||
|
||||
// Control services
|
||||
get fiControls() {
|
||||
return fiControlsService;
|
||||
}
|
||||
|
||||
get cbdcControls() {
|
||||
return cbdcControlsService;
|
||||
}
|
||||
}
|
||||
|
||||
export const scbAdminService = new SCBAdminService();
|
||||
|
||||
100
src/core/admin/shared/admin-audit.service.ts
Normal file
100
src/core/admin/shared/admin-audit.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// Admin Audit Service
|
||||
// Audit logging for all admin console actions
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { AdminPermission } from './permissions.constants';
|
||||
|
||||
export interface AdminActionAudit {
|
||||
employeeId: string;
|
||||
action: string;
|
||||
permission: AdminPermission;
|
||||
resourceType: string;
|
||||
resourceId?: string;
|
||||
beforeState?: Record<string, unknown>;
|
||||
afterState?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
export class AdminAuditService {
|
||||
/**
|
||||
* Log admin action
|
||||
*/
|
||||
async logAction(audit: AdminActionAudit): Promise<void> {
|
||||
try {
|
||||
// Store in audit log (extend existing audit infrastructure)
|
||||
// For now, we'll use logger and could extend to database table
|
||||
logger.info('Admin action', {
|
||||
auditId: uuidv4(),
|
||||
employeeId: audit.employeeId,
|
||||
action: audit.action,
|
||||
permission: audit.permission,
|
||||
resourceType: audit.resourceType,
|
||||
resourceId: audit.resourceId,
|
||||
beforeState: audit.beforeState,
|
||||
afterState: audit.afterState,
|
||||
metadata: audit.metadata,
|
||||
ipAddress: audit.ipAddress,
|
||||
userAgent: audit.userAgent,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
// TODO: Store in AdminActionAudit table when schema is added
|
||||
} catch (error) {
|
||||
logger.error('Error logging admin action', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
audit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit log for employee
|
||||
*/
|
||||
async getAuditLog(
|
||||
employeeId?: string,
|
||||
resourceType?: string,
|
||||
limit: number = 100
|
||||
): Promise<AdminActionAudit[]> {
|
||||
try {
|
||||
// TODO: Query from AdminActionAudit table
|
||||
// For now, return empty array
|
||||
return [];
|
||||
} catch (error) {
|
||||
logger.error('Error getting audit log', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
employeeId,
|
||||
resourceType,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit log for regulators
|
||||
*/
|
||||
async exportAuditLog(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
employeeId?: string
|
||||
): Promise<AdminActionAudit[]> {
|
||||
try {
|
||||
// TODO: Query and format for export
|
||||
return [];
|
||||
} catch (error) {
|
||||
logger.error('Error exporting audit log', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
startDate,
|
||||
endDate,
|
||||
employeeId,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const adminAuditService = new AdminAuditService();
|
||||
|
||||
192
src/core/admin/shared/admin-permissions.service.ts
Normal file
192
src/core/admin/shared/admin-permissions.service.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Admin Permissions Service
|
||||
// Permission checking and role management for admin consoles
|
||||
|
||||
import { AdminPermission, AdminRole, ROLE_PERMISSIONS } from './permissions.constants';
|
||||
import { roleManagementService } from '@/core/operations/role-management.service';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
export interface PermissionCheckResult {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class AdminPermissionsService {
|
||||
/**
|
||||
* Check if employee has specific permission
|
||||
*/
|
||||
async hasPermission(
|
||||
employeeId: string,
|
||||
permission: AdminPermission
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// First check if they have 'all' permission (DBIS Governor)
|
||||
const hasAll = await roleManagementService.hasPermission(employeeId, 'all');
|
||||
if (hasAll) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get employee's role
|
||||
const roleName = await this.getEmployeeRoleName(employeeId);
|
||||
if (!roleName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Map DBIS role names to AdminRole enum
|
||||
const adminRole = this.mapRoleToAdminRole(roleName);
|
||||
if (!adminRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if role has permission
|
||||
const rolePermissions = ROLE_PERMISSIONS[adminRole] || [];
|
||||
return rolePermissions.includes(permission);
|
||||
} catch (error) {
|
||||
logger.error('Error checking permission', {
|
||||
employeeId,
|
||||
permission,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permission with detailed result
|
||||
*/
|
||||
async checkPermission(
|
||||
employeeId: string,
|
||||
permission: AdminPermission
|
||||
): Promise<PermissionCheckResult> {
|
||||
const allowed = await this.hasPermission(employeeId, permission);
|
||||
return {
|
||||
allowed,
|
||||
reason: allowed ? undefined : 'Insufficient permissions',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if employee can perform action on specific SCB
|
||||
* SCB admins can only act on their own SCB
|
||||
*/
|
||||
async canActOnSCB(
|
||||
employeeId: string,
|
||||
targetSCBId: string,
|
||||
employeeSCBId?: string
|
||||
): Promise<boolean> {
|
||||
// DBIS admins can act on any SCB
|
||||
const hasAll = await roleManagementService.hasPermission(employeeId, 'all');
|
||||
if (hasAll) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// SCB admins can only act on their own SCB
|
||||
if (employeeSCBId && employeeSCBId === targetSCBId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions for an employee
|
||||
*/
|
||||
async getEmployeePermissions(employeeId: string): Promise<AdminPermission[]> {
|
||||
try {
|
||||
const hasAll = await roleManagementService.hasPermission(employeeId, 'all');
|
||||
if (hasAll) {
|
||||
// Return all permissions
|
||||
return Object.values(AdminPermission);
|
||||
}
|
||||
|
||||
// Get employee to find their role
|
||||
const prisma = (await import('@/shared/database/prisma')).default;
|
||||
const employee = await prisma.employeeCredential.findUnique({
|
||||
where: { employeeId },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
if (!employee || employee.status !== 'active') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const adminRole = this.mapRoleToAdminRole(employee.role.roleName);
|
||||
if (!adminRole) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ROLE_PERMISSIONS[adminRole] || [];
|
||||
} catch (error) {
|
||||
logger.error('Error getting employee permissions', {
|
||||
employeeId,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get employee's role name
|
||||
*/
|
||||
async getEmployeeRoleName(employeeId: string): Promise<string | null> {
|
||||
try {
|
||||
const prisma = (await import('@/shared/database/prisma')).default;
|
||||
const employee = await prisma.employeeCredential.findUnique({
|
||||
where: { employeeId },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
return employee?.role.roleName || null;
|
||||
} catch (error) {
|
||||
logger.error('Error getting employee role', {
|
||||
employeeId,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map DBIS role name to AdminRole enum
|
||||
*/
|
||||
private mapRoleToAdminRole(roleName: string): AdminRole | null {
|
||||
const roleMap: Record<string, AdminRole> = {
|
||||
Governor: AdminRole.DBIS_SUPER_ADMIN,
|
||||
MSC_Officer: AdminRole.DBIS_OPS,
|
||||
CAA_Auditor: AdminRole.DBIS_RISK,
|
||||
DBIS_Super_Admin: AdminRole.DBIS_SUPER_ADMIN,
|
||||
DBIS_Ops: AdminRole.DBIS_OPS,
|
||||
DBIS_Risk: AdminRole.DBIS_RISK,
|
||||
SCB_Admin: AdminRole.SCB_ADMIN,
|
||||
SCB_Risk: AdminRole.SCB_RISK,
|
||||
SCB_Tech: AdminRole.SCB_TECH,
|
||||
SCB_Read_Only: AdminRole.SCB_READ_ONLY,
|
||||
};
|
||||
|
||||
return roleMap[roleName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is DBIS-level (can act globally)
|
||||
*/
|
||||
async isDBISLevel(employeeId: string): Promise<boolean> {
|
||||
const hasAll = await roleManagementService.hasPermission(employeeId, 'all');
|
||||
if (hasAll) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const roleName = await this.getEmployeeRoleName(employeeId);
|
||||
if (!roleName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const adminRole = this.mapRoleToAdminRole(roleName);
|
||||
return (
|
||||
adminRole === AdminRole.DBIS_SUPER_ADMIN ||
|
||||
adminRole === AdminRole.DBIS_OPS ||
|
||||
adminRole === AdminRole.DBIS_RISK
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const adminPermissionsService = new AdminPermissionsService();
|
||||
|
||||
217
src/core/admin/shared/permissions.constants.ts
Normal file
217
src/core/admin/shared/permissions.constants.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
// Admin Console Permission Constants
|
||||
// Granular permissions for DBIS and SCB admin consoles
|
||||
|
||||
export enum AdminPermission {
|
||||
// GRU Management
|
||||
GRU_CREATE_CLASS = 'gru:create_class',
|
||||
GRU_CHANGE_CLASS = 'gru:change_class',
|
||||
GRU_LOCK_UNLOCK = 'gru:lock_unlock',
|
||||
GRU_ISSUANCE_PROPOSAL = 'gru:issuance_proposal',
|
||||
GRU_INDEX_WEIGHT_ADJUST = 'gru:index_weight_adjust',
|
||||
GRU_CIRCUIT_BREAKERS = 'gru:circuit_breakers',
|
||||
GRU_BOND_ISSUANCE_WINDOW = 'gru:bond_issuance_window',
|
||||
GRU_BOND_BUYBACK = 'gru:bond_buyback',
|
||||
|
||||
// Corridor Management
|
||||
CORRIDOR_ADJUST_CAPS = 'corridor:adjust_caps',
|
||||
CORRIDOR_THROTTLE = 'corridor:throttle',
|
||||
CORRIDOR_ENABLE_DISABLE = 'corridor:enable_disable',
|
||||
CORRIDOR_REQUEST_CHANGE = 'corridor:request_change',
|
||||
|
||||
// Network Controls
|
||||
NETWORK_QUIESCE_SUBSYSTEM = 'network:quiesce_subsystem',
|
||||
NETWORK_KILL_SWITCH = 'network:kill_switch',
|
||||
NETWORK_ESCALATE_INCIDENT = 'network:escalate_incident',
|
||||
|
||||
// SCB Management
|
||||
SCB_PAUSE_SETTLEMENT = 'scb:pause_settlement',
|
||||
SCB_VIEW_DETAILS = 'scb:view_details',
|
||||
SCB_IMPERSONATE_VIEW = 'scb:impersonate_view',
|
||||
SCB_JURISDICTION_SETTINGS = 'scb:jurisdiction_settings',
|
||||
|
||||
// FI Management
|
||||
FI_APPROVE_SUSPEND = 'fi:approve_suspend',
|
||||
FI_SET_LIMITS = 'fi:set_limits',
|
||||
FI_API_PROFILES = 'fi:api_profiles',
|
||||
|
||||
// CBDC Management
|
||||
CBDC_APPROVE_TYPE = 'cbdc:approve_type',
|
||||
CBDC_CROSS_BORDER_CORRIDOR = 'cbdc:cross_border_corridor',
|
||||
CBDC_UPDATE_PARAMETERS = 'cbdc:update_parameters',
|
||||
|
||||
// GAS & QPS
|
||||
GAS_SET_LIMITS = 'gas:set_limits',
|
||||
GAS_ENABLE_DISABLE_SETTLEMENT = 'gas:enable_disable_settlement',
|
||||
GAS_THROTTLE_BANDWIDTH = 'gas:throttle_bandwidth',
|
||||
QPS_ENABLE_DISABLE = 'qps:enable_disable',
|
||||
QPS_SET_MAPPING_PROFILES = 'qps:set_mapping_profiles',
|
||||
|
||||
// Metaverse & Edge
|
||||
METAVERSE_ENABLE_ONRAMP = 'metaverse:enable_onramp',
|
||||
METAVERSE_SET_LIMITS = 'metaverse:set_limits',
|
||||
EDGE_DRAIN_LOAD = 'edge:drain_load',
|
||||
EDGE_QUARANTINE = 'edge:quarantine',
|
||||
|
||||
// Risk & Compliance
|
||||
RISK_ACKNOWLEDGE_ALERT = 'risk:acknowledge_alert',
|
||||
RISK_TRIGGER_STRESS_TEST = 'risk:trigger_stress_test',
|
||||
RISK_PUSH_POLICY_UPDATE = 'risk:push_policy_update',
|
||||
RISK_MARK_SCENARIO = 'risk:mark_scenario',
|
||||
|
||||
// Nostro/Vostro
|
||||
NOSTRO_VOSTRO_OPEN = 'nostro_vostro:open',
|
||||
NOSTRO_VOSTRO_ADJUST_LIMITS = 'nostro_vostro:adjust_limits',
|
||||
NOSTRO_VOSTRO_FREEZE = 'nostro_vostro:freeze',
|
||||
|
||||
// Developer & Integrations
|
||||
API_KEY_ROTATE = 'api:key_rotate',
|
||||
API_KEY_REVOKE = 'api:key_revoke',
|
||||
API_SANDBOX_MODE = 'api:sandbox_mode',
|
||||
|
||||
// Security & Identity
|
||||
RBAC_EDIT = 'rbac:edit',
|
||||
AUDIT_EXPORT = 'audit:export',
|
||||
|
||||
// Read-only permissions
|
||||
VIEW_GLOBAL_OVERVIEW = 'view:global_overview',
|
||||
VIEW_PARTICIPANTS = 'view:participants',
|
||||
VIEW_GRU_COMMAND = 'view:gru_command',
|
||||
VIEW_GAS_QPS = 'view:gas_qps',
|
||||
VIEW_CBDC_FX = 'view:cbdc_fx',
|
||||
VIEW_METAVERSE_EDGE = 'view:metaverse_edge',
|
||||
VIEW_RISK_COMPLIANCE = 'view:risk_compliance',
|
||||
VIEW_SCB_OVERVIEW = 'view:scb_overview',
|
||||
VIEW_FI_MANAGEMENT = 'view:fi_management',
|
||||
VIEW_CORRIDOR_POLICY = 'view:corridor_policy',
|
||||
}
|
||||
|
||||
export enum AdminRole {
|
||||
DBIS_SUPER_ADMIN = 'DBIS_Super_Admin',
|
||||
DBIS_OPS = 'DBIS_Ops',
|
||||
DBIS_RISK = 'DBIS_Risk',
|
||||
SCB_ADMIN = 'SCB_Admin',
|
||||
SCB_RISK = 'SCB_Risk',
|
||||
SCB_TECH = 'SCB_Tech',
|
||||
SCB_READ_ONLY = 'SCB_Read_Only',
|
||||
}
|
||||
|
||||
// Permission matrix: Role -> Permissions
|
||||
export const ROLE_PERMISSIONS: Record<AdminRole, AdminPermission[]> = {
|
||||
[AdminRole.DBIS_SUPER_ADMIN]: [
|
||||
// All permissions
|
||||
AdminPermission.GRU_CREATE_CLASS,
|
||||
AdminPermission.GRU_CHANGE_CLASS,
|
||||
AdminPermission.GRU_LOCK_UNLOCK,
|
||||
AdminPermission.GRU_ISSUANCE_PROPOSAL,
|
||||
AdminPermission.GRU_INDEX_WEIGHT_ADJUST,
|
||||
AdminPermission.GRU_CIRCUIT_BREAKERS,
|
||||
AdminPermission.GRU_BOND_ISSUANCE_WINDOW,
|
||||
AdminPermission.GRU_BOND_BUYBACK,
|
||||
AdminPermission.CORRIDOR_ADJUST_CAPS,
|
||||
AdminPermission.CORRIDOR_THROTTLE,
|
||||
AdminPermission.CORRIDOR_ENABLE_DISABLE,
|
||||
AdminPermission.NETWORK_QUIESCE_SUBSYSTEM,
|
||||
AdminPermission.NETWORK_KILL_SWITCH,
|
||||
AdminPermission.NETWORK_ESCALATE_INCIDENT,
|
||||
AdminPermission.SCB_PAUSE_SETTLEMENT,
|
||||
AdminPermission.SCB_VIEW_DETAILS,
|
||||
AdminPermission.SCB_IMPERSONATE_VIEW,
|
||||
AdminPermission.SCB_JURISDICTION_SETTINGS,
|
||||
AdminPermission.FI_APPROVE_SUSPEND,
|
||||
AdminPermission.FI_SET_LIMITS,
|
||||
AdminPermission.FI_API_PROFILES,
|
||||
AdminPermission.CBDC_APPROVE_TYPE,
|
||||
AdminPermission.CBDC_CROSS_BORDER_CORRIDOR,
|
||||
AdminPermission.CBDC_UPDATE_PARAMETERS,
|
||||
AdminPermission.GAS_SET_LIMITS,
|
||||
AdminPermission.GAS_ENABLE_DISABLE_SETTLEMENT,
|
||||
AdminPermission.GAS_THROTTLE_BANDWIDTH,
|
||||
AdminPermission.QPS_ENABLE_DISABLE,
|
||||
AdminPermission.QPS_SET_MAPPING_PROFILES,
|
||||
AdminPermission.METAVERSE_ENABLE_ONRAMP,
|
||||
AdminPermission.METAVERSE_SET_LIMITS,
|
||||
AdminPermission.EDGE_DRAIN_LOAD,
|
||||
AdminPermission.EDGE_QUARANTINE,
|
||||
AdminPermission.RISK_ACKNOWLEDGE_ALERT,
|
||||
AdminPermission.RISK_TRIGGER_STRESS_TEST,
|
||||
AdminPermission.RISK_PUSH_POLICY_UPDATE,
|
||||
AdminPermission.RISK_MARK_SCENARIO,
|
||||
AdminPermission.NOSTRO_VOSTRO_OPEN,
|
||||
AdminPermission.NOSTRO_VOSTRO_ADJUST_LIMITS,
|
||||
AdminPermission.NOSTRO_VOSTRO_FREEZE,
|
||||
AdminPermission.API_KEY_ROTATE,
|
||||
AdminPermission.API_KEY_REVOKE,
|
||||
AdminPermission.API_SANDBOX_MODE,
|
||||
AdminPermission.RBAC_EDIT,
|
||||
AdminPermission.AUDIT_EXPORT,
|
||||
AdminPermission.VIEW_GLOBAL_OVERVIEW,
|
||||
AdminPermission.VIEW_PARTICIPANTS,
|
||||
AdminPermission.VIEW_GRU_COMMAND,
|
||||
AdminPermission.VIEW_GAS_QPS,
|
||||
AdminPermission.VIEW_CBDC_FX,
|
||||
AdminPermission.VIEW_METAVERSE_EDGE,
|
||||
AdminPermission.VIEW_RISK_COMPLIANCE,
|
||||
AdminPermission.VIEW_SCB_OVERVIEW,
|
||||
AdminPermission.VIEW_FI_MANAGEMENT,
|
||||
AdminPermission.VIEW_CORRIDOR_POLICY,
|
||||
],
|
||||
[AdminRole.DBIS_OPS]: [
|
||||
AdminPermission.VIEW_GLOBAL_OVERVIEW,
|
||||
AdminPermission.VIEW_PARTICIPANTS,
|
||||
AdminPermission.VIEW_GAS_QPS,
|
||||
AdminPermission.VIEW_METAVERSE_EDGE,
|
||||
AdminPermission.NETWORK_ESCALATE_INCIDENT,
|
||||
AdminPermission.GAS_THROTTLE_BANDWIDTH,
|
||||
AdminPermission.QPS_ENABLE_DISABLE,
|
||||
AdminPermission.EDGE_DRAIN_LOAD,
|
||||
AdminPermission.RISK_ACKNOWLEDGE_ALERT,
|
||||
],
|
||||
[AdminRole.DBIS_RISK]: [
|
||||
AdminPermission.VIEW_GLOBAL_OVERVIEW,
|
||||
AdminPermission.VIEW_RISK_COMPLIANCE,
|
||||
AdminPermission.RISK_ACKNOWLEDGE_ALERT,
|
||||
AdminPermission.RISK_TRIGGER_STRESS_TEST,
|
||||
AdminPermission.RISK_MARK_SCENARIO,
|
||||
AdminPermission.AUDIT_EXPORT,
|
||||
],
|
||||
[AdminRole.SCB_ADMIN]: [
|
||||
AdminPermission.VIEW_SCB_OVERVIEW,
|
||||
AdminPermission.VIEW_FI_MANAGEMENT,
|
||||
AdminPermission.VIEW_CORRIDOR_POLICY,
|
||||
AdminPermission.FI_APPROVE_SUSPEND,
|
||||
AdminPermission.FI_SET_LIMITS,
|
||||
AdminPermission.FI_API_PROFILES,
|
||||
AdminPermission.CORRIDOR_REQUEST_CHANGE,
|
||||
AdminPermission.CBDC_UPDATE_PARAMETERS,
|
||||
AdminPermission.NOSTRO_VOSTRO_OPEN,
|
||||
AdminPermission.NOSTRO_VOSTRO_ADJUST_LIMITS,
|
||||
AdminPermission.NOSTRO_VOSTRO_FREEZE,
|
||||
AdminPermission.API_KEY_ROTATE,
|
||||
AdminPermission.API_KEY_REVOKE,
|
||||
AdminPermission.API_SANDBOX_MODE,
|
||||
AdminPermission.RISK_ACKNOWLEDGE_ALERT,
|
||||
AdminPermission.RISK_TRIGGER_STRESS_TEST,
|
||||
],
|
||||
[AdminRole.SCB_RISK]: [
|
||||
AdminPermission.VIEW_SCB_OVERVIEW,
|
||||
AdminPermission.VIEW_FI_MANAGEMENT,
|
||||
AdminPermission.VIEW_CORRIDOR_POLICY,
|
||||
AdminPermission.RISK_ACKNOWLEDGE_ALERT,
|
||||
AdminPermission.RISK_MARK_SCENARIO,
|
||||
AdminPermission.RISK_TRIGGER_STRESS_TEST,
|
||||
],
|
||||
[AdminRole.SCB_TECH]: [
|
||||
AdminPermission.VIEW_SCB_OVERVIEW,
|
||||
AdminPermission.VIEW_FI_MANAGEMENT,
|
||||
AdminPermission.FI_API_PROFILES,
|
||||
AdminPermission.API_KEY_ROTATE,
|
||||
AdminPermission.API_KEY_REVOKE,
|
||||
AdminPermission.API_SANDBOX_MODE,
|
||||
],
|
||||
[AdminRole.SCB_READ_ONLY]: [
|
||||
AdminPermission.VIEW_SCB_OVERVIEW,
|
||||
AdminPermission.VIEW_FI_MANAGEMENT,
|
||||
AdminPermission.VIEW_CORRIDOR_POLICY,
|
||||
],
|
||||
};
|
||||
|
||||
173
src/core/audit/gap-engine/gap-audit-engine.service.ts
Normal file
173
src/core/audit/gap-engine/gap-audit-engine.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
// Gap Audit Engine Service
|
||||
// Main gap scanning across multiverse systems, temporal ledgers, quantum chains, cognitive-intent layers, DLT/metaverse ecosystems
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { gapDetectionService } from './gap-detection.service';
|
||||
import { moduleGeneratorService } from './module-generator.service';
|
||||
import { recommendationsEngineService } from './recommendations-engine.service';
|
||||
|
||||
|
||||
export interface GapAuditRequest {
|
||||
auditScope: Array<
|
||||
'multiverse' | 'temporal' | 'quantum' | 'cognitive' | 'dlt' | 'metaverse'
|
||||
>;
|
||||
includeRecommendations?: boolean;
|
||||
}
|
||||
|
||||
export interface GapAuditResult {
|
||||
auditId: string;
|
||||
gapsFound: number;
|
||||
modulesGenerated: number;
|
||||
recommendationsCount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class GapAuditEngineService {
|
||||
/**
|
||||
* Execute comprehensive gap audit
|
||||
* Scans all DBIS systems for gaps
|
||||
*/
|
||||
async executeGapAudit(
|
||||
request: GapAuditRequest
|
||||
): Promise<GapAuditResult> {
|
||||
logger.info('Gap Audit: Starting comprehensive audit', { request });
|
||||
|
||||
const auditId = `GAP-AUDIT-${uuidv4()}`;
|
||||
|
||||
// Step 1: Create audit record
|
||||
const audit = await prisma.gapAudit.create({
|
||||
data: {
|
||||
auditId,
|
||||
auditScope: request.auditScope as any,
|
||||
status: 'running',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Step 2: Scan for gaps in each scope
|
||||
const allGaps: any[] = [];
|
||||
|
||||
for (const scope of request.auditScope) {
|
||||
const gaps = await gapDetectionService.detectGaps(scope);
|
||||
allGaps.push(...gaps);
|
||||
|
||||
// Save gap detections
|
||||
for (const gap of gaps) {
|
||||
await prisma.gapDetection.create({
|
||||
data: {
|
||||
detectionId: `GAP-DET-${uuidv4()}`,
|
||||
auditId,
|
||||
gapType: gap.gapType,
|
||||
systemScope: scope,
|
||||
description: gap.description,
|
||||
severity: gap.severity,
|
||||
status: 'detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Generate modules for detected gaps
|
||||
let modulesGenerated = 0;
|
||||
for (const gap of allGaps) {
|
||||
if (gap.autoGenerate) {
|
||||
const moduleId = await moduleGeneratorService.generateModule(
|
||||
gap.gapType
|
||||
);
|
||||
if (moduleId) {
|
||||
modulesGenerated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Generate recommendations if requested
|
||||
let recommendationsCount = 0;
|
||||
if (request.includeRecommendations) {
|
||||
const recommendations =
|
||||
await recommendationsEngineService.generateRecommendations(allGaps);
|
||||
recommendationsCount = recommendations.length;
|
||||
|
||||
for (const recommendation of recommendations) {
|
||||
await prisma.systemRecommendation.create({
|
||||
data: {
|
||||
recommendationId: `REC-${uuidv4()}`,
|
||||
auditId,
|
||||
recommendationType: recommendation.type,
|
||||
title: recommendation.title,
|
||||
description: recommendation.description,
|
||||
priority: recommendation.priority,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Update audit status
|
||||
await prisma.gapAudit.update({
|
||||
where: { auditId },
|
||||
data: {
|
||||
status: 'completed',
|
||||
gapsFound: allGaps.length,
|
||||
modulesGenerated,
|
||||
recommendationsCount,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('Gap Audit: Audit completed', {
|
||||
auditId,
|
||||
gapsFound: allGaps.length,
|
||||
modulesGenerated,
|
||||
});
|
||||
|
||||
return {
|
||||
auditId,
|
||||
gapsFound: allGaps.length,
|
||||
modulesGenerated,
|
||||
recommendationsCount,
|
||||
status: 'completed',
|
||||
};
|
||||
} catch (error) {
|
||||
await prisma.gapAudit.update({
|
||||
where: { auditId },
|
||||
data: {
|
||||
status: 'failed',
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit by ID
|
||||
*/
|
||||
async getAudit(auditId: string) {
|
||||
return await prisma.gapAudit.findUnique({
|
||||
where: { auditId },
|
||||
include: {
|
||||
detections: true,
|
||||
recommendations: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit history
|
||||
*/
|
||||
async getAuditHistory(limit: number = 100) {
|
||||
return await prisma.gapAudit.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
include: {
|
||||
detections: true,
|
||||
recommendations: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const gapAuditEngineService = new GapAuditEngineService();
|
||||
|
||||
84
src/core/audit/gap-engine/gap-audit.routes.ts
Normal file
84
src/core/audit/gap-engine/gap-audit.routes.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// Gap Audit API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { gapAuditEngineService } from './gap-audit-engine.service';
|
||||
import { moduleGeneratorService } from './module-generator.service';
|
||||
import { zeroTrustAuthMiddleware } from '@/integration/api-gateway/middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @route POST /api/gap-audit/execute
|
||||
* @desc Execute gap audit
|
||||
*/
|
||||
router.post(
|
||||
'/execute',
|
||||
zeroTrustAuthMiddleware,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const result = await gapAuditEngineService.executeGapAudit(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/gap-audit/:auditId
|
||||
* @desc Get audit by ID
|
||||
*/
|
||||
router.get(
|
||||
'/:auditId',
|
||||
zeroTrustAuthMiddleware,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const audit = await gapAuditEngineService.getAudit(req.params.auditId);
|
||||
if (!audit) {
|
||||
return res.status(404).json({ error: 'Audit not found' });
|
||||
}
|
||||
res.json(audit);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/gap-audit/history
|
||||
* @desc Get audit history
|
||||
*/
|
||||
router.get(
|
||||
'/history',
|
||||
zeroTrustAuthMiddleware,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit as string) || 100;
|
||||
const history = await gapAuditEngineService.getAuditHistory(limit);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/gap-audit/modules
|
||||
* @desc Get generated modules
|
||||
*/
|
||||
router.get(
|
||||
'/modules',
|
||||
zeroTrustAuthMiddleware,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const gapType = req.query.gapType as string | undefined;
|
||||
const modules = await moduleGeneratorService.getGeneratedModules(gapType);
|
||||
res.json(modules);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
169
src/core/audit/gap-engine/gap-detection.service.ts
Normal file
169
src/core/audit/gap-engine/gap-detection.service.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
// Gap Detection Service
|
||||
// Gap detection algorithms for missing components, protocols, layers
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
|
||||
export interface GapDetection {
|
||||
gapType: string;
|
||||
description: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
autoGenerate: boolean;
|
||||
}
|
||||
|
||||
export class GapDetectionService {
|
||||
/**
|
||||
* Detect gaps in specified system scope
|
||||
*/
|
||||
async detectGaps(
|
||||
scope: 'multiverse' | 'temporal' | 'quantum' | 'cognitive' | 'dlt' | 'metaverse'
|
||||
): Promise<GapDetection[]> {
|
||||
logger.info('Gap Detection: Scanning for gaps', { scope });
|
||||
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
switch (scope) {
|
||||
case 'multiverse':
|
||||
gaps.push(...(await this.detectMultiverseGaps()));
|
||||
break;
|
||||
case 'temporal':
|
||||
gaps.push(...(await this.detectTemporalGaps()));
|
||||
break;
|
||||
case 'quantum':
|
||||
gaps.push(...(await this.detectQuantumGaps()));
|
||||
break;
|
||||
case 'cognitive':
|
||||
gaps.push(...(await this.detectCognitiveGaps()));
|
||||
break;
|
||||
case 'dlt':
|
||||
gaps.push(...(await this.detectDltGaps()));
|
||||
break;
|
||||
case 'metaverse':
|
||||
gaps.push(...(await this.detectMetaverseGaps()));
|
||||
break;
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect gaps in multiverse systems
|
||||
*/
|
||||
private async detectMultiverseGaps(): Promise<GapDetection[]> {
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
// Check for missing multiverse settlement layers
|
||||
const multiverseSettlements = await prisma.gasSettlement.count({
|
||||
where: { networkType: 'multiversal' },
|
||||
});
|
||||
|
||||
if (multiverseSettlements === 0) {
|
||||
gaps.push({
|
||||
gapType: 'multiverse_settlement_layer',
|
||||
description: 'Missing multiverse settlement layer implementation',
|
||||
severity: 'high',
|
||||
autoGenerate: true,
|
||||
});
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect gaps in temporal systems
|
||||
*/
|
||||
private async detectTemporalGaps(): Promise<GapDetection[]> {
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
// Check for missing temporal ledger synchronization
|
||||
// In production, would check actual temporal ledger systems
|
||||
gaps.push({
|
||||
gapType: 'temporal_ledger_sync',
|
||||
description: 'Missing temporal ledger synchronization protocol',
|
||||
severity: 'medium',
|
||||
autoGenerate: false,
|
||||
});
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect gaps in quantum systems
|
||||
*/
|
||||
private async detectQuantumGaps(): Promise<GapDetection[]> {
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
// Check for missing quantum financial interfaces
|
||||
const quantumProxies = await prisma.quantumProxyTransaction.count();
|
||||
|
||||
if (quantumProxies === 0) {
|
||||
gaps.push({
|
||||
gapType: 'quantum_financial_interface',
|
||||
description: 'Missing quantum financial system interfaces',
|
||||
severity: 'high',
|
||||
autoGenerate: true,
|
||||
});
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect gaps in cognitive systems
|
||||
*/
|
||||
private async detectCognitiveGaps(): Promise<GapDetection[]> {
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
// Check for missing cognitive-intent layers
|
||||
gaps.push({
|
||||
gapType: 'cognitive_intent_layer',
|
||||
description: 'Missing cognitive-intent processing layer',
|
||||
severity: 'medium',
|
||||
autoGenerate: false,
|
||||
});
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect gaps in DLT systems
|
||||
*/
|
||||
private async detectDltGaps(): Promise<GapDetection[]> {
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
// Check for missing DLT integration
|
||||
gaps.push({
|
||||
gapType: 'dlt_integration',
|
||||
description: 'Missing DLT ecosystem integration',
|
||||
severity: 'low',
|
||||
autoGenerate: false,
|
||||
});
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect gaps in metaverse systems
|
||||
*/
|
||||
private async detectMetaverseGaps(): Promise<GapDetection[]> {
|
||||
const gaps: GapDetection[] = [];
|
||||
|
||||
// Check for missing metaverse support tools
|
||||
const metaverseNodes = await prisma.metaverseNode.count();
|
||||
|
||||
if (metaverseNodes === 0) {
|
||||
gaps.push({
|
||||
gapType: 'metaverse_support_tools',
|
||||
description: 'Missing metaverse support and integration tools',
|
||||
severity: 'medium',
|
||||
autoGenerate: true,
|
||||
});
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
}
|
||||
|
||||
export const gapDetectionService = new GapDetectionService();
|
||||
|
||||
167
src/core/audit/gap-engine/module-generator.service.ts
Normal file
167
src/core/audit/gap-engine/module-generator.service.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
// Module Generator Service
|
||||
// Auto-generation of missing modules (create_module(gap_type))
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
|
||||
export class ModuleGeneratorService {
|
||||
/**
|
||||
* Generate module for detected gap
|
||||
* create_module(gap_type)
|
||||
*/
|
||||
async generateModule(gapType: string): Promise<string | null> {
|
||||
logger.info('Module Generator: Generating module', { gapType });
|
||||
|
||||
// Determine module type and generate accordingly
|
||||
let moduleId: string | null = null;
|
||||
|
||||
switch (gapType) {
|
||||
case 'multiverse_settlement_layer':
|
||||
moduleId = await this.generateMultiverseSettlementLayer();
|
||||
break;
|
||||
|
||||
case 'quantum_financial_interface':
|
||||
moduleId = await this.generateQuantumFinancialInterface();
|
||||
break;
|
||||
|
||||
case 'metaverse_support_tools':
|
||||
moduleId = await this.generateMetaverseSupportTools();
|
||||
break;
|
||||
|
||||
case 'fx_layer':
|
||||
moduleId = await this.generateFxLayer();
|
||||
break;
|
||||
|
||||
case 'identity_anchor':
|
||||
moduleId = await this.generateIdentityAnchor();
|
||||
break;
|
||||
|
||||
case 'qfs_interface':
|
||||
moduleId = await this.generateQfsInterface();
|
||||
break;
|
||||
|
||||
case 'settlement_layer':
|
||||
moduleId = await this.generateSettlementLayer();
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn('Module Generator: Unknown gap type', { gapType });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (moduleId) {
|
||||
// Save generated module record
|
||||
await prisma.generatedModule.create({
|
||||
data: {
|
||||
moduleId,
|
||||
gapType,
|
||||
moduleType: this.getModuleType(gapType),
|
||||
status: 'generated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate multiverse settlement layer
|
||||
*/
|
||||
private async generateMultiverseSettlementLayer(): Promise<string> {
|
||||
const moduleId = `MODULE-MULTIVERSE-SETTLE-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated multiverse settlement layer', {
|
||||
moduleId,
|
||||
});
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate quantum financial interface
|
||||
*/
|
||||
private async generateQuantumFinancialInterface(): Promise<string> {
|
||||
const moduleId = `MODULE-QFS-INTERFACE-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated quantum financial interface', {
|
||||
moduleId,
|
||||
});
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate metaverse support tools
|
||||
*/
|
||||
private async generateMetaverseSupportTools(): Promise<string> {
|
||||
const moduleId = `MODULE-METAVERSE-TOOLS-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated metaverse support tools', {
|
||||
moduleId,
|
||||
});
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate FX layer
|
||||
*/
|
||||
private async generateFxLayer(): Promise<string> {
|
||||
const moduleId = `MODULE-FX-LAYER-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated FX layer', { moduleId });
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate identity anchor
|
||||
*/
|
||||
private async generateIdentityAnchor(): Promise<string> {
|
||||
const moduleId = `MODULE-IDENTITY-ANCHOR-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated identity anchor', { moduleId });
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QFS interface
|
||||
*/
|
||||
private async generateQfsInterface(): Promise<string> {
|
||||
const moduleId = `MODULE-QFS-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated QFS interface', { moduleId });
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate settlement layer
|
||||
*/
|
||||
private async generateSettlementLayer(): Promise<string> {
|
||||
const moduleId = `MODULE-SETTLE-LAYER-${uuidv4()}`;
|
||||
logger.info('Module Generator: Generated settlement layer', { moduleId });
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module type from gap type
|
||||
*/
|
||||
private getModuleType(gapType: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
multiverse_settlement_layer: 'settlement',
|
||||
quantum_financial_interface: 'quantum',
|
||||
metaverse_support_tools: 'metaverse',
|
||||
fx_layer: 'fx',
|
||||
identity_anchor: 'identity',
|
||||
qfs_interface: 'quantum',
|
||||
settlement_layer: 'settlement',
|
||||
};
|
||||
|
||||
return typeMap[gapType] || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get generated modules
|
||||
*/
|
||||
async getGeneratedModules(gapType?: string) {
|
||||
return await prisma.generatedModule.findMany({
|
||||
where: gapType ? { gapType } : {},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const moduleGeneratorService = new ModuleGeneratorService();
|
||||
|
||||
97
src/core/audit/gap-engine/recommendations-engine.service.ts
Normal file
97
src/core/audit/gap-engine/recommendations-engine.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// Recommendations Engine Service
|
||||
// System improvements, additional settlement layers, synthetic assets, AI supervisory engines
|
||||
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
export interface Recommendation {
|
||||
type: string;
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
}
|
||||
|
||||
export class RecommendationsEngineService {
|
||||
/**
|
||||
* Generate recommendations based on detected gaps
|
||||
*/
|
||||
async generateRecommendations(gaps: any[]): Promise<Recommendation[]> {
|
||||
logger.info('Recommendations Engine: Generating recommendations', {
|
||||
gapsCount: gaps.length,
|
||||
});
|
||||
|
||||
const recommendations: Recommendation[] = [];
|
||||
|
||||
// Generate recommendations based on gap types
|
||||
for (const gap of gaps) {
|
||||
switch (gap.gapType) {
|
||||
case 'multiverse_settlement_layer':
|
||||
recommendations.push({
|
||||
type: 'settlement_layer',
|
||||
title: 'Implement Multiverse Settlement Layer',
|
||||
description:
|
||||
'Add dedicated settlement layer for multiversal transactions',
|
||||
priority: 'high',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'quantum_financial_interface':
|
||||
recommendations.push({
|
||||
type: 'quantum_interface',
|
||||
title: 'Enhance Quantum Financial Interfaces',
|
||||
description:
|
||||
'Expand quantum financial system interfaces for better integration',
|
||||
priority: 'high',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'metaverse_support_tools':
|
||||
recommendations.push({
|
||||
type: 'metaverse_tools',
|
||||
title: 'Develop Metaverse Support Tools',
|
||||
description:
|
||||
'Create additional tools for metaverse economy management',
|
||||
priority: 'medium',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add general recommendations
|
||||
recommendations.push({
|
||||
type: 'system_improvement',
|
||||
title: 'Additional Settlement Layers',
|
||||
description:
|
||||
'Consider implementing additional settlement layers for specialized use cases',
|
||||
priority: 'medium',
|
||||
});
|
||||
|
||||
recommendations.push({
|
||||
type: 'synthetic_assets',
|
||||
title: 'New Forms of Synthetic Assets',
|
||||
description:
|
||||
'Explore new synthetic asset types for enhanced liquidity options',
|
||||
priority: 'low',
|
||||
});
|
||||
|
||||
recommendations.push({
|
||||
type: 'ai_supervisory',
|
||||
title: 'Parallel AI Supervisory Engines',
|
||||
description:
|
||||
'Implement parallel AI supervisory engines for enhanced monitoring',
|
||||
priority: 'medium',
|
||||
});
|
||||
|
||||
recommendations.push({
|
||||
type: 'cross_reality',
|
||||
title: 'Cross-Reality Liquidity Upgrades',
|
||||
description:
|
||||
'Enhance cross-reality liquidity mechanisms for better stability',
|
||||
priority: 'low',
|
||||
});
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
|
||||
export const recommendationsEngineService = new RecommendationsEngineService();
|
||||
|
||||
145
src/core/behavioral/beie/beie-incentive.service.ts
Normal file
145
src/core/behavioral/beie/beie-incentive.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
// BEIE Incentive Service
|
||||
// CBDC micro-rewards and SSU fee adjustments
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { beieMetricsService } from './beie-metrics.service';
|
||||
|
||||
|
||||
export interface CreateIncentiveRequest {
|
||||
entityId: string;
|
||||
entityType: string; // retail_cbdc_user, institution, sovereign
|
||||
incentiveType: string; // cbdc_micro_reward, ssu_fee_adjustment
|
||||
incentiveAmount?: string;
|
||||
incentiveReason: string; // stabilizing_behavior, low_risk_flow
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export interface IncentiveResult {
|
||||
incentiveId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class BeieIncentiveService {
|
||||
/**
|
||||
* Create behavioral incentive
|
||||
*/
|
||||
async createIncentive(request: CreateIncentiveRequest): Promise<IncentiveResult> {
|
||||
// Calculate incentive amount if not provided
|
||||
let incentiveAmount = new Decimal(0);
|
||||
if (request.incentiveAmount) {
|
||||
incentiveAmount = new Decimal(request.incentiveAmount);
|
||||
} else {
|
||||
incentiveAmount = await this.calculateIncentiveAmount(
|
||||
request.entityId,
|
||||
request.entityType,
|
||||
request.incentiveType
|
||||
);
|
||||
}
|
||||
|
||||
const incentiveId = `BEIE-INC-${uuidv4()}`;
|
||||
|
||||
const incentive = await prisma.behavioralIncentive.create({
|
||||
data: {
|
||||
incentiveId,
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
incentiveType: request.incentiveType,
|
||||
incentiveAmount,
|
||||
incentiveReason: request.incentiveReason,
|
||||
status: 'pending',
|
||||
expiresAt: request.expiresAt || null,
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-apply if conditions are met
|
||||
await this.applyIncentive(incentiveId);
|
||||
|
||||
return {
|
||||
incentiveId: incentive.incentiveId,
|
||||
status: incentive.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate incentive amount based on behavior
|
||||
*/
|
||||
private async calculateIncentiveAmount(
|
||||
entityId: string,
|
||||
entityType: string,
|
||||
incentiveType: string
|
||||
): Promise<Decimal> {
|
||||
// Get behavioral metrics
|
||||
const metrics = await beieMetricsService.getMetrics(entityId);
|
||||
|
||||
let amount = new Decimal(0);
|
||||
|
||||
if (incentiveType === 'cbdc_micro_reward') {
|
||||
// Reward for stabilizing behaviors
|
||||
const ccv = metrics.find((m) => m.metricType === 'ccv');
|
||||
if (ccv) {
|
||||
const ccvValue = parseFloat(ccv.metricValue.toString());
|
||||
// Higher CCV (spending) = higher reward
|
||||
amount = new Decimal(ccvValue * 10); // Scale factor
|
||||
}
|
||||
} else if (incentiveType === 'ssu_fee_adjustment') {
|
||||
// Lower fees for low-risk flow patterns
|
||||
const ilb = metrics.find((m) => m.metricType === 'ilb');
|
||||
if (ilb) {
|
||||
const ilbValue = parseFloat(ilb.metricValue.toString());
|
||||
// Negative ILB (dumping) = fee increase, Positive (hoarding) = fee decrease
|
||||
amount = new Decimal(ilbValue * -5); // Negative for fee reduction
|
||||
}
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply incentive
|
||||
*/
|
||||
async applyIncentive(incentiveId: string): Promise<void> {
|
||||
const incentive = await prisma.behavioralIncentive.findUnique({
|
||||
where: { incentiveId },
|
||||
});
|
||||
|
||||
if (!incentive || incentive.status !== 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
// In production, this would actually apply the incentive (transfer funds, adjust fees, etc.)
|
||||
await prisma.behavioralIncentive.update({
|
||||
where: { incentiveId },
|
||||
data: {
|
||||
status: 'applied',
|
||||
appliedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incentive
|
||||
*/
|
||||
async getIncentive(incentiveId: string) {
|
||||
return await prisma.behavioralIncentive.findUnique({
|
||||
where: { incentiveId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List incentives for entity
|
||||
*/
|
||||
async listIncentives(entityId: string, status?: string) {
|
||||
return await prisma.behavioralIncentive.findMany({
|
||||
where: {
|
||||
entityId,
|
||||
...(status ? { status } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const beieIncentiveService = new BeieIncentiveService();
|
||||
|
||||
137
src/core/behavioral/beie/beie-metrics.service.ts
Normal file
137
src/core/behavioral/beie/beie-metrics.service.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
// BEIE Metrics Service
|
||||
// CCV, ILB, SRP calculation
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface CalculateMetricRequest {
|
||||
entityId: string;
|
||||
entityType: string; // retail_cbdc_user, institution, sovereign_liquidity_actor
|
||||
metricType: string; // ccv, ilb, srp
|
||||
}
|
||||
|
||||
export interface MetricResult {
|
||||
metricId: string;
|
||||
metricValue: string;
|
||||
metricType: string;
|
||||
}
|
||||
|
||||
export class BeieMetricsService {
|
||||
/**
|
||||
* Calculate behavioral metric
|
||||
*/
|
||||
async calculateMetric(request: CalculateMetricRequest): Promise<MetricResult> {
|
||||
let metricValue: Decimal;
|
||||
|
||||
switch (request.metricType) {
|
||||
case 'ccv':
|
||||
metricValue = await this.calculateCCV(request.entityId, request.entityType);
|
||||
break;
|
||||
case 'ilb':
|
||||
metricValue = await this.calculateILB(request.entityId, request.entityType);
|
||||
break;
|
||||
case 'srp':
|
||||
metricValue = await this.calculateSRP(request.entityId, request.entityType);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown metric type: ${request.metricType}`);
|
||||
}
|
||||
|
||||
const metricId = `BEIE-METRIC-${uuidv4()}`;
|
||||
|
||||
await prisma.behavioralMetric.create({
|
||||
data: {
|
||||
metricId,
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
metricType: request.metricType,
|
||||
metricValue,
|
||||
calculatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
metricId,
|
||||
metricValue: metricValue.toString(),
|
||||
metricType: request.metricType,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Consumer CBDC Velocity (CCV)
|
||||
* Tracks spending patterns
|
||||
*/
|
||||
private async calculateCCV(entityId: string, entityType: string): Promise<Decimal> {
|
||||
// In production, this would analyze actual CBDC transaction data
|
||||
// For now, return a simulated value based on entity type
|
||||
if (entityType !== 'retail_cbdc_user') {
|
||||
return new Decimal(0);
|
||||
}
|
||||
|
||||
// Simulate CCV calculation (would use real transaction data)
|
||||
// CCV = total_spending / time_period / average_balance
|
||||
const simulatedCCV = 0.5 + Math.random() * 0.5; // 0.5 to 1.0
|
||||
return new Decimal(simulatedCCV);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Institutional Liquidity Behavior (ILB)
|
||||
* Predicts institutional hoarding or dumping
|
||||
*/
|
||||
private async calculateILB(entityId: string, entityType: string): Promise<Decimal> {
|
||||
// In production, this would analyze institutional liquidity patterns
|
||||
if (entityType !== 'institution') {
|
||||
return new Decimal(0);
|
||||
}
|
||||
|
||||
// Simulate ILB calculation
|
||||
// ILB = liquidity_velocity / liquidity_holdings_ratio
|
||||
// Positive = hoarding, Negative = dumping
|
||||
const simulatedILB = -0.3 + Math.random() * 0.6; // -0.3 to 0.3
|
||||
return new Decimal(simulatedILB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Sovereign Reaction Profile (SRP)
|
||||
* Forecasts SCB responses to shocks
|
||||
*/
|
||||
private async calculateSRP(entityId: string, entityType: string): Promise<Decimal> {
|
||||
// In production, this would use historical SCB response data
|
||||
if (entityType !== 'sovereign_liquidity_actor') {
|
||||
return new Decimal(0);
|
||||
}
|
||||
|
||||
// Simulate SRP calculation
|
||||
// SRP = reaction_speed * intervention_probability * policy_effectiveness
|
||||
const simulatedSRP = 0.3 + Math.random() * 0.4; // 0.3 to 0.7
|
||||
return new Decimal(simulatedSRP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest metric for entity
|
||||
*/
|
||||
async getLatestMetric(entityId: string, metricType: string) {
|
||||
return await prisma.behavioralMetric.findFirst({
|
||||
where: {
|
||||
entityId,
|
||||
metricType,
|
||||
},
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all metrics for entity
|
||||
*/
|
||||
async getMetrics(entityId: string) {
|
||||
return await prisma.behavioralMetric.findMany({
|
||||
where: { entityId },
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const beieMetricsService = new BeieMetricsService();
|
||||
|
||||
198
src/core/behavioral/beie/beie-penalty.service.ts
Normal file
198
src/core/behavioral/beie/beie-penalty.service.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
// BEIE Penalty Service
|
||||
// Predictive penalty contract application
|
||||
// Logic: if (SRP_risk > threshold) impose_liquidity_penalty()
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { beieMetricsService } from './beie-metrics.service';
|
||||
|
||||
|
||||
export interface CreatePenaltyRequest {
|
||||
entityId: string;
|
||||
entityType: string; // retail_cbdc_user, institution, sovereign
|
||||
penaltyType: string; // liquidity_penalty, fee_increase, access_restriction
|
||||
penaltyAmount?: string;
|
||||
penaltyReason: string; // risky_behavior_detected, srp_risk_threshold_exceeded
|
||||
threshold?: string;
|
||||
predictiveContract?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PenaltyResult {
|
||||
penaltyId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class BeiePenaltyService {
|
||||
/**
|
||||
* Create behavioral penalty
|
||||
* Auto-applied when risky behavior detected
|
||||
*/
|
||||
async createPenalty(request: CreatePenaltyRequest): Promise<PenaltyResult> {
|
||||
// Get SRP risk score
|
||||
const srpMetric = await beieMetricsService.getLatestMetric(
|
||||
request.entityId,
|
||||
'srp'
|
||||
);
|
||||
|
||||
let riskScore = new Decimal(0);
|
||||
if (srpMetric) {
|
||||
riskScore = srpMetric.metricValue;
|
||||
} else {
|
||||
// Calculate SRP if not available
|
||||
const metric = await beieMetricsService.calculateMetric({
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
metricType: 'srp',
|
||||
});
|
||||
riskScore = new Decimal(metric.metricValue);
|
||||
}
|
||||
|
||||
// Use provided threshold or default
|
||||
const threshold = request.threshold
|
||||
? new Decimal(request.threshold)
|
||||
: new Decimal(0.5);
|
||||
|
||||
// Check if SRP_risk > threshold
|
||||
if (riskScore.gt(threshold)) {
|
||||
// Calculate penalty amount if not provided
|
||||
let penaltyAmount = new Decimal(0);
|
||||
if (request.penaltyAmount) {
|
||||
penaltyAmount = new Decimal(request.penaltyAmount);
|
||||
} else {
|
||||
penaltyAmount = await this.calculatePenaltyAmount(
|
||||
request.penaltyType,
|
||||
riskScore,
|
||||
threshold
|
||||
);
|
||||
}
|
||||
|
||||
const penaltyId = `BEIE-PEN-${uuidv4()}`;
|
||||
|
||||
const penalty = await prisma.behavioralPenalty.create({
|
||||
data: {
|
||||
penaltyId,
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
penaltyType: request.penaltyType,
|
||||
penaltyAmount,
|
||||
penaltyReason: request.penaltyReason,
|
||||
riskScore,
|
||||
threshold,
|
||||
predictiveContract: request.predictiveContract || null,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-apply penalty
|
||||
await this.applyPenalty(penaltyId);
|
||||
|
||||
return {
|
||||
penaltyId: penalty.penaltyId,
|
||||
status: penalty.status,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Risk score ${riskScore.toString()} does not exceed threshold ${threshold.toString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate penalty amount
|
||||
*/
|
||||
private async calculatePenaltyAmount(
|
||||
penaltyType: string,
|
||||
riskScore: Decimal,
|
||||
threshold: Decimal
|
||||
): Promise<Decimal> {
|
||||
// Calculate excess risk
|
||||
const excessRisk = riskScore.minus(threshold);
|
||||
|
||||
// Base penalty amounts
|
||||
const basePenalties: Record<string, number> = {
|
||||
liquidity_penalty: 1000,
|
||||
fee_increase: 100,
|
||||
access_restriction: 0, // No monetary amount
|
||||
};
|
||||
|
||||
const basePenalty = basePenalties[penaltyType] || 100;
|
||||
|
||||
// Scale by excess risk
|
||||
return new Decimal(basePenalty * parseFloat(excessRisk.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply penalty
|
||||
* Logic: if (SRP_risk > threshold) impose_liquidity_penalty()
|
||||
*/
|
||||
async applyPenalty(penaltyId: string): Promise<void> {
|
||||
const penalty = await prisma.behavioralPenalty.findUnique({
|
||||
where: { penaltyId },
|
||||
});
|
||||
|
||||
if (!penalty || penalty.status !== 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
// In production, this would actually apply the penalty
|
||||
// (deduct funds, increase fees, restrict access, etc.)
|
||||
await prisma.behavioralPenalty.update({
|
||||
where: { penaltyId },
|
||||
data: {
|
||||
status: 'applied',
|
||||
appliedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and auto-apply penalties based on SRP risk
|
||||
*/
|
||||
async checkAndApplyPenalties(entityId: string, entityType: string): Promise<void> {
|
||||
const srpMetric = await beieMetricsService.getLatestMetric(entityId, 'srp');
|
||||
|
||||
if (!srpMetric) {
|
||||
return;
|
||||
}
|
||||
|
||||
const riskScore = srpMetric.metricValue;
|
||||
const threshold = new Decimal(0.5); // Default threshold
|
||||
|
||||
if (riskScore.gt(threshold)) {
|
||||
// Auto-create and apply penalty
|
||||
await this.createPenalty({
|
||||
entityId,
|
||||
entityType,
|
||||
penaltyType: 'liquidity_penalty',
|
||||
penaltyReason: 'srp_risk_threshold_exceeded',
|
||||
threshold: threshold.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get penalty
|
||||
*/
|
||||
async getPenalty(penaltyId: string) {
|
||||
return await prisma.behavioralPenalty.findUnique({
|
||||
where: { penaltyId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List penalties for entity
|
||||
*/
|
||||
async listPenalties(entityId: string, status?: string) {
|
||||
return await prisma.behavioralPenalty.findMany({
|
||||
where: {
|
||||
entityId,
|
||||
...(status ? { status } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const beiePenaltyService = new BeiePenaltyService();
|
||||
|
||||
145
src/core/behavioral/beie/beie-profile.service.ts
Normal file
145
src/core/behavioral/beie/beie-profile.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
// BEIE Profile Service
|
||||
// Behavioral profile management
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { beieMetricsService } from './beie-metrics.service';
|
||||
|
||||
|
||||
export interface CreateProfileRequest {
|
||||
entityId: string;
|
||||
entityType: string; // retail_cbdc_user, institution, sovereign
|
||||
}
|
||||
|
||||
export interface ProfileResult {
|
||||
profileId: string;
|
||||
riskLevel: string;
|
||||
}
|
||||
|
||||
export class BeieProfileService {
|
||||
/**
|
||||
* Create or update behavioral profile
|
||||
*/
|
||||
async createOrUpdateProfile(request: CreateProfileRequest): Promise<ProfileResult> {
|
||||
// Calculate all metrics
|
||||
const ccv = await beieMetricsService.calculateMetric({
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
metricType: 'ccv',
|
||||
});
|
||||
|
||||
const ilb = await beieMetricsService.calculateMetric({
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
metricType: 'ilb',
|
||||
});
|
||||
|
||||
const srp = await beieMetricsService.calculateMetric({
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
metricType: 'srp',
|
||||
});
|
||||
|
||||
// Calculate risk level
|
||||
const riskLevel = this.calculateRiskLevel(
|
||||
parseFloat(ccv.metricValue),
|
||||
parseFloat(ilb.metricValue),
|
||||
parseFloat(srp.metricValue)
|
||||
);
|
||||
|
||||
// Check if profile exists
|
||||
const existing = await prisma.behavioralProfile.findFirst({
|
||||
where: {
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update existing profile
|
||||
const updated = await prisma.behavioralProfile.update({
|
||||
where: { profileId: existing.profileId },
|
||||
data: {
|
||||
ccvScore: new Decimal(ccv.metricValue),
|
||||
ilbScore: new Decimal(ilb.metricValue),
|
||||
srpScore: new Decimal(srp.metricValue),
|
||||
riskLevel,
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
profileId: updated.profileId,
|
||||
riskLevel: updated.riskLevel,
|
||||
};
|
||||
} else {
|
||||
// Create new profile
|
||||
const profileId = `BEIE-PROF-${uuidv4()}`;
|
||||
|
||||
const profile = await prisma.behavioralProfile.create({
|
||||
data: {
|
||||
profileId,
|
||||
entityId: request.entityId,
|
||||
entityType: request.entityType,
|
||||
ccvScore: new Decimal(ccv.metricValue),
|
||||
ilbScore: new Decimal(ilb.metricValue),
|
||||
srpScore: new Decimal(srp.metricValue),
|
||||
riskLevel,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
profileId: profile.profileId,
|
||||
riskLevel: profile.riskLevel,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk level from metrics
|
||||
*/
|
||||
private calculateRiskLevel(ccv: number, ilb: number, srp: number): string {
|
||||
// Weighted risk calculation
|
||||
// Higher SRP = higher risk
|
||||
// Negative ILB (dumping) = higher risk
|
||||
// Lower CCV = higher risk (not spending)
|
||||
|
||||
const riskScore = srp * 0.5 + Math.abs(ilb) * 0.3 + (1 - ccv) * 0.2;
|
||||
|
||||
if (riskScore >= 0.7) {
|
||||
return 'critical';
|
||||
} else if (riskScore >= 0.5) {
|
||||
return 'high';
|
||||
} else if (riskScore >= 0.3) {
|
||||
return 'medium';
|
||||
} else {
|
||||
return 'low';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get profile
|
||||
*/
|
||||
async getProfile(entityId: string, entityType: string) {
|
||||
return await prisma.behavioralProfile.findFirst({
|
||||
where: {
|
||||
entityId,
|
||||
entityType,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List profiles by risk level
|
||||
*/
|
||||
async listProfiles(riskLevel?: string) {
|
||||
return await prisma.behavioralProfile.findMany({
|
||||
where: riskLevel ? { riskLevel } : undefined,
|
||||
orderBy: { lastUpdated: 'desc' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const beieProfileService = new BeieProfileService();
|
||||
|
||||
200
src/core/behavioral/beie/beie.routes.ts
Normal file
200
src/core/behavioral/beie/beie.routes.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
// BEIE API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { beieMetricsService } from './beie-metrics.service';
|
||||
import { beieIncentiveService } from './beie-incentive.service';
|
||||
import { beiePenaltyService } from './beie-penalty.service';
|
||||
import { beieProfileService } from './beie-profile.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/metric:
|
||||
* post:
|
||||
* summary: Calculate behavioral metric
|
||||
*/
|
||||
router.post('/metric', async (req, res, next) => {
|
||||
try {
|
||||
const result = await beieMetricsService.calculateMetric(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/metrics/:entityId:
|
||||
* get:
|
||||
* summary: Get all metrics for entity
|
||||
*/
|
||||
router.get('/metrics/:entityId', async (req, res, next) => {
|
||||
try {
|
||||
const metrics = await beieMetricsService.getMetrics(req.params.entityId);
|
||||
res.json(metrics);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/incentive:
|
||||
* post:
|
||||
* summary: Create behavioral incentive
|
||||
*/
|
||||
router.post('/incentive', async (req, res, next) => {
|
||||
try {
|
||||
const result = await beieIncentiveService.createIncentive(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/incentive/:incentiveId:
|
||||
* get:
|
||||
* summary: Get incentive
|
||||
*/
|
||||
router.get('/incentive/:incentiveId', async (req, res, next) => {
|
||||
try {
|
||||
const incentive = await beieIncentiveService.getIncentive(req.params.incentiveId);
|
||||
if (!incentive) {
|
||||
return res.status(404).json({ error: 'Incentive not found' });
|
||||
}
|
||||
res.json(incentive);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/incentives/:entityId:
|
||||
* get:
|
||||
* summary: List incentives for entity
|
||||
*/
|
||||
router.get('/incentives/:entityId', async (req, res, next) => {
|
||||
try {
|
||||
const { status } = req.query;
|
||||
const incentives = await beieIncentiveService.listIncentives(
|
||||
req.params.entityId,
|
||||
status as string | undefined
|
||||
);
|
||||
res.json(incentives);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/penalty:
|
||||
* post:
|
||||
* summary: Create behavioral penalty
|
||||
*/
|
||||
router.post('/penalty', async (req, res, next) => {
|
||||
try {
|
||||
const result = await beiePenaltyService.createPenalty(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/penalty/check/:entityId:
|
||||
* post:
|
||||
* summary: Check and auto-apply penalties
|
||||
*/
|
||||
router.post('/penalty/check/:entityId', async (req, res, next) => {
|
||||
try {
|
||||
const { entityType } = req.body;
|
||||
await beiePenaltyService.checkAndApplyPenalties(req.params.entityId, entityType);
|
||||
res.json({ status: 'checked' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/penalties/:entityId:
|
||||
* get:
|
||||
* summary: List penalties for entity
|
||||
*/
|
||||
router.get('/penalties/:entityId', async (req, res, next) => {
|
||||
try {
|
||||
const { status } = req.query;
|
||||
const penalties = await beiePenaltyService.listPenalties(
|
||||
req.params.entityId,
|
||||
status as string | undefined
|
||||
);
|
||||
res.json(penalties);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/profile:
|
||||
* post:
|
||||
* summary: Create or update behavioral profile
|
||||
*/
|
||||
router.post('/profile', async (req, res, next) => {
|
||||
try {
|
||||
const result = await beieProfileService.createOrUpdateProfile(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/profile/:entityId:
|
||||
* get:
|
||||
* summary: Get behavioral profile
|
||||
*/
|
||||
router.get('/profile/:entityId', async (req, res, next) => {
|
||||
try {
|
||||
const { entityType } = req.query;
|
||||
if (!entityType) {
|
||||
return res.status(400).json({ error: 'entityType is required' });
|
||||
}
|
||||
const profile = await beieProfileService.getProfile(
|
||||
req.params.entityId,
|
||||
entityType as string
|
||||
);
|
||||
if (!profile) {
|
||||
return res.status(404).json({ error: 'Profile not found' });
|
||||
}
|
||||
res.json(profile);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/beie/profiles:
|
||||
* get:
|
||||
* summary: List profiles by risk level
|
||||
*/
|
||||
router.get('/profiles', async (req, res, next) => {
|
||||
try {
|
||||
const { riskLevel } = req.query;
|
||||
const profiles = await beieProfileService.listProfiles(riskLevel as string | undefined);
|
||||
res.json(profiles);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
130
src/core/cbdc/cbdc-transaction.service.ts
Normal file
130
src/core/cbdc/cbdc-transaction.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
// CBDC Transaction Modes - Online, Offline, Dual-Mode
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { CbdcOfflineCapsule } from '@/shared/types';
|
||||
import { encryptionService } from '@/infrastructure/encryption/encryption.service';
|
||||
import { DEFAULT_CONFIG } from '@/shared/constants';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export class CbdcTransactionService {
|
||||
/**
|
||||
* Create offline transaction capsule
|
||||
*/
|
||||
async createOfflineCapsule(
|
||||
senderWalletId: string,
|
||||
receiverWalletId: string,
|
||||
amount: string
|
||||
): Promise<CbdcOfflineCapsule> {
|
||||
const capsuleId = `CAPSULE-${uuidv4()}`;
|
||||
const doubleSpendToken = encryptionService.generateNonce();
|
||||
const timestamp = new Date();
|
||||
|
||||
// Create signature payload
|
||||
const payload = JSON.stringify({
|
||||
capsuleId,
|
||||
senderWalletId,
|
||||
receiverWalletId,
|
||||
amount,
|
||||
timestamp: timestamp.toISOString(),
|
||||
doubleSpendToken,
|
||||
});
|
||||
|
||||
const signature = encryptionService.hash(payload);
|
||||
|
||||
const capsule = await prisma.cbdcOfflineCapsule.create({
|
||||
data: {
|
||||
capsuleId,
|
||||
senderWalletId,
|
||||
receiverWalletId,
|
||||
amount: new Decimal(amount),
|
||||
timestamp,
|
||||
expiryWindow: DEFAULT_CONFIG.OFFLINE_CAPSULE_EXPIRY_SECONDS,
|
||||
doubleSpendToken,
|
||||
signature,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
capsuleId: capsule.capsuleId,
|
||||
senderWalletId: capsule.senderWalletId,
|
||||
receiverWalletId: capsule.receiverWalletId,
|
||||
amount: capsule.amount.toString(),
|
||||
timestamp: capsule.timestamp,
|
||||
expiryWindow: capsule.expiryWindow,
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
signature: capsule.signature,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sync offline capsule
|
||||
*/
|
||||
async validateAndSyncCapsule(capsuleId: string): Promise<boolean> {
|
||||
const capsule = await prisma.cbdcOfflineCapsule.findUnique({
|
||||
where: { capsuleId },
|
||||
});
|
||||
|
||||
if (!capsule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiry
|
||||
const now = new Date();
|
||||
const expiryTime = new Date(capsule.timestamp.getTime() + capsule.expiryWindow * 1000);
|
||||
if (now > expiryTime) {
|
||||
await prisma.cbdcOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: { status: 'rejected' },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check double-spend
|
||||
const existingCapsule = await prisma.cbdcOfflineCapsule.findFirst({
|
||||
where: {
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
status: { in: ['validated', 'synced'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCapsule && existingCapsule.capsuleId !== capsuleId) {
|
||||
await prisma.cbdcOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: { status: 'rejected' },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate signature
|
||||
const payload = JSON.stringify({
|
||||
capsuleId: capsule.capsuleId,
|
||||
senderWalletId: capsule.senderWalletId,
|
||||
receiverWalletId: capsule.receiverWalletId,
|
||||
amount: capsule.amount.toString(),
|
||||
timestamp: capsule.timestamp.toISOString(),
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
});
|
||||
|
||||
const computedSignature = encryptionService.hash(payload);
|
||||
if (computedSignature !== capsule.signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as validated and synced
|
||||
await prisma.cbdcOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
status: 'synced',
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcTransactionService = new CbdcTransactionService();
|
||||
|
||||
75
src/core/cbdc/cbdc-wallet.service.ts
Normal file
75
src/core/cbdc/cbdc-wallet.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// CBDC Wallet System
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { CbdcWallet, CbdcWalletType } from '@/shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export class CbdcWalletService {
|
||||
/**
|
||||
* Create CBDC wallet
|
||||
*/
|
||||
async createWallet(
|
||||
sovereignBankId: string,
|
||||
walletType: CbdcWalletType,
|
||||
currencyCode: string
|
||||
): Promise<CbdcWallet> {
|
||||
const walletId = `WALLET-${uuidv4()}`;
|
||||
|
||||
const wallet = await prisma.cbdcWallet.create({
|
||||
data: {
|
||||
walletId,
|
||||
sovereignBankId,
|
||||
walletType,
|
||||
currencyCode,
|
||||
balance: new Decimal(0),
|
||||
status: 'active',
|
||||
tieredAccess: this.getDefaultTieredAccess(walletType),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: wallet.id,
|
||||
walletId: wallet.walletId,
|
||||
sovereignBankId: wallet.sovereignBankId,
|
||||
walletType: wallet.walletType as CbdcWalletType,
|
||||
currencyCode: wallet.currencyCode,
|
||||
balance: wallet.balance.toString(),
|
||||
status: wallet.status,
|
||||
tieredAccess: wallet.tieredAccess as Record<string, unknown> | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default tiered access configuration
|
||||
*/
|
||||
private getDefaultTieredAccess(walletType: CbdcWalletType): Record<string, unknown> {
|
||||
switch (walletType) {
|
||||
case CbdcWalletType.RETAIL:
|
||||
return {
|
||||
maxTransactionAmount: '10000',
|
||||
dailyLimit: '50000',
|
||||
requiresKYC: true,
|
||||
};
|
||||
case CbdcWalletType.WHOLESALE:
|
||||
return {
|
||||
maxTransactionAmount: '100000000',
|
||||
dailyLimit: '1000000000',
|
||||
requiresKYC: true,
|
||||
};
|
||||
case CbdcWalletType.INSTITUTIONAL:
|
||||
return {
|
||||
maxTransactionAmount: '1000000000',
|
||||
dailyLimit: '10000000000',
|
||||
requiresKYC: true,
|
||||
smartContractEnabled: true,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcWalletService = new CbdcWalletService();
|
||||
|
||||
173
src/core/cbdc/cbdc.service.ts
Normal file
173
src/core/cbdc/cbdc.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
// CBDC Engine - Minting, Burning, Distribution
|
||||
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import {
|
||||
CbdcOperationType,
|
||||
CbdcIssuance,
|
||||
AssetType,
|
||||
} from '@/shared/types';
|
||||
import { ledgerService } from '@/core/ledger/ledger.service';
|
||||
import { accountService } from '@/core/accounts/account.service';
|
||||
import { LedgerEntryType } from '@/shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { DbisError, ErrorCode } from '@/shared/types';
|
||||
import prisma from '@/shared/database/prisma';
|
||||
|
||||
export class CbdcService {
|
||||
/**
|
||||
* Mint CBDC
|
||||
*/
|
||||
async mintCbdc(
|
||||
sovereignBankId: string,
|
||||
walletId: string | null,
|
||||
amount: string,
|
||||
operatorIdentity: string,
|
||||
reason?: string
|
||||
): Promise<CbdcIssuance> {
|
||||
// Verify 1:1 reserve backing
|
||||
await this.verifyReserveBacking(sovereignBankId, amount);
|
||||
|
||||
const recordId = `CBDC-MINT-${uuidv4()}`;
|
||||
|
||||
// Get treasury account for the sovereign bank
|
||||
const treasuryAccount = await this.getTreasuryAccount(sovereignBankId, 'OMDC');
|
||||
|
||||
// Create CBDC issuance record
|
||||
const issuance = await prisma.cbdcIssuance.create({
|
||||
data: {
|
||||
recordId,
|
||||
sovereignBankId,
|
||||
walletId,
|
||||
amountMinted: new Decimal(amount),
|
||||
amountBurned: new Decimal(0),
|
||||
netChange: new Decimal(amount),
|
||||
operationType: CbdcOperationType.MINT,
|
||||
operatorIdentity,
|
||||
reserveBacking: new Decimal(amount), // 1:1 backing
|
||||
timestampUtc: new Date(),
|
||||
metadata: reason ? { reason } : null,
|
||||
},
|
||||
});
|
||||
|
||||
// Create ledger entry
|
||||
const ledgerId = `${sovereignBankId}-CBDC`;
|
||||
await ledgerService.postDoubleEntry(
|
||||
ledgerId,
|
||||
treasuryAccount.id, // Debit: Treasury account
|
||||
treasuryAccount.id, // Credit: CBDC wallet (simplified)
|
||||
amount,
|
||||
'OMDC',
|
||||
AssetType.CBDC,
|
||||
LedgerEntryType.TYPE_B, // CBDC issuance
|
||||
recordId,
|
||||
undefined,
|
||||
{ operationType: 'mint', walletId, reason }
|
||||
);
|
||||
|
||||
return this.mapToCbdcIssuance(issuance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Burn CBDC
|
||||
*/
|
||||
async burnCbdc(
|
||||
sovereignBankId: string,
|
||||
walletId: string | null,
|
||||
amount: string,
|
||||
operatorIdentity: string,
|
||||
reason?: string
|
||||
): Promise<CbdcIssuance> {
|
||||
const recordId = `CBDC-BURN-${uuidv4()}`;
|
||||
|
||||
// Get treasury account
|
||||
const treasuryAccount = await this.getTreasuryAccount(sovereignBankId, 'OMDC');
|
||||
|
||||
// Create CBDC issuance record
|
||||
const issuance = await prisma.cbdcIssuance.create({
|
||||
data: {
|
||||
recordId,
|
||||
sovereignBankId,
|
||||
walletId,
|
||||
amountMinted: new Decimal(0),
|
||||
amountBurned: new Decimal(amount),
|
||||
netChange: new Decimal(amount).neg(),
|
||||
operationType: CbdcOperationType.BURN,
|
||||
operatorIdentity,
|
||||
timestampUtc: new Date(),
|
||||
metadata: reason ? { reason } : null,
|
||||
},
|
||||
});
|
||||
|
||||
// Create ledger entry
|
||||
const ledgerId = `${sovereignBankId}-CBDC`;
|
||||
await ledgerService.postDoubleEntry(
|
||||
ledgerId,
|
||||
treasuryAccount.id, // Debit: CBDC wallet
|
||||
treasuryAccount.id, // Credit: Treasury account (simplified)
|
||||
amount,
|
||||
'OMDC',
|
||||
AssetType.CBDC,
|
||||
LedgerEntryType.TYPE_B, // CBDC redemption
|
||||
recordId,
|
||||
undefined,
|
||||
{ operationType: 'burn', walletId, reason }
|
||||
);
|
||||
|
||||
return this.mapToCbdcIssuance(issuance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify 1:1 reserve backing
|
||||
*/
|
||||
private async verifyReserveBacking(sovereignBankId: string, amount: string): Promise<void> {
|
||||
// Get reserve account
|
||||
const reserveAccount = await this.getTreasuryAccount(sovereignBankId, 'OMF');
|
||||
const reserveBalance = parseFloat(reserveAccount.balance);
|
||||
const mintAmount = parseFloat(amount);
|
||||
|
||||
if (reserveBalance < mintAmount) {
|
||||
throw new DbisError(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
'Insufficient reserve backing for CBDC minting'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get treasury account for a sovereign bank
|
||||
*/
|
||||
private async getTreasuryAccount(sovereignBankId: string, currencyCode: string) {
|
||||
const accounts = await accountService.getAccountsBySovereign(sovereignBankId);
|
||||
const treasuryAccount = accounts.find(
|
||||
(acc) => acc.accountType === 'treasury' && acc.currencyCode === currencyCode
|
||||
);
|
||||
|
||||
if (!treasuryAccount) {
|
||||
throw new DbisError(ErrorCode.NOT_FOUND, 'Treasury account not found');
|
||||
}
|
||||
|
||||
return treasuryAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Prisma model to CbdcIssuance type
|
||||
*/
|
||||
private mapToCbdcIssuance(issuance: any): CbdcIssuance {
|
||||
return {
|
||||
id: issuance.id,
|
||||
recordId: issuance.recordId,
|
||||
sovereignBankId: issuance.sovereignBankId,
|
||||
walletId: issuance.walletId || undefined,
|
||||
amountMinted: issuance.amountMinted.toString(),
|
||||
amountBurned: issuance.amountBurned.toString(),
|
||||
netChange: issuance.netChange.toString(),
|
||||
operationType: issuance.operationType as CbdcOperationType,
|
||||
operatorIdentity: issuance.operatorIdentity,
|
||||
reserveBacking: issuance.reserveBacking?.toString(),
|
||||
timestampUtc: issuance.timestampUtc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcService = new CbdcService();
|
||||
|
||||
101
src/core/cbdc/face/face-behavioral.service.ts
Normal file
101
src/core/cbdc/face/face-behavioral.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// FACE Behavioral Engine Service
|
||||
// AI behavioral engine (integrates with Volume V SARE)
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface CreateBehavioralEngineRequest {
|
||||
economyId: string;
|
||||
engineConfig: Record<string, unknown>;
|
||||
behaviorModel: string;
|
||||
}
|
||||
|
||||
export class FaceBehavioralService {
|
||||
/**
|
||||
* Create or update behavioral engine
|
||||
*/
|
||||
async createBehavioralEngine(request: CreateBehavioralEngineRequest) {
|
||||
// Check if engine already exists
|
||||
const existing = await prisma.faceBehavioralEngine.findUnique({
|
||||
where: { economyId: request.economyId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return prisma.faceBehavioralEngine.update({
|
||||
where: { engineId: existing.engineId },
|
||||
data: {
|
||||
engineConfig: request.engineConfig,
|
||||
behaviorModel: request.behaviorModel,
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const engineId = `FACE-BE-${uuidv4()}`;
|
||||
|
||||
const engine = await prisma.faceBehavioralEngine.create({
|
||||
data: {
|
||||
engineId,
|
||||
economyId: request.economyId,
|
||||
engineConfig: request.engineConfig,
|
||||
behaviorModel: request.behaviorModel,
|
||||
status: 'active',
|
||||
lastUpdated: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get behavioral engine for economy
|
||||
*/
|
||||
async getBehavioralEngine(economyId: string) {
|
||||
const engine = await prisma.faceBehavioralEngine.findUnique({
|
||||
where: { economyId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!engine) {
|
||||
throw new Error(`Behavioral engine not found for economy: ${economyId}`);
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze behavior (integrates with SARE from Volume V)
|
||||
*/
|
||||
async analyzeBehavior(economyId: string, behaviorData: Record<string, unknown>) {
|
||||
const engine = await this.getBehavioralEngine(economyId);
|
||||
|
||||
// In production, this would:
|
||||
// 1. Feed behavior data to AI model
|
||||
// 2. Integrate with SARE (Sovereign AI Risk Engine) from Volume V
|
||||
// 3. Generate behavioral insights and predictions
|
||||
|
||||
// Mock analysis
|
||||
const analysis = {
|
||||
velocity: behaviorData.velocity as number || 0,
|
||||
circulation: behaviorData.circulation as number || 0,
|
||||
riskLevel: 'medium',
|
||||
recommendations: [] as string[],
|
||||
};
|
||||
|
||||
if (analysis.velocity < 0.5) {
|
||||
analysis.recommendations.push('Low velocity detected - consider supply adjustment');
|
||||
}
|
||||
|
||||
if (analysis.velocity > 2.0) {
|
||||
analysis.recommendations.push('High velocity detected - consider stabilization measures');
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
}
|
||||
|
||||
export const faceBehavioralService = new FaceBehavioralService();
|
||||
|
||||
107
src/core/cbdc/face/face-economy.service.ts
Normal file
107
src/core/cbdc/face/face-economy.service.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// FACE Economy Service
|
||||
// Economy lifecycle management
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface CreateFaceEconomyRequest {
|
||||
sovereignBankId: string;
|
||||
economyName: string;
|
||||
description: string;
|
||||
economyType: 'retail' | 'wholesale' | 'hybrid';
|
||||
}
|
||||
|
||||
export class FaceEconomyService {
|
||||
/**
|
||||
* Create FACE economy
|
||||
*/
|
||||
async createEconomy(request: CreateFaceEconomyRequest) {
|
||||
const economyId = `FACE-${uuidv4()}`;
|
||||
|
||||
const economy = await prisma.faceEconomy.create({
|
||||
data: {
|
||||
economyId,
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
economyName: request.economyName,
|
||||
description: request.description,
|
||||
economyType: request.economyType,
|
||||
status: 'active',
|
||||
activatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return economy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get economy by ID
|
||||
*/
|
||||
async getEconomy(economyId: string) {
|
||||
const economy = await prisma.faceEconomy.findUnique({
|
||||
where: { economyId },
|
||||
include: {
|
||||
sovereignBank: true,
|
||||
behavioralEngine: true,
|
||||
supplyContracts: true,
|
||||
stabilizationContracts: true,
|
||||
incentives: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!economy) {
|
||||
throw new Error(`Economy not found: ${economyId}`);
|
||||
}
|
||||
|
||||
return economy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get economies for sovereign bank
|
||||
*/
|
||||
async getEconomiesForBank(sovereignBankId: string) {
|
||||
return prisma.faceEconomy.findMany({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
status: 'active',
|
||||
},
|
||||
include: {
|
||||
behavioralEngine: true,
|
||||
_count: {
|
||||
select: {
|
||||
supplyContracts: true,
|
||||
stabilizationContracts: true,
|
||||
incentives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend economy
|
||||
*/
|
||||
async suspendEconomy(economyId: string) {
|
||||
return prisma.faceEconomy.update({
|
||||
where: { economyId },
|
||||
data: {
|
||||
status: 'suspended',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive economy
|
||||
*/
|
||||
async archiveEconomy(economyId: string) {
|
||||
return prisma.faceEconomy.update({
|
||||
where: { economyId },
|
||||
data: {
|
||||
status: 'archived',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const faceEconomyService = new FaceEconomyService();
|
||||
|
||||
147
src/core/cbdc/face/face-incentive.service.ts
Normal file
147
src/core/cbdc/face/face-incentive.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// FACE Incentive Service
|
||||
// Reward/penalty system
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface CreateIncentiveRequest {
|
||||
economyId: string;
|
||||
incentiveType: 'reward' | 'penalty' | 'predictive_nudge';
|
||||
targetBehavior: string;
|
||||
incentiveAmount: number;
|
||||
conditions: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class FaceIncentiveService {
|
||||
/**
|
||||
* Create incentive
|
||||
*/
|
||||
async createIncentive(request: CreateIncentiveRequest) {
|
||||
const incentiveId = `FACE-INC-${uuidv4()}`;
|
||||
|
||||
const incentive = await prisma.faceIncentive.create({
|
||||
data: {
|
||||
incentiveId,
|
||||
economyId: request.economyId,
|
||||
incentiveType: request.incentiveType,
|
||||
targetBehavior: request.targetBehavior,
|
||||
incentiveAmount: new Decimal(request.incentiveAmount),
|
||||
conditions: request.conditions,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return incentive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and apply incentive
|
||||
*/
|
||||
async checkAndApplyIncentive(
|
||||
incentiveId: string,
|
||||
behaviorData: Record<string, unknown>
|
||||
) {
|
||||
const incentive = await prisma.faceIncentive.findUnique({
|
||||
where: { incentiveId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incentive) {
|
||||
throw new Error(`Incentive not found: ${incentiveId}`);
|
||||
}
|
||||
|
||||
if (incentive.status !== 'active') {
|
||||
return { applied: false, reason: 'Incentive not active' };
|
||||
}
|
||||
|
||||
// Check conditions
|
||||
const conditionsMet = this.checkConditions(incentive.conditions, behaviorData);
|
||||
|
||||
if (!conditionsMet) {
|
||||
return { applied: false, reason: 'Conditions not met' };
|
||||
}
|
||||
|
||||
// Apply incentive
|
||||
// In production, this would:
|
||||
// - For rewards: credit CBDC wallet, apply fee reduction, etc.
|
||||
// - For penalties: charge fee, restrict access, etc.
|
||||
// - For predictive nudges: send notification, adjust rates, etc.
|
||||
|
||||
await prisma.faceIncentive.update({
|
||||
where: { incentiveId },
|
||||
data: {
|
||||
status: 'applied',
|
||||
appliedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
applied: true,
|
||||
incentiveType: incentive.incentiveType,
|
||||
amount: incentive.incentiveAmount.toString(),
|
||||
targetBehavior: incentive.targetBehavior,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if conditions are met
|
||||
*/
|
||||
private checkConditions(
|
||||
conditions: Record<string, unknown>,
|
||||
behaviorData: Record<string, unknown>
|
||||
): boolean {
|
||||
// Simple condition checking
|
||||
// In production, would use more sophisticated rule engine
|
||||
|
||||
for (const [key, value] of Object.entries(conditions)) {
|
||||
if (key === 'min_velocity' && (behaviorData.velocity as number) < (value as number)) {
|
||||
return false;
|
||||
}
|
||||
if (key === 'max_velocity' && (behaviorData.velocity as number) > (value as number)) {
|
||||
return false;
|
||||
}
|
||||
if (key === 'min_circulation' && (behaviorData.circulation as number) < (value as number)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incentives for economy
|
||||
*/
|
||||
async getIncentivesForEconomy(economyId: string) {
|
||||
return prisma.faceIncentive.findMany({
|
||||
where: { economyId },
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incentive by ID
|
||||
*/
|
||||
async getIncentive(incentiveId: string) {
|
||||
const incentive = await prisma.faceIncentive.findUnique({
|
||||
where: { incentiveId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incentive) {
|
||||
throw new Error(`Incentive not found: ${incentiveId}`);
|
||||
}
|
||||
|
||||
return incentive;
|
||||
}
|
||||
}
|
||||
|
||||
export const faceIncentiveService = new FaceIncentiveService();
|
||||
|
||||
136
src/core/cbdc/face/face-stabilization.service.ts
Normal file
136
src/core/cbdc/face/face-stabilization.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// FACE Stabilization Contract Service
|
||||
// Auto-stabilization: if SRI_risk > threshold: impose_rate_adjustment()
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sriCalculatorService } from '@/core/risk/sri/sri-calculator.service';
|
||||
|
||||
|
||||
export interface CreateStabilizationContractRequest {
|
||||
economyId: string;
|
||||
sriThreshold: number;
|
||||
rateAdjustmentRule: Record<string, unknown>;
|
||||
adjustmentType: 'interest_rate' | 'liquidity_rate' | 'fee_adjustment';
|
||||
}
|
||||
|
||||
export class FaceStabilizationService {
|
||||
/**
|
||||
* Create stabilization contract
|
||||
*/
|
||||
async createStabilizationContract(request: CreateStabilizationContractRequest) {
|
||||
const contractId = `FACE-STAB-${uuidv4()}`;
|
||||
|
||||
const contract = await prisma.faceStabilizationContract.create({
|
||||
data: {
|
||||
contractId,
|
||||
economyId: request.economyId,
|
||||
contractType: 'auto_stabilization',
|
||||
sriThreshold: new Decimal(request.sriThreshold),
|
||||
rateAdjustmentRule: request.rateAdjustmentRule || {
|
||||
rule: 'if SRI_risk > threshold: impose_rate_adjustment()',
|
||||
},
|
||||
adjustmentType: request.adjustmentType,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and execute stabilization contract
|
||||
*/
|
||||
async checkStabilizationContract(contractId: string) {
|
||||
const contract = await prisma.faceStabilizationContract.findUnique({
|
||||
where: { contractId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(`Stabilization contract not found: ${contractId}`);
|
||||
}
|
||||
|
||||
if (contract.status !== 'active') {
|
||||
return { triggered: false, reason: 'Contract not active' };
|
||||
}
|
||||
|
||||
// Get current SRI for the sovereign bank
|
||||
const sriResult = await sriCalculatorService.calculateSRI(contract.economy.sovereignBankId);
|
||||
const currentSRI = sriResult.sriScore;
|
||||
const threshold = parseFloat(contract.sriThreshold.toString());
|
||||
|
||||
// if SRI_risk > threshold: impose_rate_adjustment()
|
||||
if (currentSRI > threshold) {
|
||||
// Calculate adjustment based on SRI excess
|
||||
const excess = currentSRI - threshold;
|
||||
const adjustmentPercentage = Math.min(excess * 0.1, 5); // Max 5% adjustment
|
||||
|
||||
// In production, this would actually adjust rates in the system
|
||||
// For now, just record the adjustment
|
||||
const adjustment = {
|
||||
type: contract.adjustmentType,
|
||||
percentage: adjustmentPercentage,
|
||||
sriExcess: excess,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update contract
|
||||
await prisma.faceStabilizationContract.update({
|
||||
where: { contractId },
|
||||
data: {
|
||||
lastTriggeredAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
triggered: true,
|
||||
currentSRI,
|
||||
threshold,
|
||||
adjustment,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
triggered: false,
|
||||
reason: 'SRI within acceptable threshold',
|
||||
currentSRI,
|
||||
threshold,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stabilization contracts for economy
|
||||
*/
|
||||
async getContractsForEconomy(economyId: string) {
|
||||
return prisma.faceStabilizationContract.findMany({
|
||||
where: { economyId },
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contract by ID
|
||||
*/
|
||||
async getContract(contractId: string) {
|
||||
const contract = await prisma.faceStabilizationContract.findUnique({
|
||||
where: { contractId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(`Stabilization contract not found: ${contractId}`);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
|
||||
export const faceStabilizationService = new FaceStabilizationService();
|
||||
|
||||
164
src/core/cbdc/face/face-supply.service.ts
Normal file
164
src/core/cbdc/face/face-supply.service.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
// FACE Supply Contract Service
|
||||
// Automatic supply contracts: if velocity < target: mint_cbdc() elif velocity > danger_threshold: burn_cbdc()
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { cbdcService } from '@/core/cbdc/cbdc.service';
|
||||
|
||||
|
||||
export interface CreateSupplyContractRequest {
|
||||
economyId: string;
|
||||
velocityTarget: number;
|
||||
velocityDangerThreshold: number;
|
||||
mintCondition?: Record<string, unknown>;
|
||||
burnCondition?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class FaceSupplyService {
|
||||
/**
|
||||
* Create supply contract
|
||||
*/
|
||||
async createSupplyContract(request: CreateSupplyContractRequest) {
|
||||
const contractId = `FACE-SUPPLY-${uuidv4()}`;
|
||||
|
||||
const contract = await prisma.faceSupplyContract.create({
|
||||
data: {
|
||||
contractId,
|
||||
economyId: request.economyId,
|
||||
contractType: 'automatic_supply_adjustment',
|
||||
velocityTarget: new Decimal(request.velocityTarget),
|
||||
velocityDangerThreshold: new Decimal(request.velocityDangerThreshold),
|
||||
mintCondition: request.mintCondition || {
|
||||
condition: 'if velocity < target: mint_cbdc()',
|
||||
},
|
||||
burnCondition: request.burnCondition || {
|
||||
condition: 'elif velocity > danger_threshold: burn_cbdc()',
|
||||
},
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and execute supply contract
|
||||
*/
|
||||
async checkSupplyContract(contractId: string, currentVelocity: number) {
|
||||
const contract = await prisma.faceSupplyContract.findUnique({
|
||||
where: { contractId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(`Supply contract not found: ${contractId}`);
|
||||
}
|
||||
|
||||
if (contract.status !== 'active') {
|
||||
return { triggered: false, reason: 'Contract not active' };
|
||||
}
|
||||
|
||||
const velocity = new Decimal(currentVelocity);
|
||||
const target = contract.velocityTarget;
|
||||
const dangerThreshold = contract.velocityDangerThreshold;
|
||||
|
||||
let action: 'mint' | 'burn' | null = null;
|
||||
let amount: Decimal | null = null;
|
||||
|
||||
// if velocity < target: mint_cbdc()
|
||||
if (velocity.lt(target)) {
|
||||
action = 'mint';
|
||||
// Calculate mint amount based on velocity gap
|
||||
const gap = target.minus(velocity);
|
||||
amount = gap.times(1000000); // Simplified: 1M per 0.1 velocity gap
|
||||
}
|
||||
// elif velocity > danger_threshold: burn_cbdc()
|
||||
else if (velocity.gt(dangerThreshold)) {
|
||||
action = 'burn';
|
||||
// Calculate burn amount based on excess velocity
|
||||
const excess = velocity.minus(dangerThreshold);
|
||||
amount = excess.times(1000000); // Simplified: 1M per 0.1 velocity excess
|
||||
}
|
||||
|
||||
if (action && amount) {
|
||||
// Execute action
|
||||
const economy = contract.economy;
|
||||
const operatorIdentity = `FACE-AUTO-${contractId}`;
|
||||
|
||||
if (action === 'mint') {
|
||||
await cbdcService.mintCbdc(
|
||||
economy.sovereignBankId,
|
||||
null, // No specific wallet
|
||||
amount.toString(),
|
||||
operatorIdentity,
|
||||
`FACE automatic supply adjustment: velocity ${currentVelocity} < target ${target}`
|
||||
);
|
||||
} else if (action === 'burn') {
|
||||
await cbdcService.burnCbdc(
|
||||
economy.sovereignBankId,
|
||||
null, // No specific wallet
|
||||
amount.toString(),
|
||||
operatorIdentity,
|
||||
`FACE automatic supply adjustment: velocity ${currentVelocity} > danger threshold ${dangerThreshold}`
|
||||
);
|
||||
}
|
||||
|
||||
// Update contract
|
||||
await prisma.faceSupplyContract.update({
|
||||
where: { contractId },
|
||||
data: {
|
||||
lastTriggeredAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
triggered: true,
|
||||
action,
|
||||
amount: amount.toString(),
|
||||
velocity: currentVelocity,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
triggered: false,
|
||||
reason: 'Velocity within acceptable range',
|
||||
velocity: currentVelocity,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supply contracts for economy
|
||||
*/
|
||||
async getContractsForEconomy(economyId: string) {
|
||||
return prisma.faceSupplyContract.findMany({
|
||||
where: { economyId },
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contract by ID
|
||||
*/
|
||||
async getContract(contractId: string) {
|
||||
const contract = await prisma.faceSupplyContract.findUnique({
|
||||
where: { contractId },
|
||||
include: {
|
||||
economy: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(`Supply contract not found: ${contractId}`);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
|
||||
export const faceSupplyService = new FaceSupplyService();
|
||||
|
||||
153
src/core/cbdc/face/face.routes.ts
Normal file
153
src/core/cbdc/face/face.routes.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
// FACE API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { faceEconomyService } from './face-economy.service';
|
||||
import { faceBehavioralService } from './face-behavioral.service';
|
||||
import { faceSupplyService } from './face-supply.service';
|
||||
import { faceStabilizationService } from './face-stabilization.service';
|
||||
import { faceIncentiveService } from './face-incentive.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Economy routes
|
||||
router.post('/economies', async (req, res, next) => {
|
||||
try {
|
||||
const economy = await faceEconomyService.createEconomy(req.body);
|
||||
res.json(economy);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/economies', async (req, res, next) => {
|
||||
try {
|
||||
const economies = await faceEconomyService.getEconomiesForBank(req.query.sovereignBankId as string);
|
||||
res.json(economies);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/economies/:economyId', async (req, res, next) => {
|
||||
try {
|
||||
const economy = await faceEconomyService.getEconomy(req.params.economyId);
|
||||
res.json(economy);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Behavioral engine routes
|
||||
router.post('/behavioral', async (req, res, next) => {
|
||||
try {
|
||||
const engine = await faceBehavioralService.createBehavioralEngine(req.body);
|
||||
res.json(engine);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/behavioral/:economyId', async (req, res, next) => {
|
||||
try {
|
||||
const engine = await faceBehavioralService.getBehavioralEngine(req.params.economyId);
|
||||
res.json(engine);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/behavioral/:economyId/analyze', async (req, res, next) => {
|
||||
try {
|
||||
const analysis = await faceBehavioralService.analyzeBehavior(req.params.economyId, req.body);
|
||||
res.json(analysis);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Supply contract routes
|
||||
router.post('/supply', async (req, res, next) => {
|
||||
try {
|
||||
const contract = await faceSupplyService.createSupplyContract(req.body);
|
||||
res.json(contract);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/supply/:economyId', async (req, res, next) => {
|
||||
try {
|
||||
const contracts = await faceSupplyService.getContractsForEconomy(req.params.economyId);
|
||||
res.json(contracts);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/supply/:contractId/check', async (req, res, next) => {
|
||||
try {
|
||||
const result = await faceSupplyService.checkSupplyContract(req.params.contractId, req.body.currentVelocity);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Stabilization contract routes
|
||||
router.post('/stabilization', async (req, res, next) => {
|
||||
try {
|
||||
const contract = await faceStabilizationService.createStabilizationContract(req.body);
|
||||
res.json(contract);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stabilization/:economyId', async (req, res, next) => {
|
||||
try {
|
||||
const contracts = await faceStabilizationService.getContractsForEconomy(req.params.economyId);
|
||||
res.json(contracts);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/stabilization/:contractId/check', async (req, res, next) => {
|
||||
try {
|
||||
const result = await faceStabilizationService.checkStabilizationContract(req.params.contractId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Incentive routes
|
||||
router.post('/incentives', async (req, res, next) => {
|
||||
try {
|
||||
const incentive = await faceIncentiveService.createIncentive(req.body);
|
||||
res.json(incentive);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/incentives/:economyId', async (req, res, next) => {
|
||||
try {
|
||||
const incentives = await faceIncentiveService.getIncentivesForEconomy(req.params.economyId);
|
||||
res.json(incentives);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/incentives/:incentiveId/apply', async (req, res, next) => {
|
||||
try {
|
||||
const result = await faceIncentiveService.checkAndApplyIncentive(req.params.incentiveId, req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
41
src/core/cbdc/governance/cbdc-compliance-board.service.ts
Normal file
41
src/core/cbdc/governance/cbdc-compliance-board.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// CBDC Compliance Board Service
|
||||
// CCEB enforcement
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export class CbdcComplianceBoardService {
|
||||
/**
|
||||
* Initialize or get CCEB
|
||||
*/
|
||||
async initializeCCEB() {
|
||||
const existing = await prisma.cbdcComplianceBoard.findFirst({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return await prisma.cbdcComplianceBoard.create({
|
||||
data: {
|
||||
boardId: `CCEB-${uuidv4()}`,
|
||||
boardName: 'CBDC Compliance & Enforcement Board',
|
||||
memberCount: 0,
|
||||
enforcementLevel: 'binding',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CCEB
|
||||
*/
|
||||
async getCCEB() {
|
||||
return await this.initializeCCEB();
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcComplianceBoardService = new CbdcComplianceBoardService();
|
||||
|
||||
48
src/core/cbdc/governance/cbdc-governance.routes.ts
Normal file
48
src/core/cbdc/governance/cbdc-governance.routes.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// CBDC Governance API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { cbdcSupplyControlService } from './cbdc-supply-control.service';
|
||||
import { cbdcVelocityControlService } from './cbdc-velocity-control.service';
|
||||
import { cbdcLiquidityManagementService } from './cbdc-liquidity-management.service';
|
||||
import { cbdcMonetarySimulationService } from './cbdc-monetary-simulation.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/supply-control', async (req, res, next) => {
|
||||
try {
|
||||
const control = await cbdcSupplyControlService.createSupplyControl(req.body);
|
||||
res.status(201).json(control);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/velocity-control', async (req, res, next) => {
|
||||
try {
|
||||
const control = await cbdcVelocityControlService.createVelocityControl(req.body);
|
||||
res.status(201).json(control);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/liquidity-window', async (req, res, next) => {
|
||||
try {
|
||||
const window = await cbdcLiquidityManagementService.createLiquidityWindow(req.body);
|
||||
res.status(201).json(window);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/simulation', async (req, res, next) => {
|
||||
try {
|
||||
const simulation = await cbdcMonetarySimulationService.runSimulation(req.body);
|
||||
res.json(simulation);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
103
src/core/cbdc/governance/cbdc-liquidity-management.service.ts
Normal file
103
src/core/cbdc/governance/cbdc-liquidity-management.service.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// CBDC Liquidity Management Service
|
||||
// Liquidity windows, CBDC-to-SSU swaps
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface LiquidityWindowRequest {
|
||||
sovereignBankId: string;
|
||||
windowType: string; // standing, emergency
|
||||
availableLiquidity: number;
|
||||
swapRate?: number;
|
||||
}
|
||||
|
||||
export class CbdcLiquidityManagementService {
|
||||
/**
|
||||
* Create liquidity window
|
||||
*/
|
||||
async createLiquidityWindow(request: LiquidityWindowRequest) {
|
||||
return await prisma.cbdcLiquidityWindow.create({
|
||||
data: {
|
||||
windowId: `WINDOW-${uuidv4()}`,
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
windowType: request.windowType,
|
||||
availableLiquidity: new Decimal(request.availableLiquidity),
|
||||
swapRate: request.swapRate ? new Decimal(request.swapRate) : null,
|
||||
status: 'open',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close liquidity window
|
||||
*/
|
||||
async closeLiquidityWindow(windowId: string) {
|
||||
return await prisma.cbdcLiquidityWindow.update({
|
||||
where: { windowId },
|
||||
data: {
|
||||
status: 'closed',
|
||||
closedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get open liquidity windows
|
||||
*/
|
||||
async getOpenWindows(sovereignBankId?: string) {
|
||||
const where: any = {
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
if (sovereignBankId) {
|
||||
where.sovereignBankId = sovereignBankId;
|
||||
}
|
||||
|
||||
return await prisma.cbdcLiquidityWindow.findMany({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute CBDC-to-SSU swap
|
||||
*/
|
||||
async executeCbdcToSsuSwap(
|
||||
windowId: string,
|
||||
cbdcAmount: number,
|
||||
swapRate: number
|
||||
) {
|
||||
const window = await prisma.cbdcLiquidityWindow.findUnique({
|
||||
where: { windowId },
|
||||
});
|
||||
|
||||
if (!window || window.status !== 'open') {
|
||||
throw new Error('Liquidity window not available');
|
||||
}
|
||||
|
||||
const ssuAmount = cbdcAmount * swapRate;
|
||||
const available = parseFloat(window.availableLiquidity.toString());
|
||||
|
||||
if (available < cbdcAmount) {
|
||||
throw new Error('Insufficient liquidity');
|
||||
}
|
||||
|
||||
// Update available liquidity
|
||||
await prisma.cbdcLiquidityWindow.update({
|
||||
where: { windowId },
|
||||
data: {
|
||||
availableLiquidity: new Decimal(available - cbdcAmount),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
cbdcAmount,
|
||||
ssuAmount,
|
||||
swapRate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcLiquidityManagementService = new CbdcLiquidityManagementService();
|
||||
|
||||
53
src/core/cbdc/governance/cbdc-monetary-committee.service.ts
Normal file
53
src/core/cbdc/governance/cbdc-monetary-committee.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// CBDC Monetary Committee Service
|
||||
// SCB Monetary Committees
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export class CbdcMonetaryCommitteeService {
|
||||
/**
|
||||
* Create monetary committee
|
||||
*/
|
||||
async createCommittee(
|
||||
sovereignBankId: string,
|
||||
committeeName: string,
|
||||
memberCount?: number,
|
||||
votingMechanism: string = 'simple_majority'
|
||||
) {
|
||||
return await prisma.cbdcMonetaryCommittee.create({
|
||||
data: {
|
||||
committeeId: `COMMITTEE-${uuidv4()}`,
|
||||
sovereignBankId,
|
||||
committeeName,
|
||||
memberCount,
|
||||
votingMechanism,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get committee by ID
|
||||
*/
|
||||
async getCommittee(committeeId: string) {
|
||||
return await prisma.cbdcMonetaryCommittee.findUnique({
|
||||
where: { committeeId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get committees for bank
|
||||
*/
|
||||
async getCommitteesForBank(sovereignBankId: string) {
|
||||
return await prisma.cbdcMonetaryCommittee.findMany({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcMonetaryCommitteeService = new CbdcMonetaryCommitteeService();
|
||||
|
||||
133
src/core/cbdc/governance/cbdc-monetary-simulation.service.ts
Normal file
133
src/core/cbdc/governance/cbdc-monetary-simulation.service.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// CBDC Monetary Simulation Service
|
||||
// Simulation: impact = CBDC_supply_change * velocity_factor * FX_reserve_strength
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface MonetarySimulationRequest {
|
||||
simulationType: string;
|
||||
sovereignBankId?: string;
|
||||
supplyChange?: number;
|
||||
velocityFactor?: number;
|
||||
fxReserveStrength?: number;
|
||||
}
|
||||
|
||||
export class CbdcMonetarySimulationService {
|
||||
/**
|
||||
* Run monetary simulation
|
||||
*/
|
||||
async runSimulation(request: MonetarySimulationRequest) {
|
||||
const simulationId = `SIM-${uuidv4()}`;
|
||||
|
||||
const simulation = await prisma.cbdcMonetarySimulation.create({
|
||||
data: {
|
||||
simulationId,
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
simulationType: request.simulationType,
|
||||
supplyChange: request.supplyChange ? new Decimal(request.supplyChange) : null,
|
||||
velocityFactor: request.velocityFactor ? new Decimal(request.velocityFactor) : null,
|
||||
fxReserveStrength: request.fxReserveStrength ? new Decimal(request.fxReserveStrength) : null,
|
||||
status: 'running',
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate impact score
|
||||
const impactScore = this.calculateImpactScore(
|
||||
request.supplyChange || 0,
|
||||
request.velocityFactor || 1,
|
||||
request.fxReserveStrength || 1
|
||||
);
|
||||
|
||||
// Run simulation logic based on type
|
||||
const simulationResults = await this.executeSimulation(
|
||||
request.simulationType,
|
||||
impactScore,
|
||||
request
|
||||
);
|
||||
|
||||
// Update simulation with results
|
||||
await prisma.cbdcMonetarySimulation.update({
|
||||
where: { simulationId },
|
||||
data: {
|
||||
impactScore: new Decimal(impactScore),
|
||||
simulationResults,
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate impact score
|
||||
*/
|
||||
private calculateImpactScore(
|
||||
supplyChange: number,
|
||||
velocityFactor: number,
|
||||
fxReserveStrength: number
|
||||
): number {
|
||||
// impact = CBDC_supply_change * velocity_factor * FX_reserve_strength
|
||||
return supplyChange * velocityFactor * fxReserveStrength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute simulation based on type
|
||||
*/
|
||||
private async executeSimulation(
|
||||
simulationType: string,
|
||||
impactScore: number,
|
||||
request: MonetarySimulationRequest
|
||||
): Promise<Record<string, unknown>> {
|
||||
// Simulate different types
|
||||
const results: Record<string, unknown> = {
|
||||
impactScore,
|
||||
simulationType,
|
||||
};
|
||||
|
||||
switch (simulationType) {
|
||||
case 'cross_border_flows':
|
||||
results.flows = {};
|
||||
break;
|
||||
case 'liquidity_shock':
|
||||
results.shockImpact = {};
|
||||
break;
|
||||
case 'fx_spillover':
|
||||
results.spilloverEffect = {};
|
||||
break;
|
||||
case 'commodity_backed_circulation':
|
||||
results.circulation = {};
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get simulation by ID
|
||||
*/
|
||||
async getSimulation(simulationId: string) {
|
||||
return await prisma.cbdcMonetarySimulation.findUnique({
|
||||
where: { simulationId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get simulations by type
|
||||
*/
|
||||
async getSimulationsByType(simulationType: string) {
|
||||
return await prisma.cbdcMonetarySimulation.findMany({
|
||||
where: {
|
||||
simulationType,
|
||||
},
|
||||
orderBy: {
|
||||
startedAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcMonetarySimulationService = new CbdcMonetarySimulationService();
|
||||
|
||||
87
src/core/cbdc/governance/cbdc-supply-control.service.ts
Normal file
87
src/core/cbdc/governance/cbdc-supply-control.service.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// CBDC Supply Control Service
|
||||
// Issue/Burn with dual-signature, stress-adjusted caps
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface SupplyControlRequest {
|
||||
sovereignBankId: string;
|
||||
operationType: string; // issue, burn
|
||||
amount: number;
|
||||
dualSignature1: string;
|
||||
dualSignature2: string;
|
||||
stressAdjustedCap?: number;
|
||||
committeeId?: string;
|
||||
}
|
||||
|
||||
export class CbdcSupplyControlService {
|
||||
/**
|
||||
* Create supply control operation
|
||||
*/
|
||||
async createSupplyControl(request: SupplyControlRequest) {
|
||||
return await prisma.cbdcSupplyControl.create({
|
||||
data: {
|
||||
controlId: `SUPPLY-${uuidv4()}`,
|
||||
committeeId: request.committeeId,
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
operationType: request.operationType,
|
||||
amount: new Decimal(request.amount),
|
||||
dualSignature1: request.dualSignature1,
|
||||
dualSignature2: request.dualSignature2,
|
||||
stressAdjustedCap: request.stressAdjustedCap ? new Decimal(request.stressAdjustedCap) : null,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve supply control
|
||||
*/
|
||||
async approveSupplyControl(controlId: string) {
|
||||
return await prisma.cbdcSupplyControl.update({
|
||||
where: { controlId },
|
||||
data: {
|
||||
status: 'approved',
|
||||
approvedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute supply control
|
||||
*/
|
||||
async executeSupplyControl(controlId: string) {
|
||||
return await prisma.cbdcSupplyControl.update({
|
||||
where: { controlId },
|
||||
data: {
|
||||
status: 'executed',
|
||||
executedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supply controls
|
||||
*/
|
||||
async getSupplyControls(sovereignBankId?: string, status?: string) {
|
||||
const where: any = {};
|
||||
if (sovereignBankId) {
|
||||
where.sovereignBankId = sovereignBankId;
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
return await prisma.cbdcSupplyControl.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcSupplyControlService = new CbdcSupplyControlService();
|
||||
|
||||
95
src/core/cbdc/governance/cbdc-velocity-control.service.ts
Normal file
95
src/core/cbdc/governance/cbdc-velocity-control.service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
// CBDC Velocity Control Service
|
||||
// Wallet limits, spending categories, throttles
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface VelocityControlRequest {
|
||||
sovereignBankId: string;
|
||||
walletId?: string;
|
||||
walletLevelLimit?: number;
|
||||
spendingCategory?: string;
|
||||
timeBasedThrottle?: Record<string, unknown>;
|
||||
committeeId?: string;
|
||||
effectiveDate: Date;
|
||||
expiryDate?: Date;
|
||||
}
|
||||
|
||||
export class CbdcVelocityControlService {
|
||||
/**
|
||||
* Create velocity control
|
||||
*/
|
||||
async createVelocityControl(request: VelocityControlRequest) {
|
||||
return await prisma.cbdcVelocityControl.create({
|
||||
data: {
|
||||
controlId: `VELOCITY-${uuidv4()}`,
|
||||
committeeId: request.committeeId,
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
walletId: request.walletId,
|
||||
walletLevelLimit: request.walletLevelLimit ? new Decimal(request.walletLevelLimit) : null,
|
||||
spendingCategory: request.spendingCategory,
|
||||
timeBasedThrottle: request.timeBasedThrottle || null,
|
||||
status: 'active',
|
||||
effectiveDate: request.effectiveDate,
|
||||
expiryDate: request.expiryDate || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active velocity controls
|
||||
*/
|
||||
async getActiveControls(sovereignBankId?: string, walletId?: string) {
|
||||
const where: any = {
|
||||
status: 'active',
|
||||
effectiveDate: {
|
||||
lte: new Date(),
|
||||
},
|
||||
OR: [
|
||||
{ expiryDate: null },
|
||||
{ expiryDate: { gte: new Date() } },
|
||||
],
|
||||
};
|
||||
|
||||
if (sovereignBankId) {
|
||||
where.sovereignBankId = sovereignBankId;
|
||||
}
|
||||
if (walletId) {
|
||||
where.walletId = walletId;
|
||||
}
|
||||
|
||||
return await prisma.cbdcVelocityControl.findMany({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check velocity limit
|
||||
*/
|
||||
async checkVelocityLimit(
|
||||
walletId: string,
|
||||
amount: number,
|
||||
category?: string
|
||||
): Promise<boolean> {
|
||||
const controls = await this.getActiveControls(undefined, walletId);
|
||||
|
||||
for (const control of controls) {
|
||||
// Check category match
|
||||
if (control.spendingCategory && category && control.spendingCategory !== category) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check wallet level limit
|
||||
if (control.walletLevelLimit && amount > parseFloat(control.walletLevelLimit.toString())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const cbdcVelocityControlService = new CbdcVelocityControlService();
|
||||
|
||||
41
src/core/cbdc/governance/dbis-monetary-council.service.ts
Normal file
41
src/core/cbdc/governance/dbis-monetary-council.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// DBIS Monetary Council Service
|
||||
// MSC coordination
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export class DbisMonetaryCouncilService {
|
||||
/**
|
||||
* Initialize or get MSC
|
||||
*/
|
||||
async initializeMSC() {
|
||||
const existing = await prisma.dbisMonetaryCouncil.findFirst({
|
||||
where: { status: 'active' },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return await prisma.dbisMonetaryCouncil.create({
|
||||
data: {
|
||||
councilId: `MSC-${uuidv4()}`,
|
||||
councilName: 'DBIS Monetary & Settlement Council',
|
||||
memberCount: 0,
|
||||
votingMechanism: 'supermajority_2_3',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MSC
|
||||
*/
|
||||
async getMSC() {
|
||||
return await this.initializeMSC();
|
||||
}
|
||||
}
|
||||
|
||||
export const dbisMonetaryCouncilService = new DbisMonetaryCouncilService();
|
||||
|
||||
329
src/core/cbdc/interoperability/cim-contracts.service.ts
Normal file
329
src/core/cbdc/interoperability/cim-contracts.service.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
// CIM Contracts Service
|
||||
// DBIS Contract Templates (DBIS-CT) management
|
||||
// Unified rule validation
|
||||
// Time-locked cross-border contracts
|
||||
// Condition-based execution
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface CimContractTemplateRequest {
|
||||
templateCode: string;
|
||||
templateName: string;
|
||||
templateType: string;
|
||||
contractLogic: Record<string, unknown>;
|
||||
validationRules: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CimContractExecutionRequest {
|
||||
templateCode: string;
|
||||
parameters: Record<string, unknown>;
|
||||
signatories: string[];
|
||||
executionTime?: Date;
|
||||
conditions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CimContractsService {
|
||||
/**
|
||||
* Create DBIS Contract Template (DBIS-CT)
|
||||
*/
|
||||
async createContractTemplate(
|
||||
request: CimContractTemplateRequest
|
||||
): Promise<string> {
|
||||
const templateId = `CIM-TEMPLATE-${uuidv4()}`;
|
||||
|
||||
const template = await prisma.cimContractTemplate.create({
|
||||
data: {
|
||||
templateId,
|
||||
templateCode: request.templateCode,
|
||||
templateName: request.templateName,
|
||||
templateType: request.templateType,
|
||||
contractLogic: request.contractLogic as any,
|
||||
validationRules: request.validationRules as any,
|
||||
status: 'active',
|
||||
version: 1,
|
||||
effectiveDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return template.templateId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contract template by code
|
||||
*/
|
||||
async getContractTemplate(templateCode: string) {
|
||||
return await prisma.cimContractTemplate.findFirst({
|
||||
where: {
|
||||
templateCode,
|
||||
status: 'active',
|
||||
},
|
||||
orderBy: { version: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all active contract templates
|
||||
*/
|
||||
async listContractTemplates(templateType?: string) {
|
||||
return await prisma.cimContractTemplate.findMany({
|
||||
where: {
|
||||
...(templateType && { templateType }),
|
||||
status: 'active',
|
||||
},
|
||||
orderBy: { templateCode: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate contract against unified rules
|
||||
*/
|
||||
async validateContract(
|
||||
templateCode: string,
|
||||
parameters: Record<string, unknown>
|
||||
): Promise<{ valid: boolean; errors: string[] }> {
|
||||
const template = await this.getContractTemplate(templateCode);
|
||||
|
||||
if (!template) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: ['Contract template not found'],
|
||||
};
|
||||
}
|
||||
|
||||
const validationRules = template.validationRules as Record<string, unknown>;
|
||||
const errors: string[] = [];
|
||||
|
||||
// Apply unified rule validation
|
||||
for (const [ruleKey, ruleValue] of Object.entries(validationRules)) {
|
||||
const paramValue = parameters[ruleKey];
|
||||
|
||||
if (ruleValue === 'required' && !paramValue) {
|
||||
errors.push(`Required parameter ${ruleKey} is missing`);
|
||||
}
|
||||
|
||||
if (typeof ruleValue === 'object' && ruleValue !== null) {
|
||||
const rule = ruleValue as Record<string, unknown>;
|
||||
|
||||
// Type validation
|
||||
if (rule.type && typeof paramValue !== rule.type) {
|
||||
errors.push(`Parameter ${ruleKey} must be of type ${rule.type}`);
|
||||
}
|
||||
|
||||
// Range validation
|
||||
if (rule.min !== undefined && (paramValue as number) < (rule.min as number)) {
|
||||
errors.push(`Parameter ${ruleKey} must be at least ${rule.min}`);
|
||||
}
|
||||
|
||||
if (rule.max !== undefined && (paramValue as number) > (rule.max as number)) {
|
||||
errors.push(`Parameter ${ruleKey} must be at most ${rule.max}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create time-locked cross-border contract
|
||||
*/
|
||||
async createTimeLockedContract(
|
||||
request: CimContractExecutionRequest
|
||||
): Promise<string> {
|
||||
const template = await this.getContractTemplate(request.templateCode);
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Contract template not found');
|
||||
}
|
||||
|
||||
// Validate contract
|
||||
const validation = await this.validateContract(
|
||||
request.templateCode,
|
||||
request.parameters
|
||||
);
|
||||
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Contract validation failed: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
// Create smart contract with time-lock
|
||||
const contractId = `CIM-CONTRACT-${uuidv4()}`;
|
||||
|
||||
// Calculate execution time if not provided
|
||||
const executionTime = request.executionTime || new Date();
|
||||
|
||||
const contract = await prisma.smartContract.create({
|
||||
data: {
|
||||
contractId,
|
||||
sovereignBankId: '', // Will be set during execution
|
||||
templateType: `DBIS-CT:${request.templateCode}`,
|
||||
contractState: 'draft',
|
||||
parameters: {
|
||||
...request.parameters,
|
||||
executionTime: executionTime.toISOString(),
|
||||
conditions: request.conditions,
|
||||
} as any,
|
||||
signatories: request.signatories as any,
|
||||
},
|
||||
});
|
||||
|
||||
return contract.contractId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute condition-based contract
|
||||
*/
|
||||
async executeConditionBasedContract(
|
||||
contractId: string
|
||||
): Promise<boolean> {
|
||||
const contract = await prisma.smartContract.findUnique({
|
||||
where: { contractId },
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parameters = contract.parameters as Record<string, unknown>;
|
||||
const conditions = parameters.conditions as Record<string, unknown> | undefined;
|
||||
|
||||
if (!conditions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evaluate conditions
|
||||
const conditionsMet = await this.evaluateConditions(conditions);
|
||||
|
||||
if (!conditionsMet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if execution time has been reached
|
||||
const executionTime = parameters.executionTime as string | undefined;
|
||||
if (executionTime) {
|
||||
const execTime = new Date(executionTime);
|
||||
if (new Date() < execTime) {
|
||||
return false; // Not yet time to execute
|
||||
}
|
||||
}
|
||||
|
||||
// Execute contract
|
||||
const contractLogic = await this.getContractLogic(contract.templateType);
|
||||
const executionResult = await this.executeContractLogic(
|
||||
contractLogic,
|
||||
parameters
|
||||
);
|
||||
|
||||
// Update contract
|
||||
await prisma.smartContract.update({
|
||||
where: { contractId },
|
||||
data: {
|
||||
contractState: 'executed',
|
||||
executionResult: executionResult as any,
|
||||
executedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate contract conditions
|
||||
*/
|
||||
private async evaluateConditions(
|
||||
conditions: Record<string, unknown>
|
||||
): Promise<boolean> {
|
||||
// In production, this would evaluate complex conditions
|
||||
// For now, simple evaluation
|
||||
for (const [key, value] of Object.entries(conditions)) {
|
||||
// Example: Check if account balance meets condition
|
||||
if (key === 'minBalance') {
|
||||
// Would check actual balance here
|
||||
// For now, assume condition is met
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contract logic from template
|
||||
*/
|
||||
private async getContractLogic(
|
||||
templateType: string
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
const templateCode = templateType.replace('DBIS-CT:', '');
|
||||
const template = await this.getContractTemplate(templateCode);
|
||||
|
||||
return template?.contractLogic as Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute contract logic
|
||||
*/
|
||||
private async executeContractLogic(
|
||||
contractLogic: Record<string, unknown> | null,
|
||||
parameters: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!contractLogic) {
|
||||
return { status: 'no_logic' };
|
||||
}
|
||||
|
||||
// In production, this would execute the contract logic
|
||||
// For now, return success
|
||||
return {
|
||||
status: 'executed',
|
||||
executedAt: new Date().toISOString(),
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update contract template
|
||||
*/
|
||||
async updateContractTemplate(
|
||||
templateCode: string,
|
||||
updates: Partial<CimContractTemplateRequest>
|
||||
): Promise<string> {
|
||||
const existingTemplate = await this.getContractTemplate(templateCode);
|
||||
|
||||
if (!existingTemplate) {
|
||||
throw new Error('Contract template not found');
|
||||
}
|
||||
|
||||
// Create new version
|
||||
const templateId = `CIM-TEMPLATE-${uuidv4()}`;
|
||||
|
||||
const template = await prisma.cimContractTemplate.create({
|
||||
data: {
|
||||
templateId,
|
||||
templateCode,
|
||||
templateName: updates.templateName || existingTemplate.templateName,
|
||||
templateType: updates.templateType || existingTemplate.templateType,
|
||||
contractLogic: (updates.contractLogic || existingTemplate.contractLogic) as any,
|
||||
validationRules: (updates.validationRules || existingTemplate.validationRules) as any,
|
||||
status: 'active',
|
||||
version: existingTemplate.version + 1,
|
||||
effectiveDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Archive old version
|
||||
await prisma.cimContractTemplate.update({
|
||||
where: { templateId: existingTemplate.templateId },
|
||||
data: {
|
||||
status: 'superseded',
|
||||
expiryDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return template.templateId;
|
||||
}
|
||||
}
|
||||
|
||||
export const cimContractsService = new CimContractsService();
|
||||
|
||||
213
src/core/cbdc/interoperability/cim-identity.service.ts
Normal file
213
src/core/cbdc/interoperability/cim-identity.service.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
// CIM Identity Service
|
||||
// Shared KYC/AML standards enforcement
|
||||
// Cross-certification of sovereign identities
|
||||
// HSM-signed CBDC wallet identity management
|
||||
// Identity mapping registry
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
||||
export interface CimIdentityMappingRequest {
|
||||
sourceSovereignBankId: string;
|
||||
targetSovereignBankId: string;
|
||||
sourceIdentityId: string;
|
||||
targetIdentityId: string;
|
||||
identityType: string;
|
||||
certificationLevel: string;
|
||||
}
|
||||
|
||||
export interface CrossCertificationResult {
|
||||
mappingId: string;
|
||||
crossCertificationHash: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class CimIdentityService {
|
||||
/**
|
||||
* Map cross-sovereign identity
|
||||
*/
|
||||
async mapIdentity(
|
||||
request: CimIdentityMappingRequest
|
||||
): Promise<CrossCertificationResult> {
|
||||
const mappingId = `CIM-MAP-${uuidv4()}`;
|
||||
|
||||
// Generate cross-certification hash
|
||||
const crossCertHash = this.generateCrossCertificationHash(
|
||||
request.sourceIdentityId,
|
||||
request.targetIdentityId,
|
||||
request.identityType
|
||||
);
|
||||
|
||||
// Create identity mapping
|
||||
const mapping = await prisma.cimIdentityMapping.create({
|
||||
data: {
|
||||
mappingId,
|
||||
sourceSovereignBankId: request.sourceSovereignBankId,
|
||||
targetSovereignBankId: request.targetSovereignBankId,
|
||||
sourceIdentityId: request.sourceIdentityId,
|
||||
targetIdentityId: request.targetIdentityId,
|
||||
identityType: request.identityType,
|
||||
certificationLevel: request.certificationLevel,
|
||||
crossCertificationHash: crossCertHash,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
mappingId: mapping.mappingId,
|
||||
crossCertificationHash: mapping.crossCertificationHash || '',
|
||||
status: mapping.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cross-certification hash
|
||||
*/
|
||||
private generateCrossCertificationHash(
|
||||
sourceIdentityId: string,
|
||||
targetIdentityId: string,
|
||||
identityType: string
|
||||
): string {
|
||||
const combined = `${sourceIdentityId}:${targetIdentityId}:${identityType}`;
|
||||
return createHash('sha3-256').update(combined).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify cross-sovereign identity
|
||||
*/
|
||||
async verifyCrossSovereignIdentity(
|
||||
sourceSovereignBankId: string,
|
||||
identityId: string,
|
||||
identityType: string
|
||||
): Promise<boolean> {
|
||||
const mapping = await prisma.cimIdentityMapping.findFirst({
|
||||
where: {
|
||||
sourceSovereignBankId,
|
||||
sourceIdentityId: identityId,
|
||||
identityType,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return !!mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identity mappings for a sovereign bank
|
||||
*/
|
||||
async getIdentityMappings(
|
||||
sovereignBankId: string,
|
||||
identityType?: string
|
||||
) {
|
||||
return await prisma.cimIdentityMapping.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ sourceSovereignBankId: sovereignBankId },
|
||||
{ targetSovereignBankId: sovereignBankId },
|
||||
],
|
||||
...(identityType && { identityType }),
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce shared KYC/AML standards
|
||||
*/
|
||||
async enforceKycAmlStandards(
|
||||
sovereignBankId: string,
|
||||
identityType: string
|
||||
): Promise<boolean> {
|
||||
// Get minimum standards
|
||||
const minimumStandards = this.getMinimumStandards(identityType);
|
||||
|
||||
// Verify against standards
|
||||
const mappings = await this.getIdentityMappings(sovereignBankId, identityType);
|
||||
|
||||
// Check if all mappings meet minimum standards
|
||||
for (const mapping of mappings) {
|
||||
if (!this.meetsStandards(mapping.certificationLevel, minimumStandards)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum KYC/AML standards
|
||||
*/
|
||||
private getMinimumStandards(identityType: string): string[] {
|
||||
const standards: Record<string, string[]> = {
|
||||
kyc: ['basic', 'enhanced'],
|
||||
aml: ['enhanced', 'sovereign'],
|
||||
cbdc_wallet: ['basic', 'enhanced', 'sovereign'],
|
||||
};
|
||||
|
||||
return standards[identityType] || ['basic'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certification level meets standards
|
||||
*/
|
||||
private meetsStandards(
|
||||
certificationLevel: string,
|
||||
minimumStandards: string[]
|
||||
): boolean {
|
||||
const levels: Record<string, number> = {
|
||||
basic: 1,
|
||||
enhanced: 2,
|
||||
sovereign: 3,
|
||||
};
|
||||
|
||||
const certificationValue = levels[certificationLevel] || 0;
|
||||
const minValue = Math.min(
|
||||
...minimumStandards.map((s) => levels[s] || 0)
|
||||
);
|
||||
|
||||
return certificationValue >= minValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HSM-signed CBDC wallet identity
|
||||
*/
|
||||
async getCbdcWalletIdentity(walletId: string): Promise<string | null> {
|
||||
// In production, this would retrieve from HSM
|
||||
// For now, generate a signature reference
|
||||
const wallet = await prisma.cbdcWallet.findUnique({
|
||||
where: { walletId },
|
||||
});
|
||||
|
||||
if (!wallet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate HSM identity reference
|
||||
const identityData = {
|
||||
walletId: wallet.walletId,
|
||||
sovereignBankId: wallet.sovereignBankId,
|
||||
walletType: wallet.walletType,
|
||||
};
|
||||
|
||||
return createHash('sha3-256')
|
||||
.update(JSON.stringify(identityData))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke identity mapping
|
||||
*/
|
||||
async revokeIdentityMapping(mappingId: string): Promise<void> {
|
||||
await prisma.cimIdentityMapping.update({
|
||||
where: { mappingId },
|
||||
data: {
|
||||
status: 'revoked',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cimIdentityService = new CimIdentityService();
|
||||
|
||||
333
src/core/cbdc/interoperability/cim-interledger.service.ts
Normal file
333
src/core/cbdc/interoperability/cim-interledger.service.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
// CIM Interledger Service
|
||||
// CBDC cross-border conversion
|
||||
// Dual-posting (SCB + DBIS) synchronization
|
||||
// FX-linked CBDC conversions
|
||||
// Interledger routing
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ledgerService } from '@/core/ledger/ledger.service';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
||||
export interface CimInterledgerConversionRequest {
|
||||
sourceSovereignBankId: string;
|
||||
targetSovereignBankId: string;
|
||||
sourceCbdcCode: string;
|
||||
targetCbdcCode: string;
|
||||
amount: string;
|
||||
conversionType: string;
|
||||
fxRate?: string;
|
||||
}
|
||||
|
||||
export interface InterledgerConversionResult {
|
||||
conversionId: string;
|
||||
scbLedgerHash: string;
|
||||
dbisLedgerHash: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class CimInterledgerService {
|
||||
/**
|
||||
* Execute CBDC interledger conversion
|
||||
*/
|
||||
async convertCbdc(
|
||||
request: CimInterledgerConversionRequest
|
||||
): Promise<InterledgerConversionResult> {
|
||||
const conversionId = `CIM-CONV-${uuidv4()}`;
|
||||
|
||||
try {
|
||||
// Step 1: Calculate FX rate if not provided
|
||||
let fxRate = request.fxRate
|
||||
? new Decimal(request.fxRate)
|
||||
: await this.calculateFxRate(
|
||||
request.sourceCbdcCode,
|
||||
request.targetCbdcCode
|
||||
);
|
||||
|
||||
// Step 2: Post to SCB ledger (source)
|
||||
const scbHash = await this.postToScbLedger(request, conversionId);
|
||||
|
||||
// Step 3: Post to DBIS master ledger
|
||||
const dbisHash = await this.postToDbisLedger(request, conversionId, fxRate);
|
||||
|
||||
// Step 4: Create conversion record
|
||||
const conversion = await prisma.cimInterledgerConversion.create({
|
||||
data: {
|
||||
conversionId,
|
||||
sourceSovereignBankId: request.sourceSovereignBankId,
|
||||
targetSovereignBankId: request.targetSovereignBankId,
|
||||
sourceCbdcCode: request.sourceCbdcCode,
|
||||
targetCbdcCode: request.targetCbdcCode,
|
||||
amount: new Decimal(request.amount),
|
||||
fxRate,
|
||||
conversionType: request.conversionType,
|
||||
dualPostingStatus: 'both_posted',
|
||||
scbLedgerHash: scbHash,
|
||||
dbisLedgerHash: dbisHash,
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
conversionId: conversion.conversionId,
|
||||
scbLedgerHash: conversion.scbLedgerHash || '',
|
||||
dbisLedgerHash: conversion.dbisLedgerHash || '',
|
||||
status: conversion.status,
|
||||
};
|
||||
} catch (error) {
|
||||
// Create failed conversion
|
||||
await prisma.cimInterledgerConversion.create({
|
||||
data: {
|
||||
conversionId,
|
||||
sourceSovereignBankId: request.sourceSovereignBankId,
|
||||
targetSovereignBankId: request.targetSovereignBankId,
|
||||
sourceCbdcCode: request.sourceCbdcCode,
|
||||
targetCbdcCode: request.targetCbdcCode,
|
||||
amount: new Decimal(request.amount),
|
||||
fxRate: request.fxRate ? new Decimal(request.fxRate) : null,
|
||||
conversionType: request.conversionType,
|
||||
dualPostingStatus: 'pending',
|
||||
status: 'failed',
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate FX rate between two CBDCs
|
||||
*/
|
||||
private async calculateFxRate(
|
||||
sourceCbdcCode: string,
|
||||
targetCbdcCode: string
|
||||
): Promise<Decimal> {
|
||||
// In production, this would query the FX service
|
||||
// For now, return a default rate
|
||||
const fxPair = await prisma.fxPair.findFirst({
|
||||
where: {
|
||||
baseCurrency: sourceCbdcCode,
|
||||
quoteCurrency: targetCbdcCode,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
if (fxPair) {
|
||||
// Get latest trade price
|
||||
const latestTrade = await prisma.fxTrade.findFirst({
|
||||
where: {
|
||||
fxPairId: fxPair.id,
|
||||
status: 'executed',
|
||||
},
|
||||
orderBy: { executedAt: 'desc' },
|
||||
});
|
||||
|
||||
if (latestTrade) {
|
||||
return new Decimal(latestTrade.price);
|
||||
}
|
||||
}
|
||||
|
||||
// Default rate of 1:1 if no FX data available
|
||||
return new Decimal(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to SCB ledger (source sovereign bank)
|
||||
*/
|
||||
private async postToScbLedger(
|
||||
request: CimInterledgerConversionRequest,
|
||||
conversionId: string
|
||||
): Promise<string> {
|
||||
// Get CBDC wallet for source
|
||||
const sourceWallet = await prisma.cbdcWallet.findFirst({
|
||||
where: {
|
||||
sovereignBankId: request.sourceSovereignBankId,
|
||||
currencyCode: request.sourceCbdcCode,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceWallet) {
|
||||
throw new Error('Source CBDC wallet not found');
|
||||
}
|
||||
|
||||
// Post debit entry to source wallet
|
||||
// In a real implementation, this would use the ledger service
|
||||
// For now, we'll generate a hash
|
||||
const entryData = {
|
||||
conversionId,
|
||||
sourceWalletId: sourceWallet.walletId,
|
||||
amount: request.amount,
|
||||
currencyCode: request.sourceCbdcCode,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return createHash('sha3-256')
|
||||
.update(JSON.stringify(entryData))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to DBIS master ledger
|
||||
*/
|
||||
private async postToDbisLedger(
|
||||
request: CimInterledgerConversionRequest,
|
||||
conversionId: string,
|
||||
fxRate: Decimal
|
||||
): Promise<string> {
|
||||
// Get accounts
|
||||
const sourceAccounts = await prisma.bankAccount.findMany({
|
||||
where: {
|
||||
sovereignBankId: request.sourceSovereignBankId,
|
||||
currencyCode: request.sourceCbdcCode,
|
||||
assetType: 'cbdc',
|
||||
status: 'active',
|
||||
},
|
||||
take: 1,
|
||||
});
|
||||
|
||||
const targetAccounts = await prisma.bankAccount.findMany({
|
||||
where: {
|
||||
sovereignBankId: request.targetSovereignBankId,
|
||||
currencyCode: request.targetCbdcCode,
|
||||
assetType: 'cbdc',
|
||||
status: 'active',
|
||||
},
|
||||
take: 1,
|
||||
});
|
||||
|
||||
if (sourceAccounts.length === 0 || targetAccounts.length === 0) {
|
||||
throw new Error('Accounts not found for DBIS ledger posting');
|
||||
}
|
||||
|
||||
// Calculate target amount
|
||||
const sourceAmount = new Decimal(request.amount);
|
||||
const targetAmount = sourceAmount.mul(fxRate);
|
||||
|
||||
// Post to DBIS master ledger
|
||||
const result = await ledgerService.postDoubleEntry(
|
||||
'Master',
|
||||
sourceAccounts[0].id,
|
||||
targetAccounts[0].id,
|
||||
targetAmount.toString(),
|
||||
request.targetCbdcCode,
|
||||
'cbdc',
|
||||
'Type_A',
|
||||
conversionId,
|
||||
undefined,
|
||||
{
|
||||
conversionType: request.conversionType,
|
||||
fxRate: fxRate.toString(),
|
||||
sourceAmount: request.amount,
|
||||
}
|
||||
);
|
||||
|
||||
// Get block hash
|
||||
const ledgerEntry = await prisma.ledgerEntry.findUnique({
|
||||
where: { id: result.entryIds[0] },
|
||||
});
|
||||
|
||||
return ledgerEntry?.blockHash || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize dual-posting status
|
||||
*/
|
||||
async synchronizeDualPosting(conversionId: string): Promise<void> {
|
||||
const conversion = await prisma.cimInterledgerConversion.findUnique({
|
||||
where: { conversionId },
|
||||
});
|
||||
|
||||
if (!conversion) {
|
||||
throw new Error('Conversion not found');
|
||||
}
|
||||
|
||||
// Check posting status
|
||||
let dualPostingStatus = conversion.dualPostingStatus;
|
||||
|
||||
if (conversion.scbLedgerHash && !conversion.dbisLedgerHash) {
|
||||
dualPostingStatus = 'scb_posted';
|
||||
} else if (!conversion.scbLedgerHash && conversion.dbisLedgerHash) {
|
||||
dualPostingStatus = 'dbis_posted';
|
||||
} else if (conversion.scbLedgerHash && conversion.dbisLedgerHash) {
|
||||
dualPostingStatus = 'both_posted';
|
||||
}
|
||||
|
||||
// Update status
|
||||
if (dualPostingStatus !== conversion.dualPostingStatus) {
|
||||
await prisma.cimInterledgerConversion.update({
|
||||
where: { conversionId },
|
||||
data: {
|
||||
dualPostingStatus,
|
||||
...(dualPostingStatus === 'both_posted' && {
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interledger conversions
|
||||
*/
|
||||
async getInterledgerConversions(
|
||||
sovereignBankId?: string,
|
||||
status?: string,
|
||||
limit: number = 100
|
||||
) {
|
||||
return await prisma.cimInterledgerConversion.findMany({
|
||||
where: {
|
||||
...(sovereignBankId && {
|
||||
OR: [
|
||||
{ sourceSovereignBankId: sovereignBankId },
|
||||
{ targetSovereignBankId: sovereignBankId },
|
||||
],
|
||||
}),
|
||||
...(status && { status }),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Route interledger transaction
|
||||
*/
|
||||
async routeInterledgerTransaction(
|
||||
sourceBankId: string,
|
||||
targetBankId: string,
|
||||
amount: string
|
||||
): Promise<string[]> {
|
||||
// Determine optimal routing path
|
||||
// SCB → SCB (direct)
|
||||
// SCB → DBIS → SCB (via DBIS)
|
||||
// SCB → DBIS → Private Bank
|
||||
|
||||
const routes: string[] = [];
|
||||
|
||||
// Check if direct route exists
|
||||
const directRoute = await prisma.settlementRoute.findFirst({
|
||||
where: {
|
||||
sourceBankId,
|
||||
destinationBankId: targetBankId,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
if (directRoute) {
|
||||
routes.push('scb_to_scb');
|
||||
} else {
|
||||
// Route via DBIS
|
||||
routes.push('scb_to_dbis_to_scb');
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
|
||||
export const cimInterledgerService = new CimInterledgerService();
|
||||
|
||||
303
src/core/cbdc/interoperability/cim-offline.service.ts
Normal file
303
src/core/cbdc/interoperability/cim-offline.service.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
// CIM Offline Service
|
||||
// Cross-sovereign capsule recognition
|
||||
// Double-spend protection registry
|
||||
// Global capsule synchronization
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
||||
export interface CimOfflineCapsuleRequest {
|
||||
sourceSovereignBankId: string;
|
||||
targetSovereignBankId: string;
|
||||
senderWalletId: string;
|
||||
receiverWalletId: string;
|
||||
amount: string;
|
||||
expiryWindow: number;
|
||||
}
|
||||
|
||||
export interface CrossSovereignCapsuleResult {
|
||||
capsuleId: string;
|
||||
doubleSpendToken: string;
|
||||
signature: string;
|
||||
crossSovereignRecognition: boolean;
|
||||
}
|
||||
|
||||
export class CimOfflineService {
|
||||
/**
|
||||
* Create cross-sovereign offline capsule
|
||||
*/
|
||||
async createCrossSovereignCapsule(
|
||||
request: CimOfflineCapsuleRequest
|
||||
): Promise<CrossSovereignCapsuleResult> {
|
||||
const capsuleId = `CIM-CAPSULE-${uuidv4()}`;
|
||||
const doubleSpendToken = this.generateDoubleSpendToken();
|
||||
const timestamp = new Date();
|
||||
|
||||
// Create signature payload
|
||||
const payload = {
|
||||
capsuleId,
|
||||
sourceSovereignBankId: request.sourceSovereignBankId,
|
||||
targetSovereignBankId: request.targetSovereignBankId,
|
||||
senderWalletId: request.senderWalletId,
|
||||
receiverWalletId: request.receiverWalletId,
|
||||
amount: request.amount,
|
||||
timestamp: timestamp.toISOString(),
|
||||
doubleSpendToken,
|
||||
expiryWindow: request.expiryWindow,
|
||||
};
|
||||
|
||||
const signature = this.generateCapsuleSignature(payload);
|
||||
|
||||
// Create capsule
|
||||
const capsule = await prisma.cimOfflineCapsule.create({
|
||||
data: {
|
||||
capsuleId,
|
||||
sourceSovereignBankId: request.sourceSovereignBankId,
|
||||
targetSovereignBankId: request.targetSovereignBankId,
|
||||
senderWalletId: request.senderWalletId,
|
||||
receiverWalletId: request.receiverWalletId,
|
||||
amount: new Decimal(request.amount),
|
||||
timestamp,
|
||||
expiryWindow: request.expiryWindow,
|
||||
doubleSpendToken,
|
||||
signature,
|
||||
crossSovereignRecognition: false, // Will be set during sync
|
||||
globalSyncStatus: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
capsuleId: capsule.capsuleId,
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
signature: capsule.signature,
|
||||
crossSovereignRecognition: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate double-spend token
|
||||
*/
|
||||
private generateDoubleSpendToken(): string {
|
||||
return createHash('sha3-256')
|
||||
.update(uuidv4() + Date.now().toString())
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate capsule signature
|
||||
*/
|
||||
private generateCapsuleSignature(payload: Record<string, unknown>): string {
|
||||
const payloadString = JSON.stringify(payload);
|
||||
return createHash('sha3-256').update(payloadString).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize cross-sovereign capsule
|
||||
*/
|
||||
async recognizeCrossSovereignCapsule(
|
||||
capsuleId: string,
|
||||
targetSovereignBankId: string
|
||||
): Promise<boolean> {
|
||||
const capsule = await prisma.cimOfflineCapsule.findUnique({
|
||||
where: { capsuleId },
|
||||
});
|
||||
|
||||
if (!capsule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify capsule is for this target sovereign
|
||||
if (capsule.targetSovereignBankId !== targetSovereignBankId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check double-spend protection
|
||||
const isDoubleSpend = await this.checkDoubleSpend(capsule.doubleSpendToken);
|
||||
|
||||
if (isDoubleSpend) {
|
||||
await prisma.cimOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
globalSyncStatus: 'rejected',
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recognize capsule
|
||||
await prisma.cimOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
crossSovereignRecognition: true,
|
||||
globalSyncStatus: 'recognized',
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check double-spend protection
|
||||
*/
|
||||
async checkDoubleSpend(doubleSpendToken: string): Promise<boolean> {
|
||||
// Check if token already exists in global registry
|
||||
const existingCapsule = await prisma.cimOfflineCapsule.findFirst({
|
||||
where: {
|
||||
doubleSpendToken,
|
||||
globalSyncStatus: {
|
||||
in: ['recognized', 'synced'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Also check in regular offline capsules
|
||||
const regularCapsule = await prisma.cbdcOfflineCapsule.findFirst({
|
||||
where: {
|
||||
doubleSpendToken,
|
||||
status: {
|
||||
in: ['validated', 'synced'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!(existingCapsule || regularCapsule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync capsule globally
|
||||
*/
|
||||
async syncCapsuleGlobally(capsuleId: string): Promise<boolean> {
|
||||
const capsule = await prisma.cimOfflineCapsule.findUnique({
|
||||
where: { capsuleId },
|
||||
});
|
||||
|
||||
if (!capsule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (capsule.globalSyncStatus !== 'recognized') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiry window
|
||||
const expiryTime = new Date(
|
||||
capsule.timestamp.getTime() + capsule.expiryWindow * 1000
|
||||
);
|
||||
|
||||
if (new Date() > expiryTime) {
|
||||
await prisma.cimOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
globalSyncStatus: 'rejected',
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
const payload = {
|
||||
capsuleId: capsule.capsuleId,
|
||||
sourceSovereignBankId: capsule.sourceSovereignBankId,
|
||||
targetSovereignBankId: capsule.targetSovereignBankId,
|
||||
senderWalletId: capsule.senderWalletId,
|
||||
receiverWalletId: capsule.receiverWalletId,
|
||||
amount: capsule.amount.toString(),
|
||||
timestamp: capsule.timestamp.toISOString(),
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
expiryWindow: capsule.expiryWindow,
|
||||
};
|
||||
|
||||
const expectedSignature = this.generateCapsuleSignature(payload);
|
||||
|
||||
if (capsule.signature !== expectedSignature) {
|
||||
await prisma.cimOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
globalSyncStatus: 'rejected',
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sync capsule
|
||||
await prisma.cimOfflineCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
globalSyncStatus: 'synced',
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Also sync to regular offline capsule registry
|
||||
await prisma.cbdcOfflineCapsule.upsert({
|
||||
where: { capsuleId },
|
||||
update: {
|
||||
status: 'synced',
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
capsuleId,
|
||||
senderWalletId: capsule.senderWalletId,
|
||||
receiverWalletId: capsule.receiverWalletId,
|
||||
amount: capsule.amount,
|
||||
timestamp: capsule.timestamp,
|
||||
expiryWindow: capsule.expiryWindow,
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
signature: capsule.signature,
|
||||
status: 'synced',
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get capsules for synchronization
|
||||
*/
|
||||
async getPendingCapsules(
|
||||
sovereignBankId?: string,
|
||||
limit: number = 100
|
||||
) {
|
||||
return await prisma.cimOfflineCapsule.findMany({
|
||||
where: {
|
||||
...(sovereignBankId && {
|
||||
OR: [
|
||||
{ sourceSovereignBankId: sovereignBankId },
|
||||
{ targetSovereignBankId: sovereignBankId },
|
||||
],
|
||||
}),
|
||||
globalSyncStatus: {
|
||||
in: ['pending', 'recognized'],
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get double-spend registry status
|
||||
*/
|
||||
async getDoubleSpendRegistryStatus(doubleSpendToken: string) {
|
||||
const crossSovereignCapsule = await prisma.cimOfflineCapsule.findFirst({
|
||||
where: { doubleSpendToken },
|
||||
});
|
||||
|
||||
const regularCapsule = await prisma.cbdcOfflineCapsule.findFirst({
|
||||
where: { doubleSpendToken },
|
||||
});
|
||||
|
||||
return {
|
||||
exists: !!(crossSovereignCapsule || regularCapsule),
|
||||
crossSovereignCapsule,
|
||||
regularCapsule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const cimOfflineService = new CimOfflineService();
|
||||
|
||||
134
src/core/cbdc/interoperability/cim.routes.ts
Normal file
134
src/core/cbdc/interoperability/cim.routes.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
// CIM API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { cimIdentityService } from './cim-identity.service';
|
||||
import { cimInterledgerService } from './cim-interledger.service';
|
||||
import { cimContractsService } from './cim-contracts.service';
|
||||
import { cimOfflineService } from './cim-offline.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cim/identity/map:
|
||||
* post:
|
||||
* summary: Map cross-sovereign identity
|
||||
* description: Create identity mapping between sovereign banks for CBDC interoperability
|
||||
* tags: [CBDC, CIM]
|
||||
* security:
|
||||
* - SovereignToken: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - sourceSovereignId
|
||||
* - targetSovereignId
|
||||
* - identityType
|
||||
* properties:
|
||||
* sourceSovereignId:
|
||||
* type: string
|
||||
* description: Source sovereign bank ID
|
||||
* targetSovereignId:
|
||||
* type: string
|
||||
* description: Target sovereign bank ID
|
||||
* identityType:
|
||||
* type: string
|
||||
* enum: [Sovereign, Institutional, Retail, Contract]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Identity mapped successfully
|
||||
* 400:
|
||||
* description: Validation error
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
router.post('/identity/map', async (req, res, next) => {
|
||||
try {
|
||||
const result = await cimIdentityService.mapIdentity(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cim/interledger/convert:
|
||||
* post:
|
||||
* summary: CBDC interledger conversion
|
||||
* description: Convert CBDC from one sovereign ledger to another
|
||||
* tags: [CBDC, CIM]
|
||||
* security:
|
||||
* - SovereignToken: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - sourceCbdcId
|
||||
* - targetSovereignId
|
||||
* - amount
|
||||
* properties:
|
||||
* sourceCbdcId:
|
||||
* type: string
|
||||
* targetSovereignId:
|
||||
* type: string
|
||||
* amount:
|
||||
* type: string
|
||||
* example: "1000.00"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Conversion completed
|
||||
* 400:
|
||||
* description: Validation error
|
||||
*/
|
||||
router.post('/interledger/convert', async (req, res, next) => {
|
||||
try {
|
||||
const result = await cimInterledgerService.convertCbdc(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cim/contracts/templates:
|
||||
* get:
|
||||
* summary: List DBIS-CT templates
|
||||
*/
|
||||
router.get('/contracts/templates', async (req, res, next) => {
|
||||
try {
|
||||
const { templateType } = req.query;
|
||||
const templates = await cimContractsService.listContractTemplates(
|
||||
templateType as string
|
||||
);
|
||||
res.json(templates);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cim/offline/sync-capsule:
|
||||
* post:
|
||||
* summary: Sync offline capsule
|
||||
*/
|
||||
router.post('/offline/sync-capsule', async (req, res, next) => {
|
||||
try {
|
||||
const { capsuleId } = req.body;
|
||||
const result = await cimOfflineService.syncCapsuleGlobally(capsuleId);
|
||||
res.json({ success: result });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
207
src/core/cbdc/tokenomics/gctf.routes.ts
Normal file
207
src/core/cbdc/tokenomics/gctf.routes.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
// Global CBDC Tokenomics Framework (GCTF) - API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { gctfService } from './gctf.service';
|
||||
import { CBDCUnitType } from './types';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: CBDC Tokenomics
|
||||
* description: Global CBDC Tokenomics Framework (GCTF)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/gctf/issue:
|
||||
* post:
|
||||
* summary: Issue new CBDC tokens
|
||||
* description: Issue new CBDC tokens through the Global CBDC Tokenomics Framework
|
||||
* tags: [CBDC Tokenomics]
|
||||
* security:
|
||||
* - SovereignToken: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - amount
|
||||
* - sovereignBankId
|
||||
* properties:
|
||||
* amount:
|
||||
* type: string
|
||||
* sovereignBankId:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: CBDC tokens issued
|
||||
*/
|
||||
router.post('/issue', async (req, res) => {
|
||||
try {
|
||||
const adjustment = await gctfService.issueCBDC(req.body);
|
||||
|
||||
res.json(adjustment);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gctf/burn
|
||||
* Burn CBDC tokens
|
||||
*/
|
||||
router.post('/burn', async (req, res) => {
|
||||
try {
|
||||
const adjustment = await gctfService.burnCBDC(req.body);
|
||||
|
||||
res.json(adjustment);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gctf/supply-mechanics/:sovereignBankId/:unitType
|
||||
* Get supply mechanics for a CBDC unit
|
||||
*/
|
||||
router.get('/supply-mechanics/:sovereignBankId/:unitType', async (req, res) => {
|
||||
try {
|
||||
const { sovereignBankId, unitType } = req.params;
|
||||
|
||||
const mechanics = await gctfService.getSupplyMechanics(
|
||||
sovereignBankId,
|
||||
unitType as CBDCUnitType
|
||||
);
|
||||
|
||||
res.json(mechanics);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gctf/velocity-controls
|
||||
* Configure velocity controls
|
||||
*/
|
||||
router.post('/velocity-controls', async (req, res) => {
|
||||
try {
|
||||
const { sovereignBankId, unitType, ...controls } = req.body;
|
||||
|
||||
const velocityControl = await gctfService.configureVelocityControls(
|
||||
sovereignBankId,
|
||||
unitType as CBDCUnitType,
|
||||
controls
|
||||
);
|
||||
|
||||
res.json(velocityControl);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gctf/quarantine/enable
|
||||
* Enable sovereign quarantine mode
|
||||
*/
|
||||
router.post('/quarantine/enable', async (req, res) => {
|
||||
try {
|
||||
const { sovereignBankId, unitType, endDate } = req.body;
|
||||
|
||||
const controls = await gctfService.enableQuarantineMode(
|
||||
sovereignBankId,
|
||||
unitType as CBDCUnitType,
|
||||
endDate ? new Date(endDate) : undefined
|
||||
);
|
||||
|
||||
res.json(controls);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gctf/quarantine/disable
|
||||
* Disable sovereign quarantine mode
|
||||
*/
|
||||
router.post('/quarantine/disable', async (req, res) => {
|
||||
try {
|
||||
const { sovereignBankId, unitType } = req.body;
|
||||
|
||||
const controls = await gctfService.disableQuarantineMode(
|
||||
sovereignBankId,
|
||||
unitType as CBDCUnitType
|
||||
);
|
||||
|
||||
res.json(controls);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gctf/config/:sovereignBankId/:unitType
|
||||
* Get tokenomics configuration
|
||||
*/
|
||||
router.get('/config/:sovereignBankId/:unitType', async (req, res) => {
|
||||
try {
|
||||
const { sovereignBankId, unitType } = req.params;
|
||||
|
||||
const config = await gctfService.getTokenomicsConfig(
|
||||
sovereignBankId,
|
||||
unitType as CBDCUnitType
|
||||
);
|
||||
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gctf/calculate-issuance
|
||||
* Calculate CBDC issuance using the rule
|
||||
*/
|
||||
router.post('/calculate-issuance', async (req, res) => {
|
||||
try {
|
||||
const { demandAdjustment, settlementVolume, factor, stressPenalties } = req.body;
|
||||
|
||||
const rule = gctfService.calculateIssuanceRule(
|
||||
demandAdjustment,
|
||||
settlementVolume,
|
||||
factor,
|
||||
stressPenalties
|
||||
);
|
||||
|
||||
res.json(rule);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gctf/check-velocity
|
||||
* Check if a transaction is allowed under velocity controls
|
||||
*/
|
||||
router.post('/check-velocity', async (req, res) => {
|
||||
try {
|
||||
const { sovereignBankId, unitType, transactionAmount, category } = req.body;
|
||||
|
||||
const result = await gctfService.checkVelocityControls(
|
||||
sovereignBankId,
|
||||
unitType as CBDCUnitType,
|
||||
transactionAmount,
|
||||
category
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
306
src/core/cbdc/tokenomics/gctf.service.ts
Normal file
306
src/core/cbdc/tokenomics/gctf.service.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
// Global CBDC Tokenomics Framework (GCTF) - Core Service
|
||||
|
||||
import { Decimal } from 'decimal.js';
|
||||
import {
|
||||
CBDCUnitType,
|
||||
CBDCIssuancePolicy,
|
||||
CBDCSupplyMechanics,
|
||||
CBDCIssuanceRule,
|
||||
CBDCVelocityControl,
|
||||
CBDCTokenomicsConfig,
|
||||
CBDCIssuanceRequest,
|
||||
CBDCBurnRequest,
|
||||
CBDCSupplyAdjustment,
|
||||
} from './types';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class GCTFService {
|
||||
/**
|
||||
* Calculate CBDC issuance using the rule:
|
||||
* CBDC_new = demand_adjustment + (settlement_volume * factor) - stress_penalties
|
||||
*/
|
||||
calculateIssuanceRule(
|
||||
demandAdjustment: number,
|
||||
settlementVolume: string,
|
||||
factor: number,
|
||||
stressPenalties: number
|
||||
): CBDCIssuanceRule {
|
||||
const settlementDecimal = new Decimal(settlementVolume);
|
||||
const calculatedSupply = new Decimal(demandAdjustment)
|
||||
.plus(settlementDecimal.times(factor))
|
||||
.minus(stressPenalties)
|
||||
.toString();
|
||||
|
||||
return {
|
||||
demandAdjustment,
|
||||
settlementVolume,
|
||||
factor,
|
||||
stressPenalties,
|
||||
calculatedSupply,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create supply mechanics for a CBDC unit
|
||||
*/
|
||||
async getSupplyMechanics(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType
|
||||
): Promise<CBDCSupplyMechanics> {
|
||||
// In real implementation, fetch from database
|
||||
// For now, return default structure
|
||||
return {
|
||||
sovereignBankId,
|
||||
unitType,
|
||||
currentSupply: '0',
|
||||
issuancePolicy: CBDCIssuancePolicy.DEMAND_DRIVEN,
|
||||
lastIssuance: new Date(),
|
||||
lastBurn: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue new CBDC tokens
|
||||
*/
|
||||
async issueCBDC(request: CBDCIssuanceRequest): Promise<CBDCSupplyAdjustment> {
|
||||
logger.info(`GCTF: Issuing ${request.amount} ${request.unitType} for ${request.sovereignBankId}`);
|
||||
|
||||
// Verify issuance is allowed based on policy
|
||||
const supplyMechanics = await this.getSupplyMechanics(
|
||||
request.sovereignBankId,
|
||||
request.unitType
|
||||
);
|
||||
|
||||
// Check max supply if set
|
||||
if (supplyMechanics.maxSupply) {
|
||||
const current = new Decimal(supplyMechanics.currentSupply);
|
||||
const max = new Decimal(supplyMechanics.maxSupply);
|
||||
const requested = new Decimal(request.amount);
|
||||
|
||||
if (current.plus(requested).gt(max)) {
|
||||
throw new Error(
|
||||
`Issuance would exceed max supply. Current: ${current}, Max: ${max}, Requested: ${requested}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify reserve backing if required
|
||||
if (supplyMechanics.issuancePolicy === CBDCIssuancePolicy.RESERVE_BACKED) {
|
||||
await this.verifyReserveBacking(
|
||||
request.sovereignBankId,
|
||||
request.unitType,
|
||||
request.amount
|
||||
);
|
||||
}
|
||||
|
||||
const adjustment: CBDCSupplyAdjustment = {
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
unitType: request.unitType,
|
||||
adjustment: request.amount,
|
||||
reason: request.reason,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// In real implementation, update database and ledger
|
||||
logger.info(`GCTF: Issued ${request.amount} ${request.unitType}`);
|
||||
|
||||
return adjustment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Burn CBDC tokens
|
||||
*/
|
||||
async burnCBDC(request: CBDCBurnRequest): Promise<CBDCSupplyAdjustment> {
|
||||
logger.info(`GCTF: Burning ${request.amount} ${request.unitType} for ${request.sovereignBankId}`);
|
||||
|
||||
const supplyMechanics = await this.getSupplyMechanics(
|
||||
request.sovereignBankId,
|
||||
request.unitType
|
||||
);
|
||||
|
||||
const current = new Decimal(supplyMechanics.currentSupply);
|
||||
const requested = new Decimal(request.amount);
|
||||
|
||||
if (requested.gt(current)) {
|
||||
throw new Error(
|
||||
`Burn amount exceeds current supply. Current: ${current}, Requested: ${requested}`
|
||||
);
|
||||
}
|
||||
|
||||
const adjustment: CBDCSupplyAdjustment = {
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
unitType: request.unitType,
|
||||
adjustment: `-${request.amount}`, // Negative for burn
|
||||
reason: request.reason,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// In real implementation, update database and ledger
|
||||
logger.info(`GCTF: Burned ${request.amount} ${request.unitType}`);
|
||||
|
||||
return adjustment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure velocity controls for a CBDC unit
|
||||
*/
|
||||
async configureVelocityControls(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType,
|
||||
controls: Partial<CBDCVelocityControl>
|
||||
): Promise<CBDCVelocityControl> {
|
||||
logger.info(`GCTF: Configuring velocity controls for ${unitType}`, {
|
||||
sovereignBankId,
|
||||
});
|
||||
|
||||
const velocityControl: CBDCVelocityControl = {
|
||||
sovereignBankId,
|
||||
unitType,
|
||||
...controls,
|
||||
};
|
||||
|
||||
// In real implementation, save to database
|
||||
return velocityControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable sovereign quarantine mode
|
||||
*/
|
||||
async enableQuarantineMode(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType,
|
||||
endDate?: Date
|
||||
): Promise<CBDCVelocityControl> {
|
||||
logger.warn(`GCTF: Enabling quarantine mode for ${unitType}`, {
|
||||
sovereignBankId,
|
||||
});
|
||||
|
||||
const controls = await this.configureVelocityControls(sovereignBankId, unitType, {
|
||||
sovereignQuarantineMode: true,
|
||||
quarantineStartDate: new Date(),
|
||||
quarantineEndDate: endDate,
|
||||
});
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable sovereign quarantine mode
|
||||
*/
|
||||
async disableQuarantineMode(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType
|
||||
): Promise<CBDCVelocityControl> {
|
||||
logger.info(`GCTF: Disabling quarantine mode for ${unitType}`, {
|
||||
sovereignBankId,
|
||||
});
|
||||
|
||||
const controls = await this.configureVelocityControls(sovereignBankId, unitType, {
|
||||
sovereignQuarantineMode: false,
|
||||
quarantineEndDate: new Date(),
|
||||
});
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tokenomics configuration
|
||||
*/
|
||||
async getTokenomicsConfig(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType
|
||||
): Promise<CBDCTokenomicsConfig> {
|
||||
const supplyMechanics = await this.getSupplyMechanics(sovereignBankId, unitType);
|
||||
|
||||
// In real implementation, fetch velocity controls from database
|
||||
const velocityControls: CBDCVelocityControl | undefined = undefined;
|
||||
|
||||
const config: CBDCTokenomicsConfig = {
|
||||
sovereignBankId,
|
||||
unitType,
|
||||
supplyMechanics,
|
||||
velocityControls,
|
||||
monetaryPolicy: {
|
||||
reserveRequirement: 0.1, // 10% default
|
||||
},
|
||||
programmableCompliance: {
|
||||
enabled: true,
|
||||
rules: [],
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify reserve backing for issuance
|
||||
*/
|
||||
private async verifyReserveBacking(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType,
|
||||
amount: string
|
||||
): Promise<void> {
|
||||
// In real implementation, check reserve accounts
|
||||
logger.info(`GCTF: Verifying reserve backing for ${amount} ${unitType}`);
|
||||
// Placeholder - would check actual reserves
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply velocity controls to a transaction
|
||||
*/
|
||||
async checkVelocityControls(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType,
|
||||
transactionAmount: string,
|
||||
category?: string
|
||||
): Promise<{ allowed: boolean; reason?: string }> {
|
||||
// In real implementation, fetch velocity controls and check
|
||||
const controls = await this.getVelocityControls(sovereignBankId, unitType);
|
||||
|
||||
if (controls?.sovereignQuarantineMode) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Sovereign quarantine mode is active',
|
||||
};
|
||||
}
|
||||
|
||||
// Check spending category restrictions
|
||||
if (category && controls?.spendingCategoryRestrictions) {
|
||||
const restriction = controls.spendingCategoryRestrictions.find(
|
||||
(r) => r.category === category
|
||||
);
|
||||
if (restriction && !restriction.allowed) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Category ${category} is restricted`,
|
||||
};
|
||||
}
|
||||
if (restriction && new Decimal(transactionAmount).gt(restriction.maxAmount)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Transaction amount exceeds category limit`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check cross-border routing limits
|
||||
// This would be checked in the routing layer
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get velocity controls
|
||||
*/
|
||||
private async getVelocityControls(
|
||||
sovereignBankId: string,
|
||||
unitType: CBDCUnitType
|
||||
): Promise<CBDCVelocityControl | null> {
|
||||
// In real implementation, fetch from database
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const gctfService = new GCTFService();
|
||||
|
||||
6
src/core/cbdc/tokenomics/index.ts
Normal file
6
src/core/cbdc/tokenomics/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Global CBDC Tokenomics Framework (GCTF) - Module Exports
|
||||
|
||||
export * from './types';
|
||||
export * from './gctf.service';
|
||||
export { default as gctfRoutes } from './gctf.routes';
|
||||
|
||||
98
src/core/cbdc/tokenomics/types.ts
Normal file
98
src/core/cbdc/tokenomics/types.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// Global CBDC Tokenomics Framework (GCTF) - Type Definitions
|
||||
|
||||
export enum CBDCUnitType {
|
||||
RETAIL = 'rCBDC', // Retail CBDC - Consumer money
|
||||
WHOLESALE = 'wCBDC', // Wholesale CBDC - Interbank settlement token
|
||||
INSTITUTIONAL = 'iCBDC', // Institutional CBDC - For regulated corporates, supranationals
|
||||
}
|
||||
|
||||
export enum CBDCIssuancePolicy {
|
||||
DEMAND_DRIVEN = 'DEMAND_DRIVEN',
|
||||
FIXED_SUPPLY = 'FIXED_SUPPLY',
|
||||
RESERVE_BACKED = 'RESERVE_BACKED',
|
||||
HYBRID = 'HYBRID',
|
||||
}
|
||||
|
||||
export interface CBDCSupplyMechanics {
|
||||
sovereignBankId: string;
|
||||
unitType: CBDCUnitType;
|
||||
currentSupply: string; // Decimal as string
|
||||
maxSupply?: string; // Optional max supply cap
|
||||
issuancePolicy: CBDCIssuancePolicy;
|
||||
reserveBacking?: string; // For reserve-backed CBDCs
|
||||
commodityBacking?: string; // For commodity-backed CBDCs
|
||||
lastIssuance: Date;
|
||||
lastBurn: Date;
|
||||
}
|
||||
|
||||
export interface CBDCIssuanceRule {
|
||||
demandAdjustment: number; // Adjustment based on demand
|
||||
settlementVolume: string; // Current settlement volume
|
||||
factor: number; // Issuance factor (0-1)
|
||||
stressPenalties: number; // Penalties during stress periods
|
||||
calculatedSupply: string; // CBDC_new = demand_adjustment + (settlement_volume * factor) - stress_penalties
|
||||
}
|
||||
|
||||
export interface CBDCVelocityControl {
|
||||
sovereignBankId: string;
|
||||
unitType: CBDCUnitType;
|
||||
transactionVelocityCap?: {
|
||||
daily: number;
|
||||
weekly: number;
|
||||
monthly: number;
|
||||
};
|
||||
spendingCategoryRestrictions?: {
|
||||
category: string;
|
||||
maxAmount: string;
|
||||
allowed: boolean;
|
||||
}[];
|
||||
crossBorderRoutingLimit?: {
|
||||
maxAmount: string;
|
||||
maxTransactions: number;
|
||||
timeWindow: 'DAILY' | 'WEEKLY' | 'MONTHLY';
|
||||
};
|
||||
sovereignQuarantineMode?: boolean; // Active during crises
|
||||
quarantineStartDate?: Date;
|
||||
quarantineEndDate?: Date;
|
||||
}
|
||||
|
||||
export interface CBDCTokenomicsConfig {
|
||||
sovereignBankId: string;
|
||||
unitType: CBDCUnitType;
|
||||
supplyMechanics: CBDCSupplyMechanics;
|
||||
velocityControls?: CBDCVelocityControl;
|
||||
monetaryPolicy: {
|
||||
interestRate?: number; // If applicable
|
||||
reserveRequirement?: number;
|
||||
fxBandWidth?: number;
|
||||
};
|
||||
programmableCompliance: {
|
||||
enabled: boolean;
|
||||
rules: string[]; // Smart contract rule identifiers
|
||||
};
|
||||
}
|
||||
|
||||
export interface CBDCIssuanceRequest {
|
||||
sovereignBankId: string;
|
||||
unitType: CBDCUnitType;
|
||||
amount: string;
|
||||
reason: string;
|
||||
operatorIdentity: string;
|
||||
}
|
||||
|
||||
export interface CBDCBurnRequest {
|
||||
sovereignBankId: string;
|
||||
unitType: CBDCUnitType;
|
||||
amount: string;
|
||||
reason: string;
|
||||
operatorIdentity: string;
|
||||
}
|
||||
|
||||
export interface CBDCSupplyAdjustment {
|
||||
sovereignBankId: string;
|
||||
unitType: CBDCUnitType;
|
||||
adjustment: string; // Positive for issuance, negative for burn
|
||||
reason: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
277
src/core/cbdc/wallet-quantum/quantum-capsule.service.ts
Normal file
277
src/core/cbdc/wallet-quantum/quantum-capsule.service.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
// Quantum Capsule Service
|
||||
// Offline capsule management with double-spend prevention
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createHash } from 'crypto';
|
||||
import { quantumCryptoService } from '@/infrastructure/quantum/quantum-crypto.service';
|
||||
|
||||
|
||||
export interface CapsuleCreationRequest {
|
||||
senderWalletId: string;
|
||||
receiverWalletId: string;
|
||||
amount: string;
|
||||
expiryWindow: number; // seconds
|
||||
}
|
||||
|
||||
export interface CapsuleResult {
|
||||
capsuleId: string;
|
||||
doubleSpendToken: string;
|
||||
pqcSignature: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class QuantumCapsuleService {
|
||||
/**
|
||||
* Create offline quantum capsule
|
||||
*/
|
||||
async createCapsule(
|
||||
request: CapsuleCreationRequest
|
||||
): Promise<CapsuleResult> {
|
||||
const senderWallet = await prisma.quantumWallet.findUnique({
|
||||
where: { walletId: request.senderWalletId },
|
||||
});
|
||||
|
||||
if (!senderWallet) {
|
||||
throw new Error(`Sender wallet not found: ${request.senderWalletId}`);
|
||||
}
|
||||
|
||||
// Check balance
|
||||
if (senderWallet.balance.lessThan(new Decimal(request.amount))) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
|
||||
// Generate double-spend token
|
||||
const doubleSpendToken = this.generateDoubleSpendToken(
|
||||
request.senderWalletId,
|
||||
request.receiverWalletId,
|
||||
request.amount
|
||||
);
|
||||
|
||||
// Create capsule payload
|
||||
const capsulePayload = {
|
||||
senderWalletId: request.senderWalletId,
|
||||
receiverWalletId: request.receiverWalletId,
|
||||
amount: request.amount,
|
||||
timestamp: new Date().toISOString(),
|
||||
doubleSpendToken,
|
||||
expiryWindow: request.expiryWindow,
|
||||
};
|
||||
|
||||
const payloadString = JSON.stringify(capsulePayload);
|
||||
|
||||
// Sign with PQC (Dilithium)
|
||||
const pqcSignature = await this.signWithPQC(
|
||||
payloadString,
|
||||
senderWallet.dilithiumKeyId
|
||||
);
|
||||
|
||||
// Create capsule
|
||||
const capsuleId = `CAPSULE-${uuidv4()}`;
|
||||
const capsule = await prisma.quantumWalletCapsule.create({
|
||||
data: {
|
||||
capsuleId,
|
||||
senderWalletId: request.senderWalletId,
|
||||
receiverWalletId: request.receiverWalletId,
|
||||
amount: new Decimal(request.amount),
|
||||
timestamp: new Date(),
|
||||
expiryWindow: request.expiryWindow,
|
||||
doubleSpendToken,
|
||||
pqcSignature,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
// Reserve balance
|
||||
await prisma.quantumWallet.update({
|
||||
where: { walletId: request.senderWalletId },
|
||||
data: {
|
||||
balance: senderWallet.balance.minus(new Decimal(request.amount)),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
capsuleId: capsule.capsuleId,
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
pqcSignature: capsule.pqcSignature,
|
||||
status: capsule.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate capsule (dual verification: SCB + DBIS)
|
||||
*/
|
||||
async validateCapsule(capsuleId: string): Promise<{
|
||||
valid: boolean;
|
||||
scbVerified: boolean;
|
||||
dbisVerified: boolean;
|
||||
}> {
|
||||
const capsule = await prisma.quantumWalletCapsule.findUnique({
|
||||
where: { capsuleId },
|
||||
include: { wallet: true },
|
||||
});
|
||||
|
||||
if (!capsule) {
|
||||
return { valid: false, scbVerified: false, dbisVerified: false };
|
||||
}
|
||||
|
||||
// Check double-spend token
|
||||
const existingCapsule = await prisma.quantumWalletCapsule.findFirst({
|
||||
where: {
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
NOT: { capsuleId: capsule.capsuleId },
|
||||
status: { in: ['validated', 'synced'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCapsule) {
|
||||
return { valid: false, scbVerified: false, dbisVerified: false };
|
||||
}
|
||||
|
||||
// Verify PQC signature (SCB verification)
|
||||
const capsulePayload = {
|
||||
senderWalletId: capsule.senderWalletId,
|
||||
receiverWalletId: capsule.receiverWalletId,
|
||||
amount: capsule.amount.toString(),
|
||||
timestamp: capsule.timestamp.toISOString(),
|
||||
doubleSpendToken: capsule.doubleSpendToken,
|
||||
expiryWindow: capsule.expiryWindow,
|
||||
};
|
||||
|
||||
const payloadString = JSON.stringify(capsulePayload);
|
||||
const scbVerified = await this.verifyPQCSignature(
|
||||
payloadString,
|
||||
capsule.pqcSignature,
|
||||
capsule.wallet.dilithiumKeyId
|
||||
);
|
||||
|
||||
// DBIS verification (simplified - in production would have separate DBIS verification)
|
||||
const dbisVerified = scbVerified; // For now, use same verification
|
||||
|
||||
const valid = scbVerified && dbisVerified;
|
||||
|
||||
if (valid) {
|
||||
await prisma.quantumWalletCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
scbVerification: true,
|
||||
dbisVerification: true,
|
||||
status: 'validated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
valid,
|
||||
scbVerified,
|
||||
dbisVerified,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync capsule (finalize offline transaction)
|
||||
*/
|
||||
async syncCapsule(capsuleId: string): Promise<void> {
|
||||
const capsule = await prisma.quantumWalletCapsule.findUnique({
|
||||
where: { capsuleId },
|
||||
include: { wallet: true },
|
||||
});
|
||||
|
||||
if (!capsule) {
|
||||
throw new Error(`Capsule not found: ${capsuleId}`);
|
||||
}
|
||||
|
||||
if (capsule.status !== 'validated') {
|
||||
throw new Error(`Capsule must be validated before sync: ${capsule.status}`);
|
||||
}
|
||||
|
||||
// Update receiver wallet balance
|
||||
const receiverWallet = await prisma.quantumWallet.findUnique({
|
||||
where: { walletId: capsule.receiverWalletId },
|
||||
});
|
||||
|
||||
if (!receiverWallet) {
|
||||
throw new Error(`Receiver wallet not found: ${capsule.receiverWalletId}`);
|
||||
}
|
||||
|
||||
await prisma.quantumWallet.update({
|
||||
where: { walletId: capsule.receiverWalletId },
|
||||
data: {
|
||||
balance: receiverWallet.balance.plus(capsule.amount),
|
||||
},
|
||||
});
|
||||
|
||||
// Mark capsule as synced
|
||||
await prisma.quantumWalletCapsule.update({
|
||||
where: { capsuleId },
|
||||
data: {
|
||||
status: 'synced',
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate double-spend token
|
||||
*/
|
||||
private generateDoubleSpendToken(
|
||||
senderWalletId: string,
|
||||
receiverWalletId: string,
|
||||
amount: string
|
||||
): string {
|
||||
const data = `${senderWalletId}:${receiverWalletId}:${amount}:${Date.now()}`;
|
||||
return createHash('sha3-256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign with PQC
|
||||
*/
|
||||
private async signWithPQC(message: string, dilithiumKeyId: string): Promise<string> {
|
||||
// Get key
|
||||
const key = await prisma.cryptographicKey.findUnique({
|
||||
where: { keyId: dilithiumKeyId },
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new Error(`Key not found: ${dilithiumKeyId}`);
|
||||
}
|
||||
|
||||
// In production, would use actual PQC library
|
||||
// For now, generate signature hash
|
||||
const signature = createHash('sha3-256')
|
||||
.update(message + key.publicKey)
|
||||
.digest('hex');
|
||||
|
||||
return `PQC-SIG-${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify PQC signature
|
||||
*/
|
||||
private async verifyPQCSignature(
|
||||
message: string,
|
||||
signature: string,
|
||||
dilithiumKeyId: string
|
||||
): Promise<boolean> {
|
||||
// Get key
|
||||
const key = await prisma.cryptographicKey.findUnique({
|
||||
where: { keyId: dilithiumKeyId },
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In production, would use actual PQC library
|
||||
// For now, verify signature format
|
||||
const expectedSignature = createHash('sha3-256')
|
||||
.update(message + key.publicKey)
|
||||
.digest('hex');
|
||||
|
||||
return signature === `PQC-SIG-${expectedSignature}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const quantumCapsuleService = new QuantumCapsuleService();
|
||||
|
||||
170
src/core/cbdc/wallet-quantum/quantum-wallet.service.ts
Normal file
170
src/core/cbdc/wallet-quantum/quantum-wallet.service.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// Quantum Wallet Service
|
||||
// Wallet creation with PQC keys (Dilithium signatures, Kyber key exchange)
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { quantumCryptoService } from '@/infrastructure/quantum/quantum-crypto.service';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
||||
export interface QuantumWalletRequest {
|
||||
sovereignBankId: string;
|
||||
walletType: string; // retail, wholesale, institutional
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface QuantumWalletResult {
|
||||
walletId: string;
|
||||
dilithiumKeyId: string;
|
||||
kyberKeyId: string;
|
||||
hsmIdentityCert: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class QuantumWalletService {
|
||||
/**
|
||||
* Create quantum-safe wallet
|
||||
*/
|
||||
async createWallet(
|
||||
request: QuantumWalletRequest
|
||||
): Promise<QuantumWalletResult> {
|
||||
const walletId = `QWALLET-${uuidv4()}`;
|
||||
|
||||
// Generate PQC key pairs
|
||||
// Dilithium for signatures
|
||||
const dilithiumKey = await quantumCryptoService.generatePQCKeyPair(
|
||||
'CRYSTALS-Dilithium',
|
||||
`quantum_wallet_signature_${walletId}`
|
||||
);
|
||||
|
||||
// Kyber for key exchange
|
||||
const kyberKey = await quantumCryptoService.generatePQCKeyPair(
|
||||
'CRYSTALS-Kyber',
|
||||
`quantum_wallet_key_exchange_${walletId}`
|
||||
);
|
||||
|
||||
// Generate HSM-bound identity certificate
|
||||
const hsmIdentityCert = await this.generateHSMIdentityCert(
|
||||
walletId,
|
||||
request.sovereignBankId,
|
||||
dilithiumKey.keyId,
|
||||
kyberKey.keyId
|
||||
);
|
||||
|
||||
// Create quantum wallet
|
||||
const wallet = await prisma.quantumWallet.create({
|
||||
data: {
|
||||
walletId,
|
||||
sovereignBankId: request.sovereignBankId,
|
||||
walletType: request.walletType,
|
||||
currencyCode: request.currencyCode,
|
||||
balance: new Decimal(0),
|
||||
dilithiumKeyId: dilithiumKey.keyId,
|
||||
kyberKeyId: kyberKey.keyId,
|
||||
hsmIdentityCert,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
walletId: wallet.walletId,
|
||||
dilithiumKeyId: wallet.dilithiumKeyId,
|
||||
kyberKeyId: wallet.kyberKeyId,
|
||||
hsmIdentityCert: wallet.hsmIdentityCert,
|
||||
status: wallet.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HSM-bound identity certificate
|
||||
*/
|
||||
private async generateHSMIdentityCert(
|
||||
walletId: string,
|
||||
sovereignBankId: string,
|
||||
dilithiumKeyId: string,
|
||||
kyberKeyId: string
|
||||
): Promise<string> {
|
||||
// In production, this would call actual HSM
|
||||
// For now, generate a certificate hash
|
||||
const certData = {
|
||||
walletId,
|
||||
sovereignBankId,
|
||||
dilithiumKeyId,
|
||||
kyberKeyId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const certString = JSON.stringify(certData);
|
||||
const certHash = createHash('sha3-256').update(certString).digest('hex');
|
||||
const cert = `HSM-CERT-${certHash}`;
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wallet details
|
||||
*/
|
||||
async getWallet(walletId: string): Promise<any> {
|
||||
const wallet = await prisma.quantumWallet.findUnique({
|
||||
where: { walletId },
|
||||
});
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error(`Wallet not found: ${walletId}`);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update wallet balance
|
||||
*/
|
||||
async updateBalance(walletId: string, amount: string, operation: 'add' | 'subtract'): Promise<void> {
|
||||
const wallet = await prisma.quantumWallet.findUnique({
|
||||
where: { walletId },
|
||||
});
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error(`Wallet not found: ${walletId}`);
|
||||
}
|
||||
|
||||
const currentBalance = wallet.balance;
|
||||
const amountDecimal = new Decimal(amount);
|
||||
const newBalance = operation === 'add'
|
||||
? currentBalance.plus(amountDecimal)
|
||||
: currentBalance.minus(amountDecimal);
|
||||
|
||||
if (newBalance.isNegative()) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
|
||||
await prisma.quantumWallet.update({
|
||||
where: { walletId },
|
||||
data: { balance: newBalance },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend wallet
|
||||
*/
|
||||
async suspendWallet(walletId: string): Promise<void> {
|
||||
await prisma.quantumWallet.update({
|
||||
where: { walletId },
|
||||
data: { status: 'suspended' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke wallet
|
||||
*/
|
||||
async revokeWallet(walletId: string): Promise<void> {
|
||||
await prisma.quantumWallet.update({
|
||||
where: { walletId },
|
||||
data: { status: 'revoked' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const quantumWalletService = new QuantumWalletService();
|
||||
|
||||
154
src/core/cbdc/wallet-quantum/wallet-attestation.service.ts
Normal file
154
src/core/cbdc/wallet-quantum/wallet-attestation.service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
// Wallet Attestation Service
|
||||
// Device attestation (12-hour cycle)
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
||||
export interface AttestationRequest {
|
||||
walletId: string;
|
||||
deviceAttestation: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AttestationResult {
|
||||
waoId: string;
|
||||
attestationHash: string;
|
||||
attestationCycle: number;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
export class WalletAttestationService {
|
||||
/**
|
||||
* Create wallet attestation object (WAO)
|
||||
*/
|
||||
async createAttestation(
|
||||
request: AttestationRequest
|
||||
): Promise<AttestationResult> {
|
||||
const wallet = await prisma.quantumWallet.findUnique({
|
||||
where: { walletId: request.walletId },
|
||||
});
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error(`Wallet not found: ${request.walletId}`);
|
||||
}
|
||||
|
||||
// Calculate attestation cycle (12-hour cycles since wallet creation)
|
||||
const walletCreated = wallet.createdAt;
|
||||
const now = new Date();
|
||||
const hoursSinceCreation = (now.getTime() - walletCreated.getTime()) / (1000 * 60 * 60);
|
||||
const attestationCycle = Math.floor(hoursSinceCreation / 12);
|
||||
|
||||
// Generate attestation hash
|
||||
const attestationData = {
|
||||
walletId: request.walletId,
|
||||
deviceAttestation: request.deviceAttestation,
|
||||
attestationCycle,
|
||||
timestamp: now.toISOString(),
|
||||
};
|
||||
|
||||
const attestationString = JSON.stringify(attestationData);
|
||||
const attestationHash = createHash('sha3-256').update(attestationString).digest('hex');
|
||||
|
||||
// Calculate expiry (next 12-hour cycle)
|
||||
const expiresAt = new Date(walletCreated);
|
||||
expiresAt.setHours(expiresAt.getHours() + (attestationCycle + 1) * 12);
|
||||
|
||||
// Create WAO
|
||||
const waoId = `WAO-${uuidv4()}`;
|
||||
const wao = await prisma.walletAttestationObject.create({
|
||||
data: {
|
||||
waoId,
|
||||
walletId: request.walletId,
|
||||
deviceAttestation: request.deviceAttestation,
|
||||
attestationHash,
|
||||
attestationCycle,
|
||||
status: 'valid',
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
// Update wallet with WAO reference
|
||||
await prisma.quantumWallet.update({
|
||||
where: { walletId: request.walletId },
|
||||
data: { waoId: wao.waoId },
|
||||
});
|
||||
|
||||
return {
|
||||
waoId: wao.waoId,
|
||||
attestationHash: wao.attestationHash,
|
||||
attestationCycle: wao.attestationCycle,
|
||||
expiresAt: wao.expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify attestation
|
||||
*/
|
||||
async verifyAttestation(waoId: string): Promise<boolean> {
|
||||
const wao = await prisma.walletAttestationObject.findUnique({
|
||||
where: { waoId },
|
||||
});
|
||||
|
||||
if (!wao) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (new Date() > wao.expiresAt) {
|
||||
await prisma.walletAttestationObject.update({
|
||||
where: { waoId },
|
||||
data: { status: 'expired' },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check status
|
||||
return wao.status === 'valid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke attestation
|
||||
*/
|
||||
async revokeAttestation(waoId: string): Promise<void> {
|
||||
await prisma.walletAttestationObject.update({
|
||||
where: { waoId },
|
||||
data: { status: 'revoked' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current attestation for wallet
|
||||
*/
|
||||
async getCurrentAttestation(walletId: string): Promise<any | null> {
|
||||
const wao = await prisma.walletAttestationObject.findFirst({
|
||||
where: {
|
||||
walletId,
|
||||
status: 'valid',
|
||||
},
|
||||
orderBy: { attestedAt: 'desc' },
|
||||
});
|
||||
|
||||
return wao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if attestation needs renewal
|
||||
*/
|
||||
async needsRenewal(walletId: string): Promise<boolean> {
|
||||
const wao = await this.getCurrentAttestation(walletId);
|
||||
|
||||
if (!wao) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if expires within 1 hour
|
||||
const oneHourFromNow = new Date();
|
||||
oneHourFromNow.setHours(oneHourFromNow.getHours() + 1);
|
||||
|
||||
return wao.expiresAt < oneHourFromNow;
|
||||
}
|
||||
}
|
||||
|
||||
export const walletAttestationService = new WalletAttestationService();
|
||||
|
||||
130
src/core/cbdc/wallet-quantum/wallet-risk.service.ts
Normal file
130
src/core/cbdc/wallet-quantum/wallet-risk.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
// Wallet Risk Service
|
||||
// Real-time risk scoring
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface RiskScoreRequest {
|
||||
walletId: string;
|
||||
}
|
||||
|
||||
export interface RiskScoreResult {
|
||||
scoreId: string;
|
||||
riskScore: string;
|
||||
riskFactors: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class WalletRiskService {
|
||||
/**
|
||||
* Calculate wallet risk score
|
||||
*/
|
||||
async calculateRiskScore(
|
||||
request: RiskScoreRequest
|
||||
): Promise<RiskScoreResult> {
|
||||
const wallet = await prisma.quantumWallet.findUnique({
|
||||
where: { walletId: request.walletId },
|
||||
include: {
|
||||
attestations: {
|
||||
orderBy: { attestedAt: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
riskScores: {
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error(`Wallet not found: ${request.walletId}`);
|
||||
}
|
||||
|
||||
const riskFactors: Record<string, unknown> = {};
|
||||
let riskScore = 0;
|
||||
|
||||
// Factor 1: Wallet type (retail = lower risk, institutional = higher risk)
|
||||
const typeRisk = wallet.walletType === 'retail' ? 10 : wallet.walletType === 'wholesale' ? 30 : 50;
|
||||
riskFactors.walletType = typeRisk;
|
||||
riskScore += typeRisk;
|
||||
|
||||
// Factor 2: Attestation status
|
||||
const latestAttestation = wallet.attestations[0];
|
||||
if (!latestAttestation || latestAttestation.status !== 'valid') {
|
||||
riskFactors.attestationStatus = 30;
|
||||
riskScore += 30;
|
||||
} else {
|
||||
riskFactors.attestationStatus = 0;
|
||||
}
|
||||
|
||||
// Factor 3: Balance (higher balance = higher risk)
|
||||
const balanceRisk = wallet.balance.greaterThan(new Decimal(1000000))
|
||||
? 20
|
||||
: wallet.balance.greaterThan(new Decimal(100000))
|
||||
? 10
|
||||
: 0;
|
||||
riskFactors.balance = balanceRisk;
|
||||
riskScore += balanceRisk;
|
||||
|
||||
// Factor 4: Recent risk history
|
||||
if (wallet.riskScores.length > 0) {
|
||||
const avgRecentScore = wallet.riskScores.reduce(
|
||||
(sum, rs) => sum.plus(rs.riskScore),
|
||||
new Decimal(0)
|
||||
).div(wallet.riskScores.length);
|
||||
|
||||
const historyRisk = avgRecentScore.greaterThan(new Decimal(70)) ? 20 : 0;
|
||||
riskFactors.history = historyRisk;
|
||||
riskScore += historyRisk;
|
||||
}
|
||||
|
||||
// Normalize to 0-100 scale
|
||||
riskScore = Math.min(100, Math.max(0, riskScore));
|
||||
|
||||
// Create risk score record
|
||||
const scoreId = `RISK-${uuidv4()}`;
|
||||
const score = await prisma.walletRiskScore.create({
|
||||
data: {
|
||||
scoreId,
|
||||
walletId: request.walletId,
|
||||
riskScore: new Decimal(riskScore),
|
||||
riskFactors,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
scoreId: score.scoreId,
|
||||
riskScore: score.riskScore.toString(),
|
||||
riskFactors: score.riskFactors as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest risk score
|
||||
*/
|
||||
async getLatestRiskScore(walletId: string): Promise<any | null> {
|
||||
const score = await prisma.walletRiskScore.findFirst({
|
||||
where: { walletId },
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
});
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get risk score history
|
||||
*/
|
||||
async getRiskScoreHistory(walletId: string, limit: number = 10): Promise<any[]> {
|
||||
const scores = await prisma.walletRiskScore.findMany({
|
||||
where: { walletId },
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return scores;
|
||||
}
|
||||
}
|
||||
|
||||
export const walletRiskService = new WalletRiskService();
|
||||
|
||||
181
src/core/cbdc/zk-validation/zk-balance-proof.service.ts
Normal file
181
src/core/cbdc/zk-validation/zk-balance-proof.service.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
// ZK-CBDC Validation: Mode 1 - ZK-Balance Proofs (zkBP)
|
||||
// Prove wallet has sufficient funds without revealing amount
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
|
||||
|
||||
export interface ZkBalanceProofRequest {
|
||||
walletId: string;
|
||||
requiredAmount: string; // Amount to prove (not revealed in proof)
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface ZkBalanceProof {
|
||||
proofId: string;
|
||||
proofData: string;
|
||||
publicInputs: {
|
||||
walletId: string;
|
||||
currencyCode: string;
|
||||
sufficient: boolean; // Only this is revealed, not the actual amount
|
||||
};
|
||||
}
|
||||
|
||||
export class ZkBalanceProofService {
|
||||
/**
|
||||
* Generate ZK-Balance Proof
|
||||
* Proves wallet has >= requiredAmount without revealing the actual balance
|
||||
*/
|
||||
async generateBalanceProof(request: ZkBalanceProofRequest): Promise<ZkBalanceProof> {
|
||||
const proofId = `ZKBP-${uuidv4()}`;
|
||||
|
||||
// Get wallet balance (in production, this would be done securely)
|
||||
const wallet = await this.getWalletBalance(request.walletId, request.currencyCode);
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error('Wallet not found');
|
||||
}
|
||||
|
||||
const actualBalance = parseFloat(wallet.balance);
|
||||
const requiredAmount = parseFloat(request.requiredAmount);
|
||||
|
||||
// Check if balance is sufficient
|
||||
const sufficient = actualBalance >= requiredAmount;
|
||||
|
||||
if (!sufficient) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
|
||||
// Generate ZK proof (simplified - in production would use actual ZK library)
|
||||
// The proof demonstrates: balance >= requiredAmount without revealing balance
|
||||
const proofData = await this.generateZkProof({
|
||||
balance: actualBalance,
|
||||
requiredAmount,
|
||||
walletId: request.walletId,
|
||||
});
|
||||
|
||||
// Create proof record
|
||||
await prisma.zkProof.create({
|
||||
data: {
|
||||
proofId,
|
||||
walletId: request.walletId,
|
||||
proofType: 'zkBP',
|
||||
proofData,
|
||||
publicInputs: {
|
||||
walletId: request.walletId,
|
||||
currencyCode: request.currencyCode,
|
||||
sufficient: true,
|
||||
} as unknown as Record<string, unknown>,
|
||||
verificationKey: 'default_zkbp_vk', // In production, use actual verification key
|
||||
status: 'verified',
|
||||
verifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('ZK-CBDC: Generated ZK-Balance Proof', {
|
||||
proofId,
|
||||
walletId: request.walletId,
|
||||
sufficient,
|
||||
});
|
||||
|
||||
return {
|
||||
proofId,
|
||||
proofData,
|
||||
publicInputs: {
|
||||
walletId: request.walletId,
|
||||
currencyCode: request.currencyCode,
|
||||
sufficient: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify ZK-Balance Proof
|
||||
*/
|
||||
async verifyBalanceProof(proofId: string): Promise<boolean> {
|
||||
const proof = await prisma.zkProof.findUnique({
|
||||
where: { proofId },
|
||||
});
|
||||
|
||||
if (!proof || proof.proofType !== 'zkBP') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify proof (simplified - in production would use ZK verification)
|
||||
const isValid = await this.verifyZkProof(proof.proofData, proof.publicInputs as unknown as Record<string, unknown>);
|
||||
|
||||
if (isValid) {
|
||||
await prisma.zkProof.update({
|
||||
where: { proofId },
|
||||
data: {
|
||||
status: 'verified',
|
||||
verifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wallet balance (internal)
|
||||
*/
|
||||
private async getWalletBalance(walletId: string, currencyCode: string) {
|
||||
// In production, this would query CBDC wallet system
|
||||
// For now, simplified lookup
|
||||
return await prisma.bankAccount.findFirst({
|
||||
where: {
|
||||
accountNumber: walletId,
|
||||
currencyCode,
|
||||
assetType: 'cbdc',
|
||||
},
|
||||
select: {
|
||||
balance: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ZK proof (placeholder - in production would use circom/snarkjs)
|
||||
*/
|
||||
private async generateZkProof(data: {
|
||||
balance: number;
|
||||
requiredAmount: number;
|
||||
walletId: string;
|
||||
}): Promise<string> {
|
||||
// In production, this would:
|
||||
// 1. Create circuit witness
|
||||
// 2. Generate proof using snarkjs or similar
|
||||
// 3. Return proof data
|
||||
|
||||
// For now, return a placeholder proof
|
||||
return JSON.stringify({
|
||||
type: 'zkBP',
|
||||
balance: data.balance,
|
||||
requiredAmount: data.requiredAmount,
|
||||
proof: 'placeholder_zk_proof',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify ZK proof (placeholder)
|
||||
*/
|
||||
private async verifyZkProof(proofData: string, publicInputs: Record<string, unknown>): Promise<boolean> {
|
||||
// In production, this would:
|
||||
// 1. Parse proof data
|
||||
// 2. Verify using snarkjs or similar
|
||||
// 3. Return verification result
|
||||
|
||||
// For now, simplified check
|
||||
try {
|
||||
const proof = JSON.parse(proofData);
|
||||
return proof.type === 'zkBP' && publicInputs.sufficient === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const zkBalanceProofService = new ZkBalanceProofService();
|
||||
|
||||
72
src/core/cbdc/zk-validation/zk-cbdc.routes.ts
Normal file
72
src/core/cbdc/zk-validation/zk-cbdc.routes.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// ZK-CBDC API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { zkBalanceProofService } from './zk-balance-proof.service';
|
||||
import { zkComplianceProofService } from './zk-compliance-proof.service';
|
||||
import { zkIdentityProofService } from './zk-identity-proof.service';
|
||||
import { zkVerificationService } from './zk-verification.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/zk-cbdc/balance-proof:
|
||||
* post:
|
||||
* summary: Generate ZK-Balance Proof
|
||||
*/
|
||||
router.post('/balance-proof', async (req, res, next) => {
|
||||
try {
|
||||
const result = await zkBalanceProofService.generateBalanceProof(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/zk-cbdc/compliance-proof:
|
||||
* post:
|
||||
* summary: Generate ZK-Compliance Proof
|
||||
*/
|
||||
router.post('/compliance-proof', async (req, res, next) => {
|
||||
try {
|
||||
const result = await zkComplianceProofService.generateComplianceProof(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/zk-cbdc/identity-proof:
|
||||
* post:
|
||||
* summary: Generate ZK-Identity Proof
|
||||
*/
|
||||
router.post('/identity-proof', async (req, res, next) => {
|
||||
try {
|
||||
const result = await zkIdentityProofService.generateIdentityProof(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/zk-cbdc/verify:
|
||||
* post:
|
||||
* summary: Verify CBDC transfer
|
||||
*/
|
||||
router.post('/verify', async (req, res, next) => {
|
||||
try {
|
||||
const result = await zkVerificationService.verifyCbdcTransfer(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
218
src/core/cbdc/zk-validation/zk-compliance-proof.service.ts
Normal file
218
src/core/cbdc/zk-validation/zk-compliance-proof.service.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
// ZK-CBDC Validation: Mode 2 - ZK-Compliance Proofs (zkCP)
|
||||
// AML rules satisfied, sanctions clear, transaction within policy limits
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { amlVelocityEngineService } from '@/core/compliance/ai/aml-velocity-engine.service';
|
||||
|
||||
|
||||
export interface ZkComplianceProofRequest {
|
||||
walletId: string;
|
||||
transactionAmount: string;
|
||||
destinationWalletId: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface ZkComplianceProof {
|
||||
proofId: string;
|
||||
proofData: string;
|
||||
publicInputs: {
|
||||
walletId: string;
|
||||
compliant: boolean; // Only this is revealed, not the compliance details
|
||||
amlClear: boolean;
|
||||
sanctionsClear: boolean;
|
||||
policyCompliant: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export class ZkComplianceProofService {
|
||||
/**
|
||||
* Generate ZK-Compliance Proof
|
||||
* Proves compliance without revealing compliance details
|
||||
*/
|
||||
async generateComplianceProof(request: ZkComplianceProofRequest): Promise<ZkComplianceProof> {
|
||||
const proofId = `ZKCP-${uuidv4()}`;
|
||||
|
||||
// Perform compliance checks (without revealing details in proof)
|
||||
const complianceChecks = await this.performComplianceChecks(request);
|
||||
|
||||
// Check if all compliance checks pass
|
||||
const compliant =
|
||||
complianceChecks.amlClear &&
|
||||
complianceChecks.sanctionsClear &&
|
||||
complianceChecks.policyCompliant;
|
||||
|
||||
if (!compliant) {
|
||||
throw new Error('Compliance check failed');
|
||||
}
|
||||
|
||||
// Generate ZK proof
|
||||
const proofData = await this.generateZkProof({
|
||||
walletId: request.walletId,
|
||||
transactionAmount: request.transactionAmount,
|
||||
destinationWalletId: request.destinationWalletId,
|
||||
complianceChecks,
|
||||
});
|
||||
|
||||
// Create proof record
|
||||
await prisma.zkProof.create({
|
||||
data: {
|
||||
proofId,
|
||||
walletId: request.walletId,
|
||||
proofType: 'zkCP',
|
||||
proofData,
|
||||
publicInputs: {
|
||||
walletId: request.walletId,
|
||||
compliant: true,
|
||||
amlClear: true,
|
||||
sanctionsClear: true,
|
||||
policyCompliant: true,
|
||||
} as unknown as Record<string, unknown>,
|
||||
verificationKey: 'default_zkcp_vk',
|
||||
status: 'verified',
|
||||
verifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('ZK-CBDC: Generated ZK-Compliance Proof', {
|
||||
proofId,
|
||||
walletId: request.walletId,
|
||||
compliant,
|
||||
});
|
||||
|
||||
return {
|
||||
proofId,
|
||||
proofData,
|
||||
publicInputs: {
|
||||
walletId: request.walletId,
|
||||
compliant: true,
|
||||
amlClear: true,
|
||||
sanctionsClear: true,
|
||||
policyCompliant: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform compliance checks
|
||||
*/
|
||||
private async performComplianceChecks(request: ZkComplianceProofRequest): Promise<{
|
||||
amlClear: boolean;
|
||||
sanctionsClear: boolean;
|
||||
policyCompliant: boolean;
|
||||
}> {
|
||||
// AML check
|
||||
const amlAnomalies = await amlVelocityEngineService.detectVelocityAnomalies(
|
||||
request.walletId,
|
||||
'wallet',
|
||||
3600000 // 1 hour
|
||||
);
|
||||
const amlClear = amlAnomalies.length === 0;
|
||||
|
||||
// Sanctions check (simplified)
|
||||
const sanctionsClear = await this.checkSanctions(request.walletId, request.destinationWalletId);
|
||||
|
||||
// Policy compliance check
|
||||
const policyCompliant = await this.checkPolicyLimits(
|
||||
request.walletId,
|
||||
request.transactionAmount,
|
||||
request.currencyCode
|
||||
);
|
||||
|
||||
return {
|
||||
amlClear,
|
||||
sanctionsClear,
|
||||
policyCompliant,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check sanctions (simplified)
|
||||
*/
|
||||
private async checkSanctions(walletId: string, destinationWalletId: string): Promise<boolean> {
|
||||
// In production, this would query sanctions screening system
|
||||
// For now, simplified check
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check policy limits
|
||||
*/
|
||||
private async checkPolicyLimits(
|
||||
walletId: string,
|
||||
amount: string,
|
||||
currencyCode: string
|
||||
): Promise<boolean> {
|
||||
// In production, this would check transaction limits, velocity limits, etc.
|
||||
// For now, simplified check
|
||||
const amountNum = parseFloat(amount);
|
||||
const maxTransactionLimit = 1000000; // $1M limit
|
||||
|
||||
return amountNum <= maxTransactionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ZK proof (placeholder)
|
||||
*/
|
||||
private async generateZkProof(data: {
|
||||
walletId: string;
|
||||
transactionAmount: string;
|
||||
destinationWalletId: string;
|
||||
complianceChecks: {
|
||||
amlClear: boolean;
|
||||
sanctionsClear: boolean;
|
||||
policyCompliant: boolean;
|
||||
};
|
||||
}): Promise<string> {
|
||||
// In production, use actual ZK library
|
||||
return JSON.stringify({
|
||||
type: 'zkCP',
|
||||
walletId: data.walletId,
|
||||
compliant: data.complianceChecks.amlClear && data.complianceChecks.sanctionsClear && data.complianceChecks.policyCompliant,
|
||||
proof: 'placeholder_zk_proof',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify ZK-Compliance Proof
|
||||
*/
|
||||
async verifyComplianceProof(proofId: string): Promise<boolean> {
|
||||
const proof = await prisma.zkProof.findUnique({
|
||||
where: { proofId },
|
||||
});
|
||||
|
||||
if (!proof || proof.proofType !== 'zkCP') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify proof
|
||||
const publicInputs = proof.publicInputs as unknown as {
|
||||
compliant?: boolean;
|
||||
amlClear?: boolean;
|
||||
sanctionsClear?: boolean;
|
||||
policyCompliant?: boolean;
|
||||
};
|
||||
|
||||
const isValid =
|
||||
publicInputs.compliant === true &&
|
||||
publicInputs.amlClear === true &&
|
||||
publicInputs.sanctionsClear === true &&
|
||||
publicInputs.policyCompliant === true;
|
||||
|
||||
if (isValid) {
|
||||
await prisma.zkProof.update({
|
||||
where: { proofId },
|
||||
data: {
|
||||
status: 'verified',
|
||||
verifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
export const zkComplianceProofService = new ZkComplianceProofService();
|
||||
|
||||
173
src/core/cbdc/zk-validation/zk-identity-proof.service.ts
Normal file
173
src/core/cbdc/zk-validation/zk-identity-proof.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
// ZK-CBDC Validation: Mode 3 - ZK-Identity Proofs (zkIP)
|
||||
// Wallet ownership verification without identity disclosure
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { gbigService } from '@/core/identity/gbig/gbig.service';
|
||||
|
||||
|
||||
export interface ZkIdentityProofRequest {
|
||||
walletId: string;
|
||||
identityClaim: string; // Claim to prove (e.g., "wallet belongs to verified entity")
|
||||
}
|
||||
|
||||
export interface ZkIdentityProof {
|
||||
proofId: string;
|
||||
proofData: string;
|
||||
publicInputs: {
|
||||
walletId: string;
|
||||
verified: boolean; // Only this is revealed, not the identity
|
||||
kycLevel: number; // Can reveal KYC level without revealing identity
|
||||
};
|
||||
}
|
||||
|
||||
export class ZkIdentityProofService {
|
||||
/**
|
||||
* Generate ZK-Identity Proof
|
||||
* Proves wallet belongs to verified entity without revealing identity
|
||||
*/
|
||||
async generateIdentityProof(request: ZkIdentityProofRequest): Promise<ZkIdentityProof> {
|
||||
const proofId = `ZKIP-${uuidv4()}`;
|
||||
|
||||
// Get identity information from GBIG (in production, this would be done securely)
|
||||
const identityInfo = await this.getIdentityInfo(request.walletId);
|
||||
|
||||
if (!identityInfo || !identityInfo.verified) {
|
||||
throw new Error('Wallet identity not verified');
|
||||
}
|
||||
|
||||
// Generate ZK proof
|
||||
const proofData = await this.generateZkProof({
|
||||
walletId: request.walletId,
|
||||
identityId: identityInfo.identityId,
|
||||
kycLevel: identityInfo.kycLevel,
|
||||
verified: identityInfo.verified,
|
||||
});
|
||||
|
||||
// Create proof record
|
||||
await prisma.zkProof.create({
|
||||
data: {
|
||||
proofId,
|
||||
walletId: request.walletId,
|
||||
proofType: 'zkIP',
|
||||
proofData,
|
||||
publicInputs: {
|
||||
walletId: request.walletId,
|
||||
verified: true,
|
||||
kycLevel: identityInfo.kycLevel,
|
||||
} as unknown as Record<string, unknown>,
|
||||
verificationKey: 'default_zkip_vk',
|
||||
status: 'verified',
|
||||
verifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('ZK-CBDC: Generated ZK-Identity Proof', {
|
||||
proofId,
|
||||
walletId: request.walletId,
|
||||
verified: true,
|
||||
});
|
||||
|
||||
return {
|
||||
proofId,
|
||||
proofData,
|
||||
publicInputs: {
|
||||
walletId: request.walletId,
|
||||
verified: true,
|
||||
kycLevel: identityInfo.kycLevel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identity information (internal)
|
||||
*/
|
||||
private async getIdentityInfo(walletId: string): Promise<{
|
||||
identityId: string;
|
||||
verified: boolean;
|
||||
kycLevel: number;
|
||||
} | null> {
|
||||
// Query GBIG for wallet identity
|
||||
// In production, this would use GBIG service
|
||||
// For now, simplified lookup
|
||||
try {
|
||||
// Simplified: assume wallet has identity if it exists in accounts
|
||||
const account = await prisma.bankAccount.findFirst({
|
||||
where: {
|
||||
accountNumber: walletId,
|
||||
assetType: 'cbdc',
|
||||
},
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// In production, would query GBIG for actual identity
|
||||
return {
|
||||
identityId: `IDENTITY-${walletId}`,
|
||||
verified: true,
|
||||
kycLevel: 2, // Default KYC level
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get identity info', { error, walletId });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ZK proof (placeholder)
|
||||
*/
|
||||
private async generateZkProof(data: {
|
||||
walletId: string;
|
||||
identityId: string;
|
||||
kycLevel: number;
|
||||
verified: boolean;
|
||||
}): Promise<string> {
|
||||
// In production, use actual ZK library
|
||||
return JSON.stringify({
|
||||
type: 'zkIP',
|
||||
walletId: data.walletId,
|
||||
verified: data.verified,
|
||||
kycLevel: data.kycLevel,
|
||||
proof: 'placeholder_zk_proof',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify ZK-Identity Proof
|
||||
*/
|
||||
async verifyIdentityProof(proofId: string): Promise<boolean> {
|
||||
const proof = await prisma.zkProof.findUnique({
|
||||
where: { proofId },
|
||||
});
|
||||
|
||||
if (!proof || proof.proofType !== 'zkIP') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify proof
|
||||
const publicInputs = proof.publicInputs as unknown as {
|
||||
verified?: boolean;
|
||||
kycLevel?: number;
|
||||
};
|
||||
|
||||
const isValid = publicInputs.verified === true && publicInputs.kycLevel !== undefined;
|
||||
|
||||
if (isValid) {
|
||||
await prisma.zkProof.update({
|
||||
where: { proofId },
|
||||
data: {
|
||||
status: 'verified',
|
||||
verifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
export const zkIdentityProofService = new ZkIdentityProofService();
|
||||
|
||||
170
src/core/cbdc/zk-validation/zk-verification.service.ts
Normal file
170
src/core/cbdc/zk-validation/zk-verification.service.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// ZK-CBDC Verification Service
|
||||
// Smart contract verification: if zkBP && zkCP && zkIP: execute_CBDC_transfer()
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { logger } from '@/infrastructure/monitoring/logger';
|
||||
import { zkBalanceProofService } from './zk-balance-proof.service';
|
||||
import { zkComplianceProofService } from './zk-compliance-proof.service';
|
||||
import { zkIdentityProofService } from './zk-identity-proof.service';
|
||||
|
||||
|
||||
export interface ZkVerificationRequest {
|
||||
walletId: string;
|
||||
transactionAmount: string;
|
||||
destinationWalletId: string;
|
||||
currencyCode: string;
|
||||
contractId?: string;
|
||||
}
|
||||
|
||||
export interface ZkVerificationResult {
|
||||
verificationId: string;
|
||||
zkbpResult: boolean;
|
||||
zkcpResult: boolean;
|
||||
zkipResult: boolean;
|
||||
overallResult: boolean;
|
||||
canExecute: boolean;
|
||||
}
|
||||
|
||||
export class ZkVerificationService {
|
||||
/**
|
||||
* Verify all ZK proofs for CBDC transfer
|
||||
* if zkBP && zkCP && zkIP: execute_CBDC_transfer()
|
||||
*/
|
||||
async verifyCbdcTransfer(request: ZkVerificationRequest): Promise<ZkVerificationResult> {
|
||||
const verificationId = `ZK-VERIFY-${uuidv4()}`;
|
||||
|
||||
try {
|
||||
// Generate and verify ZK-Balance Proof
|
||||
const zkbpProof = await zkBalanceProofService.generateBalanceProof({
|
||||
walletId: request.walletId,
|
||||
requiredAmount: request.transactionAmount,
|
||||
currencyCode: request.currencyCode,
|
||||
});
|
||||
const zkbpResult = await zkBalanceProofService.verifyBalanceProof(zkbpProof.proofId);
|
||||
|
||||
// Generate and verify ZK-Compliance Proof
|
||||
const zkcpProof = await zkComplianceProofService.generateComplianceProof({
|
||||
walletId: request.walletId,
|
||||
transactionAmount: request.transactionAmount,
|
||||
destinationWalletId: request.destinationWalletId,
|
||||
currencyCode: request.currencyCode,
|
||||
});
|
||||
const zkcpResult = await zkComplianceProofService.verifyComplianceProof(zkcpProof.proofId);
|
||||
|
||||
// Generate and verify ZK-Identity Proof
|
||||
const zkipProof = await zkIdentityProofService.generateIdentityProof({
|
||||
walletId: request.walletId,
|
||||
identityClaim: 'wallet belongs to verified entity',
|
||||
});
|
||||
const zkipResult = await zkIdentityProofService.verifyIdentityProof(zkipProof.proofId);
|
||||
|
||||
// Overall result: all proofs must be valid
|
||||
const overallResult = zkbpResult && zkcpResult && zkipResult;
|
||||
|
||||
// Create verification record
|
||||
await prisma.zkVerification.create({
|
||||
data: {
|
||||
verificationId,
|
||||
proofId: zkbpProof.proofId, // Reference to one of the proofs
|
||||
contractId: request.contractId || null,
|
||||
verificationType: 'combined',
|
||||
zkbpResult,
|
||||
zkcpResult,
|
||||
zkipResult,
|
||||
overallResult,
|
||||
status: overallResult ? 'verified' : 'rejected',
|
||||
verifiedAt: overallResult ? new Date() : null,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('ZK-CBDC: Verification completed', {
|
||||
verificationId,
|
||||
zkbpResult,
|
||||
zkcpResult,
|
||||
zkipResult,
|
||||
overallResult,
|
||||
});
|
||||
|
||||
return {
|
||||
verificationId,
|
||||
zkbpResult,
|
||||
zkcpResult,
|
||||
zkipResult,
|
||||
overallResult,
|
||||
canExecute: overallResult,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('ZK-CBDC: Verification failed', { error, request });
|
||||
|
||||
// Create failed verification record
|
||||
await prisma.zkVerification.create({
|
||||
data: {
|
||||
verificationId,
|
||||
proofId: '',
|
||||
contractId: request.contractId || null,
|
||||
verificationType: 'combined',
|
||||
zkbpResult: false,
|
||||
zkcpResult: false,
|
||||
zkipResult: false,
|
||||
overallResult: false,
|
||||
status: 'rejected',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
verificationId,
|
||||
zkbpResult: false,
|
||||
zkcpResult: false,
|
||||
zkipResult: false,
|
||||
overallResult: false,
|
||||
canExecute: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute CBDC transfer if verification passes
|
||||
*/
|
||||
async executeCbdcTransferIfVerified(
|
||||
verificationId: string,
|
||||
transferRequest: {
|
||||
sourceWalletId: string;
|
||||
destinationWalletId: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
): Promise<{ executed: boolean; transactionId?: string }> {
|
||||
const verification = await prisma.zkVerification.findUnique({
|
||||
where: { verificationId },
|
||||
});
|
||||
|
||||
if (!verification || !verification.overallResult) {
|
||||
logger.warn('ZK-CBDC: Transfer not executed - verification failed', { verificationId });
|
||||
return { executed: false };
|
||||
}
|
||||
|
||||
// Execute transfer (in production, would call CBDC transaction service)
|
||||
try {
|
||||
// Simplified transfer execution
|
||||
const transactionId = `CBDC-TX-${Date.now()}`;
|
||||
|
||||
logger.info('ZK-CBDC: Executing CBDC transfer', {
|
||||
verificationId,
|
||||
transactionId,
|
||||
transferRequest,
|
||||
});
|
||||
|
||||
// In production, would actually execute the transfer
|
||||
return {
|
||||
executed: true,
|
||||
transactionId,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('ZK-CBDC: Transfer execution failed', { error, verificationId });
|
||||
return { executed: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const zkVerificationService = new ZkVerificationService();
|
||||
|
||||
44
src/core/clearing/clearing.service.ts
Normal file
44
src/core/clearing/clearing.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Clearing & Settlement Module
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { ledgerService } from '@/core/ledger/ledger.service';
|
||||
import { paymentService } from '@/core/payments/payment.service';
|
||||
import { AssetType, PaymentRequest, PaymentPriority } from '@/shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export class ClearingService {
|
||||
/**
|
||||
* Process clearing through DBIS Clearing House (DCH)
|
||||
*/
|
||||
async processClearing(request: PaymentRequest): Promise<string> {
|
||||
// Process payment through clearing house
|
||||
const paymentStatus = await paymentService.initiatePayment(request);
|
||||
|
||||
// Apply multi-tier netting if applicable
|
||||
if (request.priority === PaymentPriority.DNS) {
|
||||
await this.applyNetting(request);
|
||||
}
|
||||
|
||||
return paymentStatus.paymentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply multi-tier netting for DNS payments
|
||||
*/
|
||||
private async applyNetting(request: PaymentRequest): Promise<void> {
|
||||
// In production, this would aggregate multiple payments and net them
|
||||
// For now, simplified implementation
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce settlement finality
|
||||
*/
|
||||
async enforceSettlementFinality(paymentId: string): Promise<boolean> {
|
||||
const paymentStatus = await paymentService.getPaymentStatus(paymentId);
|
||||
return paymentStatus.status === 'settled';
|
||||
}
|
||||
}
|
||||
|
||||
export const clearingService = new ClearingService();
|
||||
|
||||
147
src/core/collateral/mace/mace-allocation.service.ts
Normal file
147
src/core/collateral/mace/mace-allocation.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// MACE Allocation Service
|
||||
// Optimal collateral allocation logic
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { maceOptimizationService } from './mace-optimization.service';
|
||||
|
||||
|
||||
export interface AllocationRequest {
|
||||
requiredAmount: string;
|
||||
currencyCode: string;
|
||||
assetTypes: string[]; // fiat, cbdc, commodity, security, ssu
|
||||
targetBankId?: string;
|
||||
}
|
||||
|
||||
export interface AllocationResult {
|
||||
collateralId: string;
|
||||
allocations: Array<{
|
||||
assetType: string;
|
||||
amount: string;
|
||||
valuation: string;
|
||||
}>;
|
||||
totalValuation: string;
|
||||
}
|
||||
|
||||
export class MaceAllocationService {
|
||||
/**
|
||||
* Allocate multi-asset collateral
|
||||
*/
|
||||
async allocateCollateral(
|
||||
request: AllocationRequest
|
||||
): Promise<AllocationResult> {
|
||||
// Get available assets
|
||||
const availableAssets = await this.getAvailableAssets(
|
||||
request.assetTypes,
|
||||
request.targetBankId
|
||||
);
|
||||
|
||||
// Optimize allocation
|
||||
const optimization = await maceOptimizationService.optimizeAllocation({
|
||||
requiredAmount: request.requiredAmount,
|
||||
currencyCode: request.currencyCode,
|
||||
availableAssets,
|
||||
targetBankId: request.targetBankId,
|
||||
});
|
||||
|
||||
// Create collateral allocations
|
||||
const collateralId = `COLL-${uuidv4()}`;
|
||||
const allocations = [];
|
||||
|
||||
for (const allocation of optimization.optimalAllocation) {
|
||||
const valuation = await this.valuateAsset(
|
||||
allocation.assetType,
|
||||
allocation.amount,
|
||||
request.currencyCode
|
||||
);
|
||||
|
||||
await prisma.multiAssetCollateral.create({
|
||||
data: {
|
||||
collateralId: `${collateralId}-${allocation.assetType}`,
|
||||
assetType: allocation.assetType,
|
||||
assetId: allocation.assetId,
|
||||
amount: new Decimal(allocation.amount),
|
||||
valuation,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
allocations.push({
|
||||
assetType: allocation.assetType,
|
||||
amount: allocation.amount,
|
||||
valuation: valuation.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total valuation
|
||||
const totalValuation = allocations.reduce(
|
||||
(sum, alloc) => sum.plus(new Decimal(alloc.valuation)),
|
||||
new Decimal(0)
|
||||
);
|
||||
|
||||
// Apply optimization
|
||||
await maceOptimizationService.applyOptimization(optimization.optimizationId);
|
||||
|
||||
return {
|
||||
collateralId,
|
||||
allocations,
|
||||
totalValuation: totalValuation.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available assets
|
||||
*/
|
||||
private async getAvailableAssets(
|
||||
assetTypes: string[],
|
||||
targetBankId?: string
|
||||
): Promise<Array<{ assetType: string; assetId?: string; amount: string }>> {
|
||||
// In production, would query actual asset holdings
|
||||
// For now, return mock data
|
||||
const assets: Array<{ assetType: string; assetId?: string; amount: string }> = [];
|
||||
|
||||
for (const assetType of assetTypes) {
|
||||
assets.push({
|
||||
assetType,
|
||||
amount: '1000000', // Mock amount
|
||||
});
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valuate asset
|
||||
*/
|
||||
private async valuateAsset(
|
||||
assetType: string,
|
||||
amount: string,
|
||||
targetCurrency: string
|
||||
): Promise<Decimal> {
|
||||
// In production, would use actual valuation service
|
||||
// For now, return amount as valuation (1:1)
|
||||
return new Decimal(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release collateral
|
||||
*/
|
||||
async releaseCollateral(collateralId: string): Promise<void> {
|
||||
await prisma.multiAssetCollateral.updateMany({
|
||||
where: {
|
||||
collateralId: {
|
||||
startsWith: collateralId,
|
||||
},
|
||||
status: 'active',
|
||||
},
|
||||
data: {
|
||||
status: 'released',
|
||||
releasedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const maceAllocationService = new MaceAllocationService();
|
||||
|
||||
105
src/core/collateral/mace/mace-monitoring.service.ts
Normal file
105
src/core/collateral/mace/mace-monitoring.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
// MACE Monitoring Service
|
||||
// Real-time collateral monitoring
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
|
||||
export interface MonitoringRequest {
|
||||
collateralId: string;
|
||||
}
|
||||
|
||||
export interface MonitoringResult {
|
||||
collateralId: string;
|
||||
totalValuation: string;
|
||||
haircutAdjustedValue: string;
|
||||
status: string;
|
||||
alerts: string[];
|
||||
}
|
||||
|
||||
export class MaceMonitoringService {
|
||||
/**
|
||||
* Monitor collateral
|
||||
*/
|
||||
async monitorCollateral(
|
||||
request: MonitoringRequest
|
||||
): Promise<MonitoringResult> {
|
||||
const collaterals = await prisma.multiAssetCollateral.findMany({
|
||||
where: {
|
||||
collateralId: {
|
||||
startsWith: request.collateralId,
|
||||
},
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
if (collaterals.length === 0) {
|
||||
throw new Error(`Collateral not found: ${request.collateralId}`);
|
||||
}
|
||||
|
||||
// Calculate total valuation
|
||||
const totalValuation = collaterals.reduce(
|
||||
(sum, coll) => sum.plus(coll.valuation),
|
||||
new Decimal(0)
|
||||
);
|
||||
|
||||
// Calculate haircut-adjusted value
|
||||
let haircutAdjustedValue = new Decimal(0);
|
||||
const alerts: string[] = [];
|
||||
|
||||
for (const coll of collaterals) {
|
||||
const haircut = await this.getHaircut(coll.assetType);
|
||||
const adjustedValue = coll.valuation.mul(new Decimal(100).minus(haircut)).div(100);
|
||||
haircutAdjustedValue = haircutAdjustedValue.plus(adjustedValue);
|
||||
|
||||
// Check for alerts
|
||||
if (coll.valuation.lessThan(new Decimal(1000))) {
|
||||
alerts.push(`Low valuation for ${coll.assetType}: ${coll.valuation.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check overall status
|
||||
let status = 'healthy';
|
||||
if (haircutAdjustedValue.lessThan(totalValuation.mul(0.9))) {
|
||||
status = 'warning';
|
||||
alerts.push('Haircut-adjusted value below 90% of total valuation');
|
||||
}
|
||||
|
||||
return {
|
||||
collateralId: request.collateralId,
|
||||
totalValuation: totalValuation.toString(),
|
||||
haircutAdjustedValue: haircutAdjustedValue.toString(),
|
||||
status,
|
||||
alerts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get haircut
|
||||
*/
|
||||
private async getHaircut(assetType: string): Promise<Decimal> {
|
||||
const haircut = await prisma.collateralHaircut.findFirst({
|
||||
where: {
|
||||
assetType,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return haircut ? haircut.haircutRate : new Decimal(5); // Default 5%
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active collaterals
|
||||
*/
|
||||
async getActiveCollaterals(): Promise<any[]> {
|
||||
const collaterals = await prisma.multiAssetCollateral.findMany({
|
||||
where: { status: 'active' },
|
||||
orderBy: { allocatedAt: 'desc' },
|
||||
});
|
||||
|
||||
return collaterals;
|
||||
}
|
||||
}
|
||||
|
||||
export const maceMonitoringService = new MaceMonitoringService();
|
||||
|
||||
246
src/core/collateral/mace/mace-optimization.service.ts
Normal file
246
src/core/collateral/mace/mace-optimization.service.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
// MACE Optimization Service
|
||||
// Optimal collateral allocation: argmin(haircuts + fx_cost + liquidity_weight + risk_penalty(SRI))
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sriCalculatorService } from '@/core/risk/sri/sri-calculator.service';
|
||||
|
||||
|
||||
export interface OptimizationRequest {
|
||||
requiredAmount: string;
|
||||
currencyCode: string;
|
||||
availableAssets: Array<{
|
||||
assetType: string;
|
||||
assetId?: string;
|
||||
amount: string;
|
||||
}>;
|
||||
targetBankId?: string;
|
||||
}
|
||||
|
||||
export interface OptimizationResult {
|
||||
optimizationId: string;
|
||||
optimalAllocation: Array<{
|
||||
assetType: string;
|
||||
assetId?: string;
|
||||
amount: string;
|
||||
cost: string;
|
||||
}>;
|
||||
totalCost: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class MaceOptimizationService {
|
||||
/**
|
||||
* Calculate optimal collateral allocation
|
||||
* argmin(haircuts + fx_cost + liquidity_weight + risk_penalty(SRI))
|
||||
*/
|
||||
async optimizeAllocation(
|
||||
request: OptimizationRequest
|
||||
): Promise<OptimizationResult> {
|
||||
const requiredAmount = new Decimal(request.requiredAmount);
|
||||
const optimizationId = `OPT-${uuidv4()}`;
|
||||
|
||||
// Get haircuts and liquidity weights for each asset type
|
||||
const assetCosts = await Promise.all(
|
||||
request.availableAssets.map(async (asset) => {
|
||||
const cost = await this.calculateAssetCost(
|
||||
asset.assetType,
|
||||
asset.amount,
|
||||
request.currencyCode,
|
||||
request.targetBankId
|
||||
);
|
||||
return {
|
||||
...asset,
|
||||
cost: cost.totalCost,
|
||||
haircut: cost.haircut,
|
||||
fxCost: cost.fxCost,
|
||||
liquidityWeight: cost.liquidityWeight,
|
||||
riskPenalty: cost.riskPenalty,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Sort by total cost (ascending)
|
||||
assetCosts.sort((a, b) => a.cost.comparedTo(b.cost));
|
||||
|
||||
// Greedy allocation: use cheapest assets first
|
||||
const optimalAllocation: Array<{
|
||||
assetType: string;
|
||||
assetId?: string;
|
||||
amount: string;
|
||||
cost: string;
|
||||
}> = [];
|
||||
|
||||
let remainingAmount = requiredAmount;
|
||||
let totalCost = new Decimal(0);
|
||||
|
||||
for (const asset of assetCosts) {
|
||||
if (remainingAmount.isZero()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const availableAmount = new Decimal(asset.amount);
|
||||
const allocationAmount = remainingAmount.lessThan(availableAmount)
|
||||
? remainingAmount
|
||||
: availableAmount;
|
||||
|
||||
const allocationCost = asset.cost.mul(allocationAmount).div(requiredAmount);
|
||||
|
||||
optimalAllocation.push({
|
||||
assetType: asset.assetType,
|
||||
assetId: asset.assetId,
|
||||
amount: allocationAmount.toString(),
|
||||
cost: allocationCost.toString(),
|
||||
});
|
||||
|
||||
totalCost = totalCost.plus(allocationCost);
|
||||
remainingAmount = remainingAmount.minus(allocationAmount);
|
||||
}
|
||||
|
||||
if (!remainingAmount.isZero()) {
|
||||
throw new Error('Insufficient collateral available');
|
||||
}
|
||||
|
||||
// Create optimization record
|
||||
const optimization = await prisma.collateralOptimization.create({
|
||||
data: {
|
||||
optimizationId,
|
||||
collateralId: uuidv4(), // Would link to actual collateral
|
||||
optimizationType: 'allocation',
|
||||
optimalAllocation: optimalAllocation,
|
||||
totalCost,
|
||||
calculationMethod: 'argmin',
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
optimizationId: optimization.optimizationId,
|
||||
optimalAllocation,
|
||||
totalCost: totalCost.toString(),
|
||||
status: optimization.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate asset cost
|
||||
*/
|
||||
private async calculateAssetCost(
|
||||
assetType: string,
|
||||
amount: string,
|
||||
targetCurrency: string,
|
||||
targetBankId?: string
|
||||
): Promise<{
|
||||
totalCost: Decimal;
|
||||
haircut: Decimal;
|
||||
fxCost: Decimal;
|
||||
liquidityWeight: Decimal;
|
||||
riskPenalty: Decimal;
|
||||
}> {
|
||||
// Get haircut
|
||||
const haircut = await this.getHaircut(assetType);
|
||||
const haircutCost = new Decimal(amount).mul(haircut).div(100);
|
||||
|
||||
// Get FX cost (if asset currency differs from target)
|
||||
const fxCost = await this.getFxCost(assetType, targetCurrency);
|
||||
const fxCostAmount = new Decimal(amount).mul(fxCost).div(100);
|
||||
|
||||
// Get liquidity weight
|
||||
const liquidityWeight = await this.getLiquidityWeight(assetType);
|
||||
const liquidityCost = new Decimal(amount).mul(liquidityWeight).div(100);
|
||||
|
||||
// Get risk penalty (SRI-based)
|
||||
const riskPenalty = targetBankId
|
||||
? await this.getRiskPenalty(targetBankId)
|
||||
: new Decimal(0);
|
||||
const riskCost = new Decimal(amount).mul(riskPenalty).div(100);
|
||||
|
||||
// Total cost = haircuts + fx_cost + liquidity_weight + risk_penalty
|
||||
const totalCost = haircutCost.plus(fxCostAmount).plus(liquidityCost).plus(riskCost);
|
||||
|
||||
return {
|
||||
totalCost,
|
||||
haircut,
|
||||
fxCost,
|
||||
liquidityWeight,
|
||||
riskPenalty,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get haircut for asset type
|
||||
*/
|
||||
private async getHaircut(assetType: string): Promise<Decimal> {
|
||||
const haircut = await prisma.collateralHaircut.findFirst({
|
||||
where: {
|
||||
assetType,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return haircut ? haircut.haircutRate : new Decimal(5); // Default 5%
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FX cost
|
||||
*/
|
||||
private async getFxCost(assetType: string, targetCurrency: string): Promise<Decimal> {
|
||||
// In production, would calculate actual FX conversion cost
|
||||
// For now, return minimal cost
|
||||
return new Decimal(0.1); // 0.1%
|
||||
}
|
||||
|
||||
/**
|
||||
* Get liquidity weight
|
||||
*/
|
||||
private async getLiquidityWeight(assetType: string): Promise<Decimal> {
|
||||
const liquidity = await prisma.collateralLiquidity.findFirst({
|
||||
where: {
|
||||
assetType,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return liquidity ? liquidity.liquidityWeight : new Decimal(1); // Default 1%
|
||||
}
|
||||
|
||||
/**
|
||||
* Get risk penalty (SRI-based)
|
||||
*/
|
||||
private async getRiskPenalty(sovereignBankId: string): Promise<Decimal> {
|
||||
const sri = await prisma.sovereignRiskIndex.findFirst({
|
||||
where: {
|
||||
sovereignBankId,
|
||||
status: 'active',
|
||||
},
|
||||
orderBy: { calculatedAt: 'desc' },
|
||||
});
|
||||
|
||||
if (!sri) {
|
||||
return new Decimal(0);
|
||||
}
|
||||
|
||||
// Convert SRI score (0-100) to penalty (0-5%)
|
||||
const sriScore = parseFloat(sri.sriScore.toString());
|
||||
const penalty = new Decimal(sriScore).div(100).mul(5); // Max 5% penalty
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply optimization
|
||||
*/
|
||||
async applyOptimization(optimizationId: string): Promise<void> {
|
||||
await prisma.collateralOptimization.update({
|
||||
where: { optimizationId },
|
||||
data: {
|
||||
status: 'applied',
|
||||
appliedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const maceOptimizationService = new MaceOptimizationService();
|
||||
|
||||
140
src/core/collateral/mace/mace-valuation.service.ts
Normal file
140
src/core/collateral/mace/mace-valuation.service.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// MACE Valuation Service
|
||||
// Multi-asset valuation
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
|
||||
|
||||
export interface ValuationRequest {
|
||||
assetType: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface ValuationResult {
|
||||
valuation: string;
|
||||
currencyCode: string;
|
||||
fxRate?: string;
|
||||
}
|
||||
|
||||
export class MaceValuationService {
|
||||
/**
|
||||
* Valuate asset
|
||||
*/
|
||||
async valuateAsset(request: ValuationRequest): Promise<ValuationResult> {
|
||||
switch (request.assetType) {
|
||||
case 'fiat':
|
||||
return this.valuateFiat(request);
|
||||
case 'cbdc':
|
||||
return this.valuateCBDC(request);
|
||||
case 'commodity':
|
||||
return this.valuateCommodity(request);
|
||||
case 'security':
|
||||
return this.valuateSecurity(request);
|
||||
case 'ssu':
|
||||
return this.valuateSSU(request);
|
||||
default:
|
||||
throw new Error(`Unsupported asset type: ${request.assetType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valuate fiat
|
||||
*/
|
||||
private async valuateFiat(request: ValuationRequest): Promise<ValuationResult> {
|
||||
// Fiat is already in currency, return as-is
|
||||
return {
|
||||
valuation: request.amount,
|
||||
currencyCode: request.currencyCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valuate CBDC
|
||||
*/
|
||||
private async valuateCBDC(request: ValuationRequest): Promise<ValuationResult> {
|
||||
// CBDC is 1:1 with fiat, return as-is
|
||||
return {
|
||||
valuation: request.amount,
|
||||
currencyCode: request.currencyCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valuate commodity
|
||||
*/
|
||||
private async valuateCommodity(request: ValuationRequest): Promise<ValuationResult> {
|
||||
// Get commodity price
|
||||
const commodity = await prisma.commodity.findFirst({
|
||||
where: {
|
||||
commodityType: request.currencyCode, // Using currencyCode as commodity type
|
||||
},
|
||||
});
|
||||
|
||||
if (!commodity) {
|
||||
throw new Error(`Commodity not found: ${request.currencyCode}`);
|
||||
}
|
||||
|
||||
const amount = new Decimal(request.amount);
|
||||
const valuation = amount.mul(commodity.spotPrice);
|
||||
|
||||
return {
|
||||
valuation: valuation.toString(),
|
||||
currencyCode: 'USD', // Commodities typically valued in USD
|
||||
fxRate: commodity.spotPrice.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valuate security
|
||||
*/
|
||||
private async valuateSecurity(request: ValuationRequest): Promise<ValuationResult> {
|
||||
// Get security price
|
||||
const security = await prisma.securitiesSubLedger.findFirst({
|
||||
where: {
|
||||
securityId: request.currencyCode, // Using currencyCode as security ID
|
||||
},
|
||||
});
|
||||
|
||||
if (!security || !security.price) {
|
||||
throw new Error(`Security not found or no price: ${request.currencyCode}`);
|
||||
}
|
||||
|
||||
const amount = new Decimal(request.amount);
|
||||
const valuation = amount.mul(security.price);
|
||||
|
||||
return {
|
||||
valuation: valuation.toString(),
|
||||
currencyCode: request.currencyCode,
|
||||
fxRate: security.price.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valuate SSU
|
||||
*/
|
||||
private async valuateSSU(request: ValuationRequest): Promise<ValuationResult> {
|
||||
// Get SSU conversion rate
|
||||
const ssu = await prisma.syntheticSettlementUnit.findFirst({
|
||||
where: {
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
if (!ssu || !ssu.conversionRate) {
|
||||
throw new Error('SSU not found or no conversion rate');
|
||||
}
|
||||
|
||||
const amount = new Decimal(request.amount);
|
||||
const valuation = amount.mul(ssu.conversionRate);
|
||||
|
||||
return {
|
||||
valuation: valuation.toString(),
|
||||
currencyCode: request.currencyCode,
|
||||
fxRate: ssu.conversionRate.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const maceValuationService = new MaceValuationService();
|
||||
|
||||
90
src/core/commodities/cbds/cbds.routes.ts
Normal file
90
src/core/commodities/cbds/cbds.routes.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// CBDS API Routes
|
||||
|
||||
import { Router } from 'express';
|
||||
import { cdtService } from './cdt-service';
|
||||
import { reserveCertificateService } from './reserve-certificate.service';
|
||||
import { cdtSettlementService } from './cdt-settlement.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cbds/cdt/mint:
|
||||
* post:
|
||||
* summary: Mint CDT from reserves
|
||||
*/
|
||||
router.post('/cdt/mint', async (req, res, next) => {
|
||||
try {
|
||||
const cdtId = await cdtService.mintCdt(req.body);
|
||||
res.json({ cdtId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cbds/cdt/burn:
|
||||
* post:
|
||||
* summary: Burn CDT
|
||||
*/
|
||||
router.post('/cdt/burn', async (req, res, next) => {
|
||||
try {
|
||||
const { cdtId, reason } = req.body;
|
||||
const result = await cdtService.burnCdt(cdtId, reason);
|
||||
res.json({ success: result });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cbds/cdt/:cdtId:
|
||||
* get:
|
||||
* summary: Get CDT details
|
||||
*/
|
||||
router.get('/cdt/:cdtId', async (req, res, next) => {
|
||||
try {
|
||||
const cdt = await cdtService.getCdt(req.params.cdtId);
|
||||
if (!cdt) {
|
||||
return res.status(404).json({ error: 'CDT not found' });
|
||||
}
|
||||
res.json(cdt);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cbds/reserve-certificate/create:
|
||||
* post:
|
||||
* summary: Create reserve certificate
|
||||
*/
|
||||
router.post('/reserve-certificate/create', async (req, res, next) => {
|
||||
try {
|
||||
const certificateId = await reserveCertificateService.createReserveCertificate(req.body);
|
||||
res.json({ certificateId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/cbds/settle:
|
||||
* post:
|
||||
* summary: Execute CDT settlement
|
||||
*/
|
||||
router.post('/settle', async (req, res, next) => {
|
||||
try {
|
||||
const transactionId = await cdtSettlementService.executeSettlement(req.body);
|
||||
res.json({ transactionId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
237
src/core/commodities/cbds/cdt-service.ts
Normal file
237
src/core/commodities/cbds/cdt-service.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
// CDT Service
|
||||
// CDT tokenization from physical reserves
|
||||
// Reserve certificate verification
|
||||
// CDT structure: {commodity_type, weight, reserve_certificate, custodian, sovereign_issuer, timestamp, signature}
|
||||
// CDT minting and burning
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { reserveCertificateService } from './reserve-certificate.service';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
||||
export interface CdtMintRequest {
|
||||
commodityType: string;
|
||||
weight: string;
|
||||
unit: string;
|
||||
reserveCertificateId: string;
|
||||
custodianId: string;
|
||||
sovereignIssuerId: string;
|
||||
}
|
||||
|
||||
export interface CdtStructure {
|
||||
commodityType: string;
|
||||
weight: Decimal;
|
||||
reserveCertificate: string;
|
||||
custodian: string;
|
||||
sovereignIssuer: string;
|
||||
timestamp: Date;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export class CdtService {
|
||||
/**
|
||||
* Mint CDT from reserves
|
||||
*/
|
||||
async mintCdt(request: CdtMintRequest): Promise<string> {
|
||||
// Verify reserve certificate
|
||||
const certificate = await reserveCertificateService.verifyCertificate(
|
||||
request.reserveCertificateId
|
||||
);
|
||||
|
||||
if (!certificate || certificate.verificationStatus !== 'verified') {
|
||||
throw new Error('Reserve certificate not verified');
|
||||
}
|
||||
|
||||
// Verify custodian
|
||||
const custodian = await prisma.commodityCustodian.findUnique({
|
||||
where: { custodianId: request.custodianId },
|
||||
});
|
||||
|
||||
if (!custodian || custodian.approvalStatus !== 'approved') {
|
||||
throw new Error('Custodian not approved');
|
||||
}
|
||||
|
||||
// Verify certificate belongs to custodian
|
||||
if (certificate.custodianId !== request.custodianId) {
|
||||
throw new Error('Reserve certificate does not belong to custodian');
|
||||
}
|
||||
|
||||
// Generate CDT ID
|
||||
const cdtId = `CDT-${request.commodityType}-${uuidv4()}`;
|
||||
|
||||
// Create CDT structure
|
||||
const cdtStructure = await this.createCdtStructure(request, cdtId);
|
||||
|
||||
// Create CDT
|
||||
const cdt = await prisma.commodityDigitalToken.create({
|
||||
data: {
|
||||
cdtId,
|
||||
commodityType: request.commodityType.toUpperCase(),
|
||||
weight: new Decimal(request.weight),
|
||||
unit: request.unit,
|
||||
reserveCertificateId: request.reserveCertificateId,
|
||||
custodianId: request.custodianId,
|
||||
sovereignIssuerId: request.sovereignIssuerId,
|
||||
timestamp: new Date(),
|
||||
signature: cdtStructure.signature,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
|
||||
return cdt.cdtId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CDT structure
|
||||
*/
|
||||
private async createCdtStructure(
|
||||
request: CdtMintRequest,
|
||||
cdtId: string
|
||||
): Promise<CdtStructure> {
|
||||
// Get certificate hash
|
||||
const certificate = await prisma.commodityReserveCertificate.findUnique({
|
||||
where: { certificateId: request.reserveCertificateId },
|
||||
});
|
||||
|
||||
if (!certificate) {
|
||||
throw new Error('Reserve certificate not found');
|
||||
}
|
||||
|
||||
// Create CDT structure
|
||||
const cdtData = {
|
||||
commodity_type: request.commodityType.toUpperCase(),
|
||||
weight: request.weight,
|
||||
reserve_certificate: certificate.certificateHash,
|
||||
custodian: request.custodianId,
|
||||
sovereign_issuer: request.sovereignIssuerId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Generate signature (in production, this would be HSM-signed)
|
||||
const signature = this.generateSignature(cdtData);
|
||||
|
||||
return {
|
||||
commodityType: request.commodityType.toUpperCase(),
|
||||
weight: new Decimal(request.weight),
|
||||
reserveCertificate: certificate.certificateHash,
|
||||
custodian: request.custodianId,
|
||||
sovereignIssuer: request.sovereignIssuerId,
|
||||
timestamp: new Date(),
|
||||
signature,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CDT signature
|
||||
*/
|
||||
private generateSignature(cdtData: Record<string, unknown>): string {
|
||||
const dataString = JSON.stringify(cdtData);
|
||||
return createHash('sha256').update(dataString).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Burn CDT
|
||||
*/
|
||||
async burnCdt(cdtId: string, reason?: string): Promise<boolean> {
|
||||
const cdt = await prisma.commodityDigitalToken.findUnique({
|
||||
where: { cdtId },
|
||||
});
|
||||
|
||||
if (!cdt || cdt.status !== 'active') {
|
||||
throw new Error('CDT not found or not active');
|
||||
}
|
||||
|
||||
// Create burn transaction
|
||||
await prisma.cdtTransaction.create({
|
||||
data: {
|
||||
transactionId: `CDT-TX-BURN-${uuidv4()}`,
|
||||
cdtId,
|
||||
transactionType: 'burn',
|
||||
amount: cdt.weight,
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Update CDT status
|
||||
await prisma.commodityDigitalToken.update({
|
||||
where: { cdtId },
|
||||
data: {
|
||||
status: 'burned',
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CDT by ID
|
||||
*/
|
||||
async getCdt(cdtId: string) {
|
||||
return await prisma.commodityDigitalToken.findUnique({
|
||||
where: { cdtId },
|
||||
include: {
|
||||
reserveCertificate: true,
|
||||
custodian: true,
|
||||
transactions: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List CDTs by commodity type
|
||||
*/
|
||||
async listCdts(
|
||||
commodityType?: string,
|
||||
status?: string,
|
||||
limit: number = 100
|
||||
) {
|
||||
return await prisma.commodityDigitalToken.findMany({
|
||||
where: {
|
||||
...(commodityType && { commodityType }),
|
||||
...(status && { status }),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify CDT structure
|
||||
*/
|
||||
async verifyCdtStructure(cdtId: string): Promise<boolean> {
|
||||
const cdt = await prisma.commodityDigitalToken.findUnique({
|
||||
where: { cdtId },
|
||||
include: {
|
||||
reserveCertificate: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!cdt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reconstruct CDT structure
|
||||
const cdtData = {
|
||||
commodity_type: cdt.commodityType,
|
||||
weight: cdt.weight.toString(),
|
||||
reserve_certificate: cdt.reserveCertificate.certificateHash,
|
||||
custodian: cdt.custodianId,
|
||||
sovereign_issuer: cdt.sovereignIssuerId,
|
||||
timestamp: cdt.timestamp.toISOString(),
|
||||
};
|
||||
|
||||
// Verify signature
|
||||
const dataString = JSON.stringify(cdtData);
|
||||
const expectedSignature = createHash('sha256').update(dataString).digest('hex');
|
||||
return cdt.signature === expectedSignature;
|
||||
}
|
||||
}
|
||||
|
||||
export const cdtService = new CdtService();
|
||||
|
||||
305
src/core/commodities/cbds/cdt-settlement.service.ts
Normal file
305
src/core/commodities/cbds/cdt-settlement.service.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
// CDT Settlement Service
|
||||
// SSU derivatives settlement
|
||||
// Direct exchange: CDT ↔ CBDC
|
||||
// Cross-commodity swaps
|
||||
// Multi-leg commodity settlements
|
||||
|
||||
import prisma from '@/shared/database/prisma';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
||||
export interface CdtSettlementRequest {
|
||||
cdtId: string;
|
||||
targetAssetType: string; // ssu, cbdc, commodity
|
||||
targetAssetId?: string;
|
||||
targetAmount?: string;
|
||||
sourceBankId?: string;
|
||||
destinationBankId?: string;
|
||||
}
|
||||
|
||||
export interface CrossCommoditySwapRequest {
|
||||
sourceCdtId: string;
|
||||
targetCommodityType: string;
|
||||
targetAmount: string;
|
||||
}
|
||||
|
||||
export class CdtSettlementService {
|
||||
/**
|
||||
* Execute CDT settlement
|
||||
*/
|
||||
async executeSettlement(
|
||||
request: CdtSettlementRequest
|
||||
): Promise<string> {
|
||||
const cdt = await prisma.commodityDigitalToken.findUnique({
|
||||
where: { cdtId: request.cdtId },
|
||||
});
|
||||
|
||||
if (!cdt || cdt.status !== 'active') {
|
||||
throw new Error('CDT not found or not active');
|
||||
}
|
||||
|
||||
let transactionType = 'transfer';
|
||||
|
||||
// Determine settlement type
|
||||
if (request.targetAssetType === 'ssu') {
|
||||
transactionType = 'ssu_derivative';
|
||||
return await this.settleSsuDerivative(request);
|
||||
} else if (request.targetAssetType === 'cbdc') {
|
||||
transactionType = 'exchange_cbdc';
|
||||
return await this.exchangeCdtForCbdc(request);
|
||||
} else if (request.targetAssetType === 'commodity') {
|
||||
transactionType = 'cross_commodity_swap';
|
||||
return await this.executeCrossCommoditySwap({
|
||||
sourceCdtId: request.cdtId,
|
||||
targetCommodityType: request.targetAssetId || '',
|
||||
targetAmount: request.targetAmount || cdt.weight.toString(),
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unsupported target asset type: ${request.targetAssetType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle SSU derivative
|
||||
*/
|
||||
private async settleSsuDerivative(
|
||||
request: CdtSettlementRequest
|
||||
): Promise<string> {
|
||||
const transactionId = `CDT-TX-SSU-${uuidv4()}`;
|
||||
|
||||
// Create transaction
|
||||
await prisma.cdtTransaction.create({
|
||||
data: {
|
||||
transactionId,
|
||||
cdtId: request.cdtId,
|
||||
transactionType: 'ssu_derivative',
|
||||
sourceBankId: request.sourceBankId || null,
|
||||
destinationBankId: request.destinationBankId || null,
|
||||
targetAssetType: 'ssu',
|
||||
targetAssetId: request.targetAssetId || null,
|
||||
amount: new Decimal(0), // Will be calculated based on SSU value
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// In production, this would:
|
||||
// 1. Calculate SSU equivalent value
|
||||
// 2. Execute SSU transfer
|
||||
// 3. Update balances
|
||||
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange CDT for CBDC
|
||||
*/
|
||||
private async exchangeCdtForCbdc(
|
||||
request: CdtSettlementRequest
|
||||
): Promise<string> {
|
||||
const cdt = await prisma.commodityDigitalToken.findUnique({
|
||||
where: { cdtId: request.cdtId },
|
||||
include: {
|
||||
reserveCertificate: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!cdt) {
|
||||
throw new Error('CDT not found');
|
||||
}
|
||||
|
||||
// Get commodity spot price
|
||||
const commodity = await prisma.commodity.findFirst({
|
||||
where: {
|
||||
commodityType: cdt.commodityType,
|
||||
unit: cdt.unit,
|
||||
},
|
||||
});
|
||||
|
||||
if (!commodity) {
|
||||
throw new Error('Commodity pricing not found');
|
||||
}
|
||||
|
||||
// Calculate CBDC amount (CDT weight * spot price)
|
||||
const cdtValue = cdt.weight.mul(commodity.spotPrice);
|
||||
const cbdcAmount = request.targetAmount
|
||||
? new Decimal(request.targetAmount)
|
||||
: cdtValue;
|
||||
|
||||
const transactionId = `CDT-TX-CBDC-${uuidv4()}`;
|
||||
|
||||
// Create transaction
|
||||
await prisma.cdtTransaction.create({
|
||||
data: {
|
||||
transactionId,
|
||||
cdtId: request.cdtId,
|
||||
transactionType: 'exchange_cbdc',
|
||||
sourceBankId: request.sourceBankId || null,
|
||||
destinationBankId: request.destinationBankId || null,
|
||||
targetAssetType: 'cbdc',
|
||||
targetAssetId: request.targetAssetId || null,
|
||||
amount: cbdcAmount,
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// In production, this would:
|
||||
// 1. Transfer CDT
|
||||
// 2. Mint/transfer CBDC
|
||||
// 3. Update balances
|
||||
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute cross-commodity swap
|
||||
*/
|
||||
async executeCrossCommoditySwap(
|
||||
request: CrossCommoditySwapRequest
|
||||
): Promise<string> {
|
||||
const sourceCdt = await prisma.commodityDigitalToken.findUnique({
|
||||
where: { cdtId: request.sourceCdtId },
|
||||
include: {
|
||||
reserveCertificate: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceCdt || sourceCdt.status !== 'active') {
|
||||
throw new Error('Source CDT not found or not active');
|
||||
}
|
||||
|
||||
// Get commodity prices
|
||||
const sourceCommodity = await prisma.commodity.findFirst({
|
||||
where: {
|
||||
commodityType: sourceCdt.commodityType,
|
||||
unit: sourceCdt.unit,
|
||||
},
|
||||
});
|
||||
|
||||
const targetCommodity = await prisma.commodity.findFirst({
|
||||
where: {
|
||||
commodityType: request.targetCommodityType.toUpperCase(),
|
||||
unit: sourceCdt.unit, // Assume same unit
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceCommodity || !targetCommodity) {
|
||||
throw new Error('Commodity pricing not found');
|
||||
}
|
||||
|
||||
// Calculate swap ratio
|
||||
const sourceValue = sourceCdt.weight.mul(sourceCommodity.spotPrice);
|
||||
const targetPrice = targetCommodity.spotPrice;
|
||||
const targetWeight = sourceValue.div(targetPrice);
|
||||
|
||||
const transactionId = `CDT-TX-SWAP-${uuidv4()}`;
|
||||
|
||||
// Create transaction
|
||||
await prisma.cdtTransaction.create({
|
||||
data: {
|
||||
transactionId,
|
||||
cdtId: request.sourceCdtId,
|
||||
transactionType: 'cross_commodity_swap',
|
||||
targetAssetType: 'commodity',
|
||||
targetAssetId: request.targetCommodityType.toUpperCase(),
|
||||
amount: targetWeight,
|
||||
status: 'completed',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// In production, this would:
|
||||
// 1. Burn source CDT
|
||||
// 2. Mint target CDT
|
||||
// 3. Update balances
|
||||
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multi-leg commodity settlement
|
||||
*/
|
||||
async executeMultiLegSettlement(
|
||||
legs: Array<{
|
||||
cdtId: string;
|
||||
targetAssetType: string;
|
||||
targetAssetId?: string;
|
||||
amount?: string;
|
||||
}>
|
||||
): Promise<string[]> {
|
||||
const transactionIds: string[] = [];
|
||||
|
||||
try {
|
||||
// Execute all legs
|
||||
for (const leg of legs) {
|
||||
const transactionId = await this.executeSettlement({
|
||||
cdtId: leg.cdtId,
|
||||
targetAssetType: leg.targetAssetType,
|
||||
targetAssetId: leg.targetAssetId,
|
||||
targetAmount: leg.amount,
|
||||
});
|
||||
|
||||
transactionIds.push(transactionId);
|
||||
}
|
||||
|
||||
return transactionIds;
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
for (const transactionId of transactionIds) {
|
||||
try {
|
||||
await prisma.cdtTransaction.update({
|
||||
where: { transactionId },
|
||||
data: {
|
||||
status: 'failed',
|
||||
},
|
||||
});
|
||||
} catch (rollbackError) {
|
||||
// Ignore rollback errors
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CDT transactions
|
||||
*/
|
||||
async getCdtTransactions(
|
||||
cdtId?: string,
|
||||
transactionType?: string,
|
||||
status?: string,
|
||||
limit: number = 100
|
||||
) {
|
||||
return await prisma.cdtTransaction.findMany({
|
||||
where: {
|
||||
...(cdtId && { cdtId }),
|
||||
...(transactionType && { transactionType }),
|
||||
...(status && { status }),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction by ID
|
||||
*/
|
||||
async getTransaction(transactionId: string) {
|
||||
return await prisma.cdtTransaction.findUnique({
|
||||
where: { transactionId },
|
||||
include: {
|
||||
cdt: {
|
||||
include: {
|
||||
reserveCertificate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cdtSettlementService = new CdtSettlementService();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user