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:
47
api/services/rest-api/src/controllers/bridge.ts
Normal file
47
api/services/rest-api/src/controllers/bridge.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
117
api/services/rest-api/src/controllers/compliance.ts
Normal file
117
api/services/rest-api/src/controllers/compliance.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
25
api/services/rest-api/src/controllers/iso.ts
Normal file
25
api/services/rest-api/src/controllers/iso.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
85
api/services/rest-api/src/controllers/liens.ts
Normal file
85
api/services/rest-api/src/controllers/liens.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
65
api/services/rest-api/src/controllers/mappings.ts
Normal file
65
api/services/rest-api/src/controllers/mappings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
76
api/services/rest-api/src/controllers/packets.ts
Normal file
76
api/services/rest-api/src/controllers/packets.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
94
api/services/rest-api/src/controllers/tokens.ts
Normal file
94
api/services/rest-api/src/controllers/tokens.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
78
api/services/rest-api/src/controllers/triggers.ts
Normal file
78
api/services/rest-api/src/controllers/triggers.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
69
api/services/rest-api/src/index.ts
Normal file
69
api/services/rest-api/src/index.ts
Normal 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;
|
||||
|
||||
16
api/services/rest-api/src/middleware/auth.ts
Normal file
16
api/services/rest-api/src/middleware/auth.ts
Normal 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();
|
||||
}
|
||||
|
||||
22
api/services/rest-api/src/middleware/error-handler.ts
Normal file
22
api/services/rest-api/src/middleware/error-handler.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
21
api/services/rest-api/src/middleware/idempotency.ts
Normal file
21
api/services/rest-api/src/middleware/idempotency.ts
Normal 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();
|
||||
}
|
||||
|
||||
14
api/services/rest-api/src/middleware/rbac.ts
Normal file
14
api/services/rest-api/src/middleware/rbac.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
|
||||
11
api/services/rest-api/src/routes/bridge.ts
Normal file
11
api/services/rest-api/src/routes/bridge.ts
Normal 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);
|
||||
|
||||
31
api/services/rest-api/src/routes/compliance.ts
Normal file
31
api/services/rest-api/src/routes/compliance.ts
Normal 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);
|
||||
|
||||
9
api/services/rest-api/src/routes/iso.ts
Normal file
9
api/services/rest-api/src/routes/iso.ts
Normal 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);
|
||||
|
||||
14
api/services/rest-api/src/routes/liens.ts
Normal file
14
api/services/rest-api/src/routes/liens.ts
Normal 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);
|
||||
|
||||
12
api/services/rest-api/src/routes/mappings.ts
Normal file
12
api/services/rest-api/src/routes/mappings.ts
Normal 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);
|
||||
|
||||
12
api/services/rest-api/src/routes/packets.ts
Normal file
12
api/services/rest-api/src/routes/packets.ts
Normal 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);
|
||||
|
||||
23
api/services/rest-api/src/routes/tokens.ts
Normal file
23
api/services/rest-api/src/routes/tokens.ts
Normal 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);
|
||||
|
||||
13
api/services/rest-api/src/routes/triggers.ts
Normal file
13
api/services/rest-api/src/routes/triggers.ts
Normal 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);
|
||||
|
||||
109
api/services/rest-api/src/services/bridge-service.ts
Normal file
109
api/services/rest-api/src/services/bridge-service.ts
Normal 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 },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
73
api/services/rest-api/src/services/compliance-service.ts
Normal file
73
api/services/rest-api/src/services/compliance-service.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
|
||||
44
api/services/rest-api/src/services/iso-service.ts
Normal file
44
api/services/rest-api/src/services/iso-service.ts
Normal 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' };
|
||||
},
|
||||
};
|
||||
|
||||
132
api/services/rest-api/src/services/lien-service.ts
Normal file
132
api/services/rest-api/src/services/lien-service.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
54
api/services/rest-api/src/services/mapping-service.ts
Normal file
54
api/services/rest-api/src/services/mapping-service.ts
Normal 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' };
|
||||
},
|
||||
};
|
||||
|
||||
110
api/services/rest-api/src/services/packet-service.ts
Normal file
110
api/services/rest-api/src/services/packet-service.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
|
||||
227
api/services/rest-api/src/services/token-service.ts
Normal file
227
api/services/rest-api/src/services/token-service.ts
Normal 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 };
|
||||
},
|
||||
};
|
||||
114
api/services/rest-api/src/services/trigger-service.ts
Normal file
114
api/services/rest-api/src/services/trigger-service.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user