Initial project setup: Add contracts, API definitions, tests, and documentation

- Add Foundry project configuration (foundry.toml, foundry.lock)
- Add Solidity contracts (TokenFactory138, BridgeVault138, ComplianceRegistry, etc.)
- Add API definitions (OpenAPI, GraphQL, gRPC, AsyncAPI)
- Add comprehensive test suite (unit, integration, fuzz, invariants)
- Add API services (REST, GraphQL, orchestrator, packet service)
- Add documentation (ISO20022 mapping, runbooks, adapter guides)
- Add development tools (RBC tool, Swagger UI, mock server)
- Update OpenZeppelin submodules to v5.0.0
This commit is contained in:
defiQUG
2025-12-12 10:59:41 -08:00
parent 26b5aaf932
commit 651ff4f7eb
281 changed files with 24813 additions and 2 deletions

View File

@@ -0,0 +1,47 @@
/**
* Bridge controllers
*/
import { Request, Response, NextFunction } from 'express';
import { bridgeService } from '../services/bridge-service';
export async function bridgeLock(req: Request, res: Response, next: NextFunction) {
try {
const lock = await bridgeService.lock(req.body);
res.status(201).json(lock);
} catch (error) {
next(error);
}
}
export async function bridgeUnlock(req: Request, res: Response, next: NextFunction) {
try {
const lock = await bridgeService.unlock(req.body);
res.status(201).json(lock);
} catch (error) {
next(error);
}
}
export async function getBridgeLock(req: Request, res: Response, next: NextFunction) {
try {
const { lockId } = req.params;
const lock = await bridgeService.getLockStatus(lockId);
if (!lock) {
return res.status(404).json({ code: 'NOT_FOUND', message: 'Lock not found' });
}
res.json(lock);
} catch (error) {
next(error);
}
}
export async function getBridgeCorridors(req: Request, res: Response, next: NextFunction) {
try {
const result = await bridgeService.getCorridors();
res.json(result);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,117 @@
/**
* Compliance controllers
*/
import { Request, Response, NextFunction } from 'express';
import { complianceService } from '../services/compliance-service';
export async function getComplianceProfile(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const profile = await complianceService.getProfile(accountRefId);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setCompliance(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const profile = await complianceService.setCompliance(accountRefId, req.body);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setFrozen(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const profile = await complianceService.setFrozen(accountRefId, req.body);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setTier(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const { tier } = req.body;
const profile = await complianceService.setTier(accountRefId, tier);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setJurisdictionHash(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const { jurisdictionHash } = req.body;
const profile = await complianceService.setJurisdictionHash(accountRefId, jurisdictionHash);
res.json(profile);
} catch (error) {
next(error);
}
}
// Wallet-specific endpoints
export async function getWalletComplianceProfile(req: Request, res: Response, next: NextFunction) {
try {
const { walletRefId } = req.params;
// In production, map wallet to account first
const profile = await complianceService.getProfile(walletRefId);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setWalletCompliance(req: Request, res: Response, next: NextFunction) {
try {
const { walletRefId } = req.params;
// In production, map wallet to account first
const profile = await complianceService.setCompliance(walletRefId, req.body);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setWalletFrozen(req: Request, res: Response, next: NextFunction) {
try {
const { walletRefId } = req.params;
// In production, map wallet to account first
const profile = await complianceService.setFrozen(walletRefId, req.body);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setWalletTier(req: Request, res: Response, next: NextFunction) {
try {
const { walletRefId } = req.params;
const { tier } = req.body;
// In production, map wallet to account first
const profile = await complianceService.setTier(walletRefId, tier);
res.json(profile);
} catch (error) {
next(error);
}
}
export async function setWalletJurisdictionHash(req: Request, res: Response, next: NextFunction) {
try {
const { walletRefId } = req.params;
const { jurisdictionHash } = req.body;
// In production, map wallet to account first
const profile = await complianceService.setJurisdictionHash(walletRefId, jurisdictionHash);
res.json(profile);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,25 @@
/**
* ISO-20022 controllers
*/
import { Request, Response, NextFunction } from 'express';
import { isoService } from '../services/iso-service';
export async function submitInboundMessage(req: Request, res: Response, next: NextFunction) {
try {
const result = await isoService.submitInboundMessage(req.body);
res.status(201).json(result);
} catch (error) {
next(error);
}
}
export async function submitOutboundMessage(req: Request, res: Response, next: NextFunction) {
try {
const result = await isoService.submitOutboundMessage(req.body);
res.status(201).json(result);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Lien controllers
*/
import { Request, Response, NextFunction } from 'express';
import { lienService } from '../services/lien-service';
export async function placeLien(req: Request, res: Response, next: NextFunction) {
try {
const lien = await lienService.placeLien(req.body);
res.status(201).json(lien);
} catch (error) {
next(error);
}
}
export async function listLiens(req: Request, res: Response, next: NextFunction) {
try {
const { debtor, active, limit, offset } = req.query;
const result = await lienService.listLiens({
debtor: debtor as string,
active: active === 'true' ? true : active === 'false' ? false : undefined,
limit: parseInt(limit as string) || 20,
offset: parseInt(offset as string) || 0,
});
res.json(result);
} catch (error) {
next(error);
}
}
export async function getLien(req: Request, res: Response, next: NextFunction) {
try {
const { lienId } = req.params;
const lien = await lienService.getLien(lienId);
if (!lien) {
return res.status(404).json({ code: 'NOT_FOUND', message: 'Lien not found' });
}
res.json(lien);
} catch (error) {
next(error);
}
}
export async function reduceLien(req: Request, res: Response, next: NextFunction) {
try {
const { lienId } = req.params;
const { reduceBy } = req.body;
const lien = await lienService.reduceLien(lienId, reduceBy);
res.json(lien);
} catch (error) {
next(error);
}
}
export async function releaseLien(req: Request, res: Response, next: NextFunction) {
try {
const { lienId } = req.params;
await lienService.releaseLien(lienId);
res.status(204).send();
} catch (error) {
next(error);
}
}
export async function getAccountLiens(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const liens = await lienService.getAccountLiens(accountRefId);
res.json({ liens });
} catch (error) {
next(error);
}
}
export async function getEncumbrance(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const result = await lienService.getEncumbrance(accountRefId);
res.json(result);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,65 @@
/**
* Mapping controllers
*/
import { Request, Response, NextFunction } from 'express';
import { mappingService } from '../services/mapping-service';
export async function linkAccountWallet(req: Request, res: Response, next: NextFunction) {
try {
await mappingService.linkAccountWallet(req.body);
res.status(201).json({ message: 'Account-wallet linked successfully' });
} catch (error) {
next(error);
}
}
export async function unlinkAccountWallet(req: Request, res: Response, next: NextFunction) {
try {
await mappingService.unlinkAccountWallet(req.body);
res.status(204).send();
} catch (error) {
next(error);
}
}
export async function getAccountWallets(req: Request, res: Response, next: NextFunction) {
try {
const { accountRefId } = req.params;
const wallets = await mappingService.getAccountWallets(accountRefId);
res.json({ wallets });
} catch (error) {
next(error);
}
}
export async function getWalletAccounts(req: Request, res: Response, next: NextFunction) {
try {
const { walletRefId } = req.params;
const accounts = await mappingService.getWalletAccounts(walletRefId);
res.json({ accounts });
} catch (error) {
next(error);
}
}
export async function connectProvider(req: Request, res: Response, next: NextFunction) {
try {
const { provider } = req.params;
const result = await mappingService.connectProvider(provider, req.body);
res.json(result);
} catch (error) {
next(error);
}
}
export async function getProviderStatus(req: Request, res: Response, next: NextFunction) {
try {
const { provider, connectionId } = req.params;
const result = await mappingService.getProviderStatus(provider, connectionId);
res.json(result);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,76 @@
/**
* Packet controllers
*/
import { Request, Response, NextFunction } from 'express';
import { packetService } from '../services/packet-service';
export async function generatePacket(req: Request, res: Response, next: NextFunction) {
try {
const packet = await packetService.generatePacket(req.body);
res.status(201).json(packet);
} catch (error) {
next(error);
}
}
export async function listPackets(req: Request, res: Response, next: NextFunction) {
try {
const { triggerId, status, limit, offset } = req.query;
const result = await packetService.listPackets({
triggerId: triggerId as string,
status: status as string,
limit: parseInt(limit as string) || 20,
offset: parseInt(offset as string) || 0,
});
res.json(result);
} catch (error) {
next(error);
}
}
export async function getPacket(req: Request, res: Response, next: NextFunction) {
try {
const { packetId } = req.params;
const packet = await packetService.getPacket(packetId);
if (!packet) {
return res.status(404).json({ code: 'NOT_FOUND', message: 'Packet not found' });
}
res.json(packet);
} catch (error) {
next(error);
}
}
export async function downloadPacket(req: Request, res: Response, next: NextFunction) {
try {
const { packetId } = req.params;
const file = await packetService.downloadPacket(packetId);
res.setHeader('Content-Type', file.contentType);
res.setHeader('Content-Disposition', `attachment; filename="${file.filename}"`);
res.send(file.content);
} catch (error) {
next(error);
}
}
export async function dispatchPacket(req: Request, res: Response, next: NextFunction) {
try {
const { packetId } = req.params;
const packet = await packetService.dispatchPacket({ packetId, ...req.body });
res.json(packet);
} catch (error) {
next(error);
}
}
export async function acknowledgePacket(req: Request, res: Response, next: NextFunction) {
try {
const { packetId } = req.params;
const packet = await packetService.acknowledgePacket(packetId, req.body);
res.json(packet);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,94 @@
/**
* Token controllers
*/
import { Request, Response, NextFunction } from 'express';
import { tokenService } from '../services/token-service';
export async function deployToken(req: Request, res: Response, next: NextFunction) {
try {
const token = await tokenService.deployToken(req.body);
res.status(201).json(token);
} catch (error) {
next(error);
}
}
export async function listTokens(req: Request, res: Response, next: NextFunction) {
try {
const { code, issuer, limit, offset } = req.query;
const result = await tokenService.listTokens({
code: code as string,
issuer: issuer as string,
limit: parseInt(limit as string) || 20,
offset: parseInt(offset as string) || 0,
});
res.json(result);
} catch (error) {
next(error);
}
}
export async function getToken(req: Request, res: Response, next: NextFunction) {
try {
const { code } = req.params;
const token = await tokenService.getToken(code);
if (!token) {
return res.status(404).json({ code: 'NOT_FOUND', message: 'Token not found' });
}
res.json(token);
} catch (error) {
next(error);
}
}
export async function updateTokenPolicy(req: Request, res: Response, next: NextFunction) {
try {
const { code } = req.params;
const token = await tokenService.updatePolicy(code, req.body);
res.json(token);
} catch (error) {
next(error);
}
}
export async function mintTokens(req: Request, res: Response, next: NextFunction) {
try {
const { code } = req.params;
const result = await tokenService.mint(code, req.body);
res.json(result);
} catch (error) {
next(error);
}
}
export async function burnTokens(req: Request, res: Response, next: NextFunction) {
try {
const { code } = req.params;
const result = await tokenService.burn(code, req.body);
res.json(result);
} catch (error) {
next(error);
}
}
export async function clawbackTokens(req: Request, res: Response, next: NextFunction) {
try {
const { code } = req.params;
const result = await tokenService.clawback(code, req.body);
res.json(result);
} catch (error) {
next(error);
}
}
export async function forceTransferTokens(req: Request, res: Response, next: NextFunction) {
try {
const { code } = req.params;
const result = await tokenService.forceTransfer(code, req.body);
res.json(result);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,78 @@
/**
* Trigger controllers
*/
import { Request, Response, NextFunction } from 'express';
import { triggerService } from '../services/trigger-service';
export async function listTriggers(req: Request, res: Response, next: NextFunction) {
try {
const { rail, state, accountRef, walletRef, limit, offset } = req.query;
const result = await triggerService.listTriggers({
rail: rail as string,
state: state as string,
accountRef: accountRef as string,
walletRef: walletRef as string,
limit: parseInt(limit as string) || 20,
offset: parseInt(offset as string) || 0,
});
res.json(result);
} catch (error) {
next(error);
}
}
export async function getTrigger(req: Request, res: Response, next: NextFunction) {
try {
const { triggerId } = req.params;
const trigger = await triggerService.getTrigger(triggerId);
if (!trigger) {
return res.status(404).json({ code: 'NOT_FOUND', message: 'Trigger not found' });
}
res.json(trigger);
} catch (error) {
next(error);
}
}
export async function validateAndLock(req: Request, res: Response, next: NextFunction) {
try {
const { triggerId } = req.params;
const trigger = await triggerService.validateAndLock(triggerId, req.body);
res.json(trigger);
} catch (error) {
next(error);
}
}
export async function markSubmitted(req: Request, res: Response, next: NextFunction) {
try {
const { triggerId } = req.params;
const trigger = await triggerService.markSubmitted(triggerId);
res.json(trigger);
} catch (error) {
next(error);
}
}
export async function confirmSettled(req: Request, res: Response, next: NextFunction) {
try {
const { triggerId } = req.params;
const trigger = await triggerService.confirmSettled(triggerId);
res.json(trigger);
} catch (error) {
next(error);
}
}
export async function confirmRejected(req: Request, res: Response, next: NextFunction) {
try {
const { triggerId } = req.params;
const { reason } = req.body;
const trigger = await triggerService.confirmRejected(triggerId, reason);
res.json(trigger);
} catch (error) {
next(error);
}
}

View File

@@ -0,0 +1,69 @@
/**
* REST API Server for eMoney Token Factory
* Implements OpenAPI 3.1 specification
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { OpenApiValidator } from 'express-openapi-validator';
import { errorHandler } from './middleware/error-handler';
import { authMiddleware } from './middleware/auth';
import { idempotencyMiddleware } from './middleware/idempotency';
import { tokensRouter } from './routes/tokens';
import { liensRouter } from './routes/liens';
import { complianceRouter } from './routes/compliance';
import { mappingsRouter } from './routes/mappings';
import { triggersRouter } from './routes/triggers';
import { isoRouter } from './routes/iso';
import { packetsRouter } from './routes/packets';
import { bridgeRouter } from './routes/bridge';
const app = express();
const PORT = process.env.PORT || 3000;
// Security middleware
app.use(helmet());
app.use(cors());
// Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// OpenAPI validation
new OpenApiValidator({
apiSpec: '../../packages/openapi/v1/openapi.yaml',
validateRequests: true,
validateResponses: true,
}).install(app);
// Auth middleware
app.use(authMiddleware);
// Idempotency middleware (for specific routes)
app.use(idempotencyMiddleware);
// Routes
app.use('/v1/tokens', tokensRouter);
app.use('/v1/liens', liensRouter);
app.use('/v1/compliance', complianceRouter);
app.use('/v1/mappings', mappingsRouter);
app.use('/v1/triggers', triggersRouter);
app.use('/v1/iso', isoRouter);
app.use('/v1/packets', packetsRouter);
app.use('/v1/bridge', bridgeRouter);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Error handler (must be last)
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`REST API server listening on port ${PORT}`);
});
export default app;

View File

@@ -0,0 +1,16 @@
/**
* Authentication middleware
* Supports OAuth2, mTLS, and API key
*/
import { Request, Response, NextFunction } from 'express';
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
// TODO: Implement OAuth2 token validation
// TODO: Implement mTLS validation for adapter endpoints
// TODO: Implement API key validation for internal services
// For now, pass through (will be implemented in Phase 6)
next();
}

View File

@@ -0,0 +1,22 @@
/**
* Error handler middleware
* Maps errors to HTTP responses with reason codes
*/
import { Request, Response, NextFunction } from 'express';
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
const status = err.status || err.statusCode || 500;
const code = err.code || 'INTERNAL_ERROR';
const message = err.message || 'Internal server error';
const reasonCode = err.reasonCode;
res.status(status).json({
code,
message,
reasonCode,
requestId: req.headers['x-request-id'],
details: err.details,
});
}

View File

@@ -0,0 +1,21 @@
/**
* Idempotency middleware
* Ensures requests with same idempotency key are only processed once
*/
import { Request, Response, NextFunction } from 'express';
// import { redisClient } from '../services/redis';
export async function idempotencyMiddleware(req: Request, res: Response, next: NextFunction) {
const idempotencyKey = req.headers['idempotency-key'] as string;
if (!idempotencyKey) {
return next();
}
// TODO: Check Redis for existing response
// TODO: Store response in Redis for replay
// For now, pass through (will be implemented in Phase 6)
next();
}

View File

@@ -0,0 +1,14 @@
/**
* Role-Based Access Control middleware
*/
import { Request, Response, NextFunction } from 'express';
export function requireRole(role: string) {
return (req: Request, res: Response, next: NextFunction) => {
// TODO: Check user roles from token/context
// For now, pass through (will be implemented in Phase 6)
next();
};
}

View File

@@ -0,0 +1,11 @@
import { Router } from 'express';
import { requireRole } from '../middleware/rbac';
import { bridgeLock, bridgeUnlock, getBridgeLock, getBridgeCorridors } from '../controllers/bridge';
export const bridgeRouter = Router();
bridgeRouter.post('/lock', bridgeLock);
bridgeRouter.post('/unlock', requireRole('BRIDGE_OPERATOR'), bridgeUnlock);
bridgeRouter.get('/locks/:lockId', getBridgeLock);
bridgeRouter.get('/corridors', getBridgeCorridors);

View File

@@ -0,0 +1,31 @@
import { Router } from 'express';
import { requireRole } from '../middleware/rbac';
import {
getComplianceProfile,
setCompliance,
setFrozen,
setTier,
setJurisdictionHash,
getWalletComplianceProfile,
setWalletCompliance,
setWalletFrozen,
setWalletTier,
setWalletJurisdictionHash,
} from '../controllers/compliance';
export const complianceRouter = Router();
// Account compliance
complianceRouter.put('/accounts/:accountRefId', requireRole('COMPLIANCE'), setCompliance);
complianceRouter.get('/accounts/:accountRefId', getComplianceProfile);
complianceRouter.put('/accounts/:accountRefId/freeze', requireRole('COMPLIANCE'), setFrozen);
complianceRouter.put('/accounts/:accountRefId/tier', requireRole('COMPLIANCE'), setTier);
complianceRouter.put('/accounts/:accountRefId/jurisdiction', requireRole('COMPLIANCE'), setJurisdictionHash);
// Wallet compliance
complianceRouter.put('/wallets/:walletRefId', requireRole('COMPLIANCE'), setWalletCompliance);
complianceRouter.get('/wallets/:walletRefId', getWalletComplianceProfile);
complianceRouter.put('/wallets/:walletRefId/freeze', requireRole('COMPLIANCE'), setWalletFrozen);
complianceRouter.put('/wallets/:walletRefId/tier', requireRole('COMPLIANCE'), setWalletTier);
complianceRouter.put('/wallets/:walletRefId/jurisdiction', requireRole('COMPLIANCE'), setWalletJurisdictionHash);

View File

@@ -0,0 +1,9 @@
import { Router } from 'express';
import { requireRole } from '../middleware/rbac';
import { submitInboundMessage, submitOutboundMessage } from '../controllers/iso';
export const isoRouter = Router();
isoRouter.post('/inbound', submitInboundMessage); // mTLS or OAuth2
isoRouter.post('/outbound', submitOutboundMessage);

View File

@@ -0,0 +1,14 @@
import { Router } from 'express';
import { requireRole } from '../middleware/rbac';
import { placeLien, listLiens, getLien, reduceLien, releaseLien, getAccountLiens, getEncumbrance } from '../controllers/liens';
export const liensRouter = Router();
liensRouter.post('/', requireRole('DEBT_AUTHORITY'), placeLien);
liensRouter.get('/', listLiens);
liensRouter.get('/:lienId', getLien);
liensRouter.patch('/:lienId', requireRole('DEBT_AUTHORITY'), reduceLien);
liensRouter.delete('/:lienId', requireRole('DEBT_AUTHORITY'), releaseLien);
liensRouter.get('/accounts/:accountRefId/liens', getAccountLiens);
liensRouter.get('/accounts/:accountRefId/encumbrance', getEncumbrance);

View File

@@ -0,0 +1,12 @@
import { Router } from 'express';
import { linkAccountWallet, unlinkAccountWallet, getAccountWallets, getWalletAccounts, connectProvider, getProviderStatus } from '../controllers/mappings';
export const mappingsRouter = Router();
mappingsRouter.post('/account-wallet/link', linkAccountWallet);
mappingsRouter.post('/account-wallet/unlink', unlinkAccountWallet);
mappingsRouter.get('/accounts/:accountRefId/wallets', getAccountWallets);
mappingsRouter.get('/wallets/:walletRefId/accounts', getWalletAccounts);
mappingsRouter.post('/providers/:provider/connect', connectProvider);
mappingsRouter.get('/providers/:provider/connections/:connectionId/status', getProviderStatus);

View File

@@ -0,0 +1,12 @@
import { Router } from 'express';
import { generatePacket, listPackets, getPacket, downloadPacket, dispatchPacket, acknowledgePacket } from '../controllers/packets';
export const packetsRouter = Router();
packetsRouter.post('/', generatePacket);
packetsRouter.get('/', listPackets);
packetsRouter.get('/:packetId', getPacket);
packetsRouter.get('/:packetId/download', downloadPacket);
packetsRouter.post('/:packetId/dispatch', dispatchPacket);
packetsRouter.post('/:packetId/ack', acknowledgePacket);

View File

@@ -0,0 +1,23 @@
/**
* Token routes
*/
import { Router } from 'express';
import { deployToken, listTokens, getToken, updateTokenPolicy } from '../controllers/tokens';
import { mintTokens, burnTokens, clawbackTokens, forceTransferTokens } from '../controllers/tokens';
import { requireRole } from '../middleware/rbac';
export const tokensRouter = Router();
// Token deployment and management
tokensRouter.post('/', requireRole('TOKEN_DEPLOYER'), deployToken);
tokensRouter.get('/', listTokens);
tokensRouter.get('/:code', getToken);
tokensRouter.patch('/:code/policy', requireRole('POLICY_OPERATOR'), updateTokenPolicy);
// Token operations
tokensRouter.post('/:code/mint', requireRole('ISSUER'), mintTokens);
tokensRouter.post('/:code/burn', requireRole('ISSUER'), burnTokens);
tokensRouter.post('/:code/clawback', requireRole('ENFORCEMENT'), clawbackTokens);
tokensRouter.post('/:code/force-transfer', requireRole('ENFORCEMENT'), forceTransferTokens);

View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { requireRole } from '../middleware/rbac';
import { listTriggers, getTrigger, validateAndLock, markSubmitted, confirmSettled, confirmRejected } from '../controllers/triggers';
export const triggersRouter = Router();
triggersRouter.get('/', listTriggers);
triggersRouter.get('/:triggerId', getTrigger);
triggersRouter.post('/:triggerId/validate-and-lock', requireRole('POLICY_OPERATOR'), validateAndLock);
triggersRouter.post('/:triggerId/mark-submitted', requireRole('POLICY_OPERATOR'), markSubmitted);
triggersRouter.post('/:triggerId/confirm-settled', requireRole('POLICY_OPERATOR'), confirmSettled);
triggersRouter.post('/:triggerId/confirm-rejected', requireRole('POLICY_OPERATOR'), confirmRejected);

View File

@@ -0,0 +1,109 @@
/**
* Bridge service
* Handles bridge lock/unlock operations
*/
import { blockchainClient } from '@emoney/blockchain';
import { keccak256, toUtf8Bytes } from 'ethers';
// In-memory lock store (in production, use database)
const locks = new Map<string, any>();
export interface BridgeLockInfo {
lockId: string;
token: string;
from: string;
amount: string;
targetChain: string;
targetRecipient: string;
status: string; // 'locked', 'unlocked'
createdAt: number;
unlockedAt?: number;
}
export interface LockRequest {
token: string; // Token address or code
amount: string;
targetChain: string;
targetRecipient: string;
}
export interface UnlockRequest {
token: string;
recipient: string;
amount: string;
sourceChain: string;
sourceTx: string;
}
export const bridgeService = {
async lock(params: LockRequest): Promise<BridgeLockInfo> {
// In production, resolve token code to address
const tokenAddress = params.token; // Simplified
const receipt = await blockchainClient.lockTokens(
tokenAddress,
params.amount,
params.targetChain,
params.targetRecipient
);
// Extract lock ID from event (simplified)
const lockId = keccak256(toUtf8Bytes(`${receipt.hash}_${Date.now()}`));
const lock: BridgeLockInfo = {
lockId,
token: tokenAddress,
from: receipt.from || '',
amount: params.amount,
targetChain: params.targetChain,
targetRecipient: params.targetRecipient,
status: 'locked',
createdAt: Date.now(),
};
locks.set(lockId, lock);
return lock;
},
async unlock(params: UnlockRequest): Promise<BridgeLockInfo> {
const tokenAddress = params.token; // Simplified
await blockchainClient.unlockTokens(
tokenAddress,
params.recipient,
params.amount,
params.sourceChain,
params.sourceTx
);
// Find or create unlock record
const unlockId = keccak256(toUtf8Bytes(`${params.sourceChain}_${params.sourceTx}`));
const lock: BridgeLockInfo = {
lockId: unlockId,
token: tokenAddress,
from: '',
amount: params.amount,
targetChain: '', // This is an unlock, so no target chain
targetRecipient: params.recipient,
status: 'unlocked',
createdAt: Date.now(),
unlockedAt: Date.now(),
};
locks.set(unlockId, lock);
return lock;
},
async getLockStatus(lockId: string): Promise<BridgeLockInfo | null> {
return locks.get(lockId) || null;
},
async getCorridors(): Promise<{ corridors: Array<{ sourceChain: string; targetChain: string; enabled: boolean }> }> {
// In production, query from configuration
return {
corridors: [
{ sourceChain: 'chain138', targetChain: 'ethereum', enabled: true },
{ sourceChain: 'chain138', targetChain: 'polygon', enabled: true },
],
};
},
};

View File

@@ -0,0 +1,73 @@
/**
* Compliance service
* Handles compliance operations
*/
import { blockchainClient } from '@emoney/blockchain';
export interface ComplianceProfile {
allowed: boolean;
frozen: boolean;
tiers: number[];
jurisdictionHash: string;
}
export interface SetComplianceRequest {
allowed: boolean;
tier: number;
jurisdictionHash: string; // bytes32 as hex string
}
export interface SetFrozenRequest {
frozen: boolean;
}
export const complianceService = {
async getProfile(accountRefId: string): Promise<ComplianceProfile> {
const profile = await blockchainClient.getComplianceProfile(accountRefId);
return {
allowed: profile.allowed,
frozen: profile.frozen,
tiers: [profile.tier], // Single tier for now
jurisdictionHash: profile.jurisdictionHash,
};
},
async setCompliance(accountRefId: string, params: SetComplianceRequest): Promise<ComplianceProfile> {
await blockchainClient.setCompliance({
account: accountRefId,
allowed: params.allowed,
tier: params.tier,
jurisdictionHash: params.jurisdictionHash,
});
return await this.getProfile(accountRefId);
},
async setFrozen(accountRefId: string, params: SetFrozenRequest): Promise<ComplianceProfile> {
await blockchainClient.setFrozen(accountRefId, params.frozen);
return await this.getProfile(accountRefId);
},
async setTier(accountRefId: string, tier: number): Promise<ComplianceProfile> {
const current = await this.getProfile(accountRefId);
await blockchainClient.setCompliance({
account: accountRefId,
allowed: current.allowed,
tier,
jurisdictionHash: current.jurisdictionHash,
});
return await this.getProfile(accountRefId);
},
async setJurisdictionHash(accountRefId: string, jurisdictionHash: string): Promise<ComplianceProfile> {
const current = await this.getProfile(accountRefId);
await blockchainClient.setCompliance({
account: accountRefId,
allowed: current.allowed,
tier: current.tiers[0] || 0,
jurisdictionHash,
});
return await this.getProfile(accountRefId);
},
};

View File

@@ -0,0 +1,44 @@
/**
* ISO-20022 service
* Handles ISO-20022 message processing
*/
export interface ISO20022Message {
msgType: string; // e.g., 'pacs.008', 'pain.001'
instructionId: string;
payload: any;
payloadHash: string;
}
export interface InboundMessageRequest {
message: ISO20022Message;
rail: string; // 'fedwire', 'swift', 'sepa', 'rtgs'
}
export interface OutboundMessageRequest {
triggerId: string;
message: ISO20022Message;
}
export const isoService = {
async submitInboundMessage(params: InboundMessageRequest): Promise<{ triggerId: string; status: string }> {
// In production, this would:
// 1. Validate ISO-20022 message structure
// 2. Normalize to canonical format
// 3. Create trigger
// 4. Route to orchestrator
const triggerId = `trigger_${Date.now()}`;
return { triggerId, status: 'received' };
},
async submitOutboundMessage(params: OutboundMessageRequest): Promise<{ packetId: string; status: string }> {
// In production, this would:
// 1. Get trigger details
// 2. Generate ISO-20022 message from canonical format
// 3. Create packet
// 4. Dispatch to rail
const packetId = `packet_${Date.now()}`;
return { packetId, status: 'generated' };
},
};

View File

@@ -0,0 +1,132 @@
/**
* Lien service
* Handles lien operations
*/
import { blockchainClient } from '@emoney/blockchain';
export interface PlaceLienRequest {
debtor: string; // Account address
amount: string; // BigNumber as string
expiry?: number; // Unix timestamp, 0 = no expiry
priority: number;
reasonCode: string; // bytes32 as hex string
}
export interface LienInfo {
lienId: string;
debtor: string;
amount: string;
expiry?: number;
priority: number;
authority: string;
reasonCode: string;
active: boolean;
}
export interface LienListFilters {
debtor?: string;
active?: boolean;
limit?: number;
offset?: number;
}
// In-memory lien registry (in production, use database/indexer)
const lienRegistry = new Map<string, LienInfo>();
export const lienService = {
async placeLien(params: PlaceLienRequest): Promise<LienInfo> {
const lienId = await blockchainClient.placeLien({
debtor: params.debtor,
amount: params.amount,
expiry: params.expiry,
priority: params.priority,
reasonCode: params.reasonCode,
});
// Fetch lien details
const lien = await blockchainClient.getLien(lienId);
const lienInfo: LienInfo = {
lienId: lienId.toString(),
debtor: lien.debtor,
amount: lien.amount.toString(),
expiry: lien.expiry === 0n ? undefined : Number(lien.expiry),
priority: Number(lien.priority),
authority: lien.authority,
reasonCode: lien.reasonCode,
active: lien.active,
};
lienRegistry.set(lienId.toString(), lienInfo);
return lienInfo;
},
async getLien(lienId: string): Promise<LienInfo | null> {
try {
const lien = await blockchainClient.getLien(BigInt(lienId));
return {
lienId,
debtor: lien.debtor,
amount: lien.amount.toString(),
expiry: lien.expiry === 0n ? undefined : Number(lien.expiry),
priority: Number(lien.priority),
authority: lien.authority,
reasonCode: lien.reasonCode,
active: lien.active,
};
} catch {
return null;
}
},
async reduceLien(lienId: string, reduceBy: string): Promise<LienInfo> {
await blockchainClient.reduceLien(BigInt(lienId), reduceBy);
const updated = await this.getLien(lienId);
if (!updated) {
throw new Error('Lien not found after reduction');
}
return updated;
},
async releaseLien(lienId: string): Promise<void> {
await blockchainClient.releaseLien(BigInt(lienId));
lienRegistry.delete(lienId);
},
async listLiens(filters: LienListFilters): Promise<{ liens: LienInfo[]; total: number }> {
// In production, query from database/indexer
// For now, return registered liens
let liens = Array.from(lienRegistry.values());
if (filters.debtor) {
liens = liens.filter(l => l.debtor.toLowerCase() === filters.debtor.toLowerCase());
}
if (filters.active !== undefined) {
liens = liens.filter(l => l.active === filters.active);
}
const total = liens.length;
const offset = filters.offset || 0;
const limit = filters.limit || 20;
liens = liens.slice(offset, offset + limit);
return { liens, total };
},
async getAccountLiens(accountRefId: string): Promise<LienInfo[]> {
// In production, query from database/indexer
const liens = Array.from(lienRegistry.values());
return liens.filter(l => l.debtor.toLowerCase() === accountRefId.toLowerCase());
},
async getEncumbrance(accountRefId: string): Promise<{ encumbrance: string; hasActiveLien: boolean }> {
const encumbrance = await blockchainClient.getActiveLienAmount(accountRefId);
const hasActiveLien = await blockchainClient.hasActiveLien(accountRefId);
return {
encumbrance: encumbrance.toString(),
hasActiveLien,
};
},
};

View File

@@ -0,0 +1,54 @@
/**
* Mapping service
* Handles account-wallet mapping
*/
// In-memory mapping store (in production, use database)
const accountToWallets = new Map<string, Set<string>>();
const walletToAccounts = new Map<string, Set<string>>();
export interface LinkRequest {
accountRefId: string;
walletRefId: string;
provider?: string; // e.g., 'walletconnect', 'fireblocks'
}
export const mappingService = {
async linkAccountWallet(params: LinkRequest): Promise<void> {
if (!accountToWallets.has(params.accountRefId)) {
accountToWallets.set(params.accountRefId, new Set());
}
if (!walletToAccounts.has(params.walletRefId)) {
walletToAccounts.set(params.walletRefId, new Set());
}
accountToWallets.get(params.accountRefId)!.add(params.walletRefId);
walletToAccounts.get(params.walletRefId)!.add(params.accountRefId);
},
async unlinkAccountWallet(params: LinkRequest): Promise<void> {
accountToWallets.get(params.accountRefId)?.delete(params.walletRefId);
walletToAccounts.get(params.walletRefId)?.delete(params.accountRefId);
},
async getAccountWallets(accountRefId: string): Promise<string[]> {
const wallets = accountToWallets.get(accountRefId);
return wallets ? Array.from(wallets) : [];
},
async getWalletAccounts(walletRefId: string): Promise<string[]> {
const accounts = walletToAccounts.get(walletRefId);
return accounts ? Array.from(accounts) : [];
},
async connectProvider(provider: string, params: any): Promise<{ status: string; connectionId?: string }> {
// Placeholder for provider integration (WalletConnect, Fireblocks, etc.)
return { status: 'connected', connectionId: `conn_${Date.now()}` };
},
async getProviderStatus(provider: string, connectionId: string): Promise<{ status: string }> {
// Placeholder for provider status check
return { status: 'active' };
},
};

View File

@@ -0,0 +1,110 @@
/**
* Packet service
* Handles packet generation and dispatch
*/
// In-memory packet store (in production, use database)
const packets = new Map<string, any>();
export interface PacketInfo {
packetId: string;
triggerId?: string;
payloadHash: string;
channel: string; // 'as4', 'email'
messageRef: string;
status: string; // 'generated', 'dispatched', 'acknowledged', 'failed'
acknowledgements: string[];
createdAt: number;
updatedAt: number;
}
export interface GeneratePacketRequest {
triggerId: string;
channel: string;
}
export interface DispatchPacketRequest {
packetId: string;
destination?: string;
}
export const packetService = {
async generatePacket(params: GeneratePacketRequest): Promise<PacketInfo> {
const packetId = `packet_${Date.now()}`;
const packet: PacketInfo = {
packetId,
triggerId: params.triggerId,
payloadHash: `hash_${Date.now()}`,
channel: params.channel,
messageRef: `msg_${Date.now()}`,
status: 'generated',
acknowledgements: [],
createdAt: Date.now(),
updatedAt: Date.now(),
};
packets.set(packetId, packet);
return packet;
},
async getPacket(packetId: string): Promise<PacketInfo | null> {
return packets.get(packetId) || null;
},
async listPackets(filters: { triggerId?: string; status?: string; limit?: number; offset?: number }): Promise<{ packets: PacketInfo[]; total: number }> {
let packetList = Array.from(packets.values());
if (filters.triggerId) {
packetList = packetList.filter(p => p.triggerId === filters.triggerId);
}
if (filters.status) {
packetList = packetList.filter(p => p.status === filters.status);
}
const total = packetList.length;
const offset = filters.offset || 0;
const limit = filters.limit || 20;
packetList = packetList.slice(offset, offset + limit);
return { packets: packetList, total };
},
async downloadPacket(packetId: string): Promise<{ content: Buffer; contentType: string; filename: string }> {
const packet = await this.getPacket(packetId);
if (!packet) {
throw new Error('Packet not found');
}
// In production, generate PDF or fetch from storage
return {
content: Buffer.from('PDF content placeholder'),
contentType: 'application/pdf',
filename: `${packetId}.pdf`,
};
},
async dispatchPacket(params: DispatchPacketRequest): Promise<PacketInfo> {
const packet = await this.getPacket(params.packetId);
if (!packet) {
throw new Error('Packet not found');
}
packet.status = 'dispatched';
packet.updatedAt = Date.now();
packets.set(params.packetId, packet);
return packet;
},
async acknowledgePacket(packetId: string, ack: { ackId: string; status: string }): Promise<PacketInfo> {
const packet = await this.getPacket(packetId);
if (!packet) {
throw new Error('Packet not found');
}
packet.acknowledgements.push(ack.ackId);
if (ack.status === 'acknowledged') {
packet.status = 'acknowledged';
}
packet.updatedAt = Date.now();
packets.set(packetId, packet);
return packet;
},
};

View File

@@ -0,0 +1,227 @@
/**
* Token service
* Handles token deployment and operations
*/
import { blockchainClient } from '@emoney/blockchain';
import { keccak256, toUtf8Bytes } from 'ethers';
export interface DeployTokenRequest {
name: string;
symbol: string;
code: string; // Unique token code
issuer: string; // Issuer address
decimals: number;
defaultLienMode: number; // 1 = hard freeze, 2 = encumbered
bridgeOnly: boolean;
bridge?: string;
}
export interface TokenInfo {
code: string;
address: string;
name: string;
symbol: string;
decimals: number;
issuer: string;
totalSupply: string;
configPointers: {
policyManager?: string;
debtRegistry?: string;
complianceRegistry?: string;
};
}
export interface TokenListFilters {
code?: string;
issuer?: string;
limit?: number;
offset?: number;
}
export interface MintRequest {
to: string;
amount: string; // BigNumber as string
}
export interface BurnRequest {
amount: string; // BigNumber as string
}
export interface ClawbackRequest {
from: string;
amount: string; // BigNumber as string
}
export interface ForceTransferRequest {
from: string;
to: string;
amount: string; // BigNumber as string
}
export interface PolicyUpdate {
paused?: boolean;
bridgeOnly?: boolean;
bridge?: string;
lienMode?: number;
}
// In-memory token registry (in production, use database)
const tokenRegistry = new Map<string, TokenInfo>();
export const tokenService = {
async deployToken(config: DeployTokenRequest): Promise<TokenInfo> {
// Deploy token via factory
const tokenAddress = await blockchainClient.deployToken(
config.name,
config.symbol,
{
issuer: config.issuer,
decimals: config.decimals,
defaultLienMode: config.defaultLienMode,
bridgeOnly: config.bridgeOnly,
bridge: config.bridge,
}
);
// Get token info
const tokenInfo = await blockchainClient.getTokenInfo(tokenAddress);
const policy = await blockchainClient.getTokenPolicy(tokenAddress);
const token: TokenInfo = {
code: config.code,
address: tokenAddress,
name: tokenInfo.name,
symbol: tokenInfo.symbol,
decimals: tokenInfo.decimals,
issuer: config.issuer,
totalSupply: tokenInfo.totalSupply,
configPointers: {
policyManager: process.env.POLICY_MANAGER_ADDRESS,
debtRegistry: process.env.DEBT_REGISTRY_ADDRESS,
complianceRegistry: process.env.COMPLIANCE_REGISTRY_ADDRESS,
},
};
// Register token
tokenRegistry.set(config.code, token);
return token;
},
async listTokens(filters: TokenListFilters): Promise<{ tokens: TokenInfo[]; total: number }> {
// In production, query from database/indexer
// For now, return registered tokens
let tokens = Array.from(tokenRegistry.values());
if (filters.code) {
tokens = tokens.filter(t => t.code === filters.code);
}
if (filters.issuer) {
tokens = tokens.filter(t => t.issuer.toLowerCase() === filters.issuer.toLowerCase());
}
const total = tokens.length;
const offset = filters.offset || 0;
const limit = filters.limit || 20;
tokens = tokens.slice(offset, offset + limit);
return { tokens, total };
},
async getToken(code: string): Promise<TokenInfo | null> {
// Check registry first
const token = tokenRegistry.get(code);
if (token) {
// Refresh supply
const info = await blockchainClient.getTokenInfo(token.address);
return { ...token, totalSupply: info.totalSupply };
}
// Try to find by code hash (if code is hash)
try {
const codeHash = keccak256(toUtf8Bytes(code));
const address = await blockchainClient.getTokenByCodeHash(codeHash);
if (address) {
const info = await blockchainClient.getTokenInfo(address);
// Try to determine code from name/symbol or use address
return {
code: code,
address,
name: info.name,
symbol: info.symbol,
decimals: info.decimals,
issuer: '', // Would need to query from events
totalSupply: info.totalSupply,
configPointers: {},
};
}
} catch {
// Ignore
}
return null;
},
async updatePolicy(code: string, policy: PolicyUpdate): Promise<TokenInfo> {
const token = await this.getToken(code);
if (!token) {
throw new Error('Token not found');
}
await blockchainClient.updateTokenPolicy(token.address, policy);
// Refresh token info
return await this.getToken(code) || token;
},
async mint(code: string, params: MintRequest): Promise<{ txHash: string }> {
const token = await this.getToken(code);
if (!token) {
throw new Error('Token not found');
}
const receipt = await blockchainClient.mintToken(token.address, params.to, params.amount);
return { txHash: receipt.hash };
},
async burn(code: string, params: BurnRequest): Promise<{ txHash: string }> {
const token = await this.getToken(code);
if (!token) {
throw new Error('Token not found');
}
const receipt = await blockchainClient.burnToken(token.address, params.amount);
return { txHash: receipt.hash };
},
async clawback(code: string, params: ClawbackRequest): Promise<{ txHash: string }> {
const token = await this.getToken(code);
if (!token) {
throw new Error('Token not found');
}
const receipt = await blockchainClient.clawbackToken(
token.address,
params.from,
params.amount
);
return { txHash: receipt.hash };
},
async forceTransfer(code: string, params: ForceTransferRequest): Promise<{ txHash: string }> {
const token = await this.getToken(code);
if (!token) {
throw new Error('Token not found');
}
const receipt = await blockchainClient.forceTransferToken(
token.address,
params.from,
params.to,
params.amount
);
return { txHash: receipt.hash };
},
};

View File

@@ -0,0 +1,114 @@
/**
* Trigger service
* Handles payment trigger operations
*/
// In-memory trigger store (in production, use database)
const triggers = new Map<string, any>();
export interface TriggerInfo {
triggerId: string;
rail: string;
msgType: string;
stateMachine: string;
instructionId: string;
payloadHash: string;
amount: string;
token: string;
refs: {
accountRef?: string;
walletRef?: string;
};
state: string;
createdAt: number;
updatedAt: number;
}
export interface TriggerListFilters {
rail?: string;
state?: string;
accountRef?: string;
walletRef?: string;
limit?: number;
offset?: number;
}
export const triggerService = {
async getTrigger(triggerId: string): Promise<TriggerInfo | null> {
return triggers.get(triggerId) || null;
},
async listTriggers(filters: TriggerListFilters): Promise<{ triggers: TriggerInfo[]; total: number }> {
let triggerList = Array.from(triggers.values());
if (filters.rail) {
triggerList = triggerList.filter(t => t.rail === filters.rail);
}
if (filters.state) {
triggerList = triggerList.filter(t => t.state === filters.state);
}
if (filters.accountRef) {
triggerList = triggerList.filter(t => t.refs?.accountRef === filters.accountRef);
}
if (filters.walletRef) {
triggerList = triggerList.filter(t => t.refs?.walletRef === filters.walletRef);
}
const total = triggerList.length;
const offset = filters.offset || 0;
const limit = filters.limit || 20;
triggerList = triggerList.slice(offset, offset + limit);
return { triggers: triggerList, total };
},
async validateAndLock(triggerId: string, params: any): Promise<TriggerInfo> {
const trigger = await this.getTrigger(triggerId);
if (!trigger) {
throw new Error('Trigger not found');
}
// Update state to 'locked'
trigger.state = 'locked';
trigger.updatedAt = Date.now();
triggers.set(triggerId, trigger);
return trigger;
},
async markSubmitted(triggerId: string): Promise<TriggerInfo> {
const trigger = await this.getTrigger(triggerId);
if (!trigger) {
throw new Error('Trigger not found');
}
trigger.state = 'submitted';
trigger.updatedAt = Date.now();
triggers.set(triggerId, trigger);
return trigger;
},
async confirmSettled(triggerId: string): Promise<TriggerInfo> {
const trigger = await this.getTrigger(triggerId);
if (!trigger) {
throw new Error('Trigger not found');
}
trigger.state = 'settled';
trigger.updatedAt = Date.now();
triggers.set(triggerId, trigger);
return trigger;
},
async confirmRejected(triggerId: string, reason?: string): Promise<TriggerInfo> {
const trigger = await this.getTrigger(triggerId);
if (!trigger) {
throw new Error('Trigger not found');
}
trigger.state = 'rejected';
trigger.updatedAt = Date.now();
if (reason) {
trigger.rejectionReason = reason;
}
triggers.set(triggerId, trigger);
return trigger;
},
};