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:
454
api/shared/blockchain/contracts.ts
Normal file
454
api/shared/blockchain/contracts.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* Blockchain contract interaction layer
|
||||
* Wrappers for TokenFactory138, DebtRegistry, ComplianceRegistry, etc.
|
||||
*/
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
import { keccak256, toUtf8Bytes } from 'ethers';
|
||||
|
||||
// Contract interfaces (minimal ABIs for the methods we need)
|
||||
const TOKEN_FACTORY_ABI = [
|
||||
'function deployToken(string name, string symbol, tuple(address issuer, uint8 decimals, uint8 defaultLienMode, bool bridgeOnly, address bridge) config) returns (address)',
|
||||
'function tokenByCodeHash(bytes32) view returns (address)',
|
||||
'event TokenDeployed(address indexed token, bytes32 indexed codeHash, string name, string symbol, uint8 decimals, address indexed issuer, uint8 defaultLienMode, bool bridgeOnly, address bridge)'
|
||||
];
|
||||
|
||||
const DEBT_REGISTRY_ABI = [
|
||||
'function activeLienAmount(address) view returns (uint256)',
|
||||
'function hasActiveLien(address) view returns (bool)',
|
||||
'function activeLienCount(address) view returns (uint256)',
|
||||
'function getLien(uint256) view returns (tuple(address debtor, uint256 amount, uint64 expiry, uint8 priority, address authority, bytes32 reasonCode, bool active))',
|
||||
'function placeLien(address debtor, uint256 amount, uint64 expiry, uint8 priority, bytes32 reasonCode) returns (uint256)',
|
||||
'function reduceLien(uint256 lienId, uint256 reduceBy)',
|
||||
'function releaseLien(uint256 lienId)',
|
||||
'event LienPlaced(uint256 indexed lienId, address indexed debtor, uint256 amount, uint64 expiry, uint8 priority, address indexed authority, bytes32 reasonCode)',
|
||||
'event LienReduced(uint256 indexed lienId, uint256 reduceBy, uint256 newAmount)',
|
||||
'event LienReleased(uint256 indexed lienId)'
|
||||
];
|
||||
|
||||
const COMPLIANCE_REGISTRY_ABI = [
|
||||
'function isAllowed(address) view returns (bool)',
|
||||
'function isFrozen(address) view returns (bool)',
|
||||
'function riskTier(address) view returns (uint8)',
|
||||
'function jurisdictionHash(address) view returns (bytes32)',
|
||||
'function setCompliance(address account, bool allowed, uint8 tier, bytes32 jurHash)',
|
||||
'function setFrozen(address account, bool frozen)',
|
||||
'event ComplianceUpdated(address indexed account, bool allowed, uint8 tier, bytes32 jurisdictionHash)',
|
||||
'event FrozenUpdated(address indexed account, bool frozen)'
|
||||
];
|
||||
|
||||
const POLICY_MANAGER_ABI = [
|
||||
'function isPaused(address) view returns (bool)',
|
||||
'function bridgeOnly(address) view returns (bool)',
|
||||
'function bridge(address) view returns (address)',
|
||||
'function lienMode(address) view returns (uint8)',
|
||||
'function isTokenFrozen(address, address) view returns (bool)',
|
||||
'function canTransfer(address, address, address, uint256) view returns (bool, bytes32)',
|
||||
'function setPaused(address, bool)',
|
||||
'function setBridgeOnly(address, bool)',
|
||||
'function setBridge(address, address)',
|
||||
'function setLienMode(address, uint8)',
|
||||
'function freeze(address, address, bool)'
|
||||
];
|
||||
|
||||
const ERC20_ABI = [
|
||||
'function name() view returns (string)',
|
||||
'function symbol() view returns (string)',
|
||||
'function decimals() view returns (uint8)',
|
||||
'function totalSupply() view returns (uint256)',
|
||||
'function balanceOf(address) view returns (uint256)',
|
||||
'function transfer(address, uint256) returns (bool)',
|
||||
'function mint(address, uint256)',
|
||||
'function burn(uint256)',
|
||||
'function clawback(address, uint256)',
|
||||
'function forceTransfer(address, address, uint256)'
|
||||
];
|
||||
|
||||
const BRIDGE_VAULT_ABI = [
|
||||
'function lock(address token, uint256 amount, bytes32 targetChain, address targetRecipient)',
|
||||
'function unlock(address token, address recipient, uint256 amount, bytes32 sourceChain, bytes32 sourceTx)',
|
||||
'function getLockStatus(bytes32 lockId) view returns (bool, address, uint256, bytes32, address)',
|
||||
'event Locked(bytes32 indexed lockId, address indexed token, address indexed from, uint256 amount, bytes32 targetChain, address targetRecipient)',
|
||||
'event Unlocked(bytes32 indexed unlockId, address indexed token, address indexed recipient, uint256 amount, bytes32 sourceChain, bytes32 sourceTx)'
|
||||
];
|
||||
|
||||
export interface TokenConfig {
|
||||
issuer: string;
|
||||
decimals: number;
|
||||
defaultLienMode: number; // 1 = hard freeze, 2 = encumbered
|
||||
bridgeOnly: boolean;
|
||||
bridge?: string;
|
||||
}
|
||||
|
||||
export interface LienParams {
|
||||
debtor: string;
|
||||
amount: string; // BigNumber as string
|
||||
expiry?: number; // Unix timestamp, 0 = no expiry
|
||||
priority: number;
|
||||
reasonCode: string; // bytes32 as hex string
|
||||
}
|
||||
|
||||
export interface ComplianceParams {
|
||||
account: string;
|
||||
allowed: boolean;
|
||||
tier: number;
|
||||
jurisdictionHash: string; // bytes32 as hex string
|
||||
}
|
||||
|
||||
export class BlockchainClient {
|
||||
private provider: ethers.JsonRpcProvider;
|
||||
private signer?: ethers.Wallet;
|
||||
private tokenFactory?: ethers.Contract;
|
||||
private debtRegistry?: ethers.Contract;
|
||||
private complianceRegistry?: ethers.Contract;
|
||||
private policyManager?: ethers.Contract;
|
||||
private bridgeVault?: ethers.Contract;
|
||||
|
||||
constructor(
|
||||
rpcUrl: string,
|
||||
privateKey?: string,
|
||||
contractAddresses?: {
|
||||
tokenFactory?: string;
|
||||
debtRegistry?: string;
|
||||
complianceRegistry?: string;
|
||||
policyManager?: string;
|
||||
bridgeVault?: string;
|
||||
}
|
||||
) {
|
||||
this.provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
if (privateKey) {
|
||||
this.signer = new ethers.Wallet(privateKey, this.provider);
|
||||
}
|
||||
|
||||
// Initialize contracts if addresses provided
|
||||
if (contractAddresses) {
|
||||
if (contractAddresses.tokenFactory && this.signer) {
|
||||
this.tokenFactory = new ethers.Contract(
|
||||
contractAddresses.tokenFactory,
|
||||
TOKEN_FACTORY_ABI,
|
||||
this.signer
|
||||
);
|
||||
}
|
||||
if (contractAddresses.debtRegistry) {
|
||||
this.debtRegistry = new ethers.Contract(
|
||||
contractAddresses.debtRegistry,
|
||||
DEBT_REGISTRY_ABI,
|
||||
this.signer || this.provider
|
||||
);
|
||||
}
|
||||
if (contractAddresses.complianceRegistry) {
|
||||
this.complianceRegistry = new ethers.Contract(
|
||||
contractAddresses.complianceRegistry,
|
||||
COMPLIANCE_REGISTRY_ABI,
|
||||
this.signer || this.provider
|
||||
);
|
||||
}
|
||||
if (contractAddresses.policyManager) {
|
||||
this.policyManager = new ethers.Contract(
|
||||
contractAddresses.policyManager,
|
||||
POLICY_MANAGER_ABI,
|
||||
this.signer || this.provider
|
||||
);
|
||||
}
|
||||
if (contractAddresses.bridgeVault) {
|
||||
this.bridgeVault = new ethers.Contract(
|
||||
contractAddresses.bridgeVault,
|
||||
BRIDGE_VAULT_ABI,
|
||||
this.signer || this.provider
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Token Factory operations
|
||||
async deployToken(name: string, symbol: string, config: TokenConfig): Promise<string> {
|
||||
if (!this.tokenFactory || !this.signer) {
|
||||
throw new Error('TokenFactory contract not initialized or signer not available');
|
||||
}
|
||||
|
||||
const tx = await this.tokenFactory.deployToken(name, symbol, {
|
||||
issuer: config.issuer,
|
||||
decimals: config.decimals,
|
||||
defaultLienMode: config.defaultLienMode,
|
||||
bridgeOnly: config.bridgeOnly,
|
||||
bridge: config.bridge || ethers.ZeroAddress,
|
||||
});
|
||||
|
||||
const receipt = await tx.wait();
|
||||
const event = receipt.logs.find((log: any) => {
|
||||
try {
|
||||
const parsed = this.tokenFactory!.interface.parseLog(log);
|
||||
return parsed?.name === 'TokenDeployed';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (event) {
|
||||
const parsed = this.tokenFactory.interface.parseLog(event);
|
||||
return parsed!.args.token;
|
||||
}
|
||||
|
||||
throw new Error('TokenDeployed event not found');
|
||||
}
|
||||
|
||||
async getTokenByCodeHash(codeHash: string): Promise<string | null> {
|
||||
if (!this.tokenFactory) {
|
||||
throw new Error('TokenFactory contract not initialized');
|
||||
}
|
||||
try {
|
||||
const address = await this.tokenFactory.tokenByCodeHash(codeHash);
|
||||
return address === ethers.ZeroAddress ? null : address;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Debt Registry operations
|
||||
async placeLien(params: LienParams): Promise<bigint> {
|
||||
if (!this.debtRegistry || !this.signer) {
|
||||
throw new Error('DebtRegistry contract not initialized or signer not available');
|
||||
}
|
||||
|
||||
const tx = await this.debtRegistry.placeLien(
|
||||
params.debtor,
|
||||
params.amount,
|
||||
params.expiry || 0,
|
||||
params.priority,
|
||||
params.reasonCode
|
||||
);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
const event = receipt.logs.find((log: any) => {
|
||||
try {
|
||||
const parsed = this.debtRegistry!.interface.parseLog(log);
|
||||
return parsed?.name === 'LienPlaced';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (event) {
|
||||
const parsed = this.debtRegistry.interface.parseLog(event);
|
||||
return parsed!.args.lienId;
|
||||
}
|
||||
|
||||
throw new Error('LienPlaced event not found');
|
||||
}
|
||||
|
||||
async getLien(lienId: bigint) {
|
||||
if (!this.debtRegistry) {
|
||||
throw new Error('DebtRegistry contract not initialized');
|
||||
}
|
||||
return await this.debtRegistry.getLien(lienId);
|
||||
}
|
||||
|
||||
async reduceLien(lienId: bigint, reduceBy: string) {
|
||||
if (!this.debtRegistry || !this.signer) {
|
||||
throw new Error('DebtRegistry contract not initialized or signer not available');
|
||||
}
|
||||
const tx = await this.debtRegistry.reduceLien(lienId, reduceBy);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async releaseLien(lienId: bigint) {
|
||||
if (!this.debtRegistry || !this.signer) {
|
||||
throw new Error('DebtRegistry contract not initialized or signer not available');
|
||||
}
|
||||
const tx = await this.debtRegistry.releaseLien(lienId);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async getActiveLienAmount(debtor: string): Promise<bigint> {
|
||||
if (!this.debtRegistry) {
|
||||
throw new Error('DebtRegistry contract not initialized');
|
||||
}
|
||||
return await this.debtRegistry.activeLienAmount(debtor);
|
||||
}
|
||||
|
||||
async hasActiveLien(debtor: string): Promise<boolean> {
|
||||
if (!this.debtRegistry) {
|
||||
throw new Error('DebtRegistry contract not initialized');
|
||||
}
|
||||
return await this.debtRegistry.hasActiveLien(debtor);
|
||||
}
|
||||
|
||||
// Compliance Registry operations
|
||||
async setCompliance(params: ComplianceParams) {
|
||||
if (!this.complianceRegistry || !this.signer) {
|
||||
throw new Error('ComplianceRegistry contract not initialized or signer not available');
|
||||
}
|
||||
const tx = await this.complianceRegistry.setCompliance(
|
||||
params.account,
|
||||
params.allowed,
|
||||
params.tier,
|
||||
params.jurisdictionHash
|
||||
);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async setFrozen(account: string, frozen: boolean) {
|
||||
if (!this.complianceRegistry || !this.signer) {
|
||||
throw new Error('ComplianceRegistry contract not initialized or signer not available');
|
||||
}
|
||||
const tx = await this.complianceRegistry.setFrozen(account, frozen);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async getComplianceProfile(account: string) {
|
||||
if (!this.complianceRegistry) {
|
||||
throw new Error('ComplianceRegistry contract not initialized');
|
||||
}
|
||||
const [allowed, frozen, tier, jurisdictionHash] = await Promise.all([
|
||||
this.complianceRegistry.isAllowed(account),
|
||||
this.complianceRegistry.isFrozen(account),
|
||||
this.complianceRegistry.riskTier(account),
|
||||
this.complianceRegistry.jurisdictionHash(account),
|
||||
]);
|
||||
return { allowed, frozen, tier: Number(tier), jurisdictionHash };
|
||||
}
|
||||
|
||||
// Policy Manager operations
|
||||
async getTokenPolicy(tokenAddress: string) {
|
||||
if (!this.policyManager) {
|
||||
throw new Error('PolicyManager contract not initialized');
|
||||
}
|
||||
const [isPaused, bridgeOnly, bridge, lienMode] = await Promise.all([
|
||||
this.policyManager.isPaused(tokenAddress),
|
||||
this.policyManager.bridgeOnly(tokenAddress),
|
||||
this.policyManager.bridge(tokenAddress),
|
||||
this.policyManager.lienMode(tokenAddress),
|
||||
]);
|
||||
return {
|
||||
isPaused,
|
||||
bridgeOnly,
|
||||
bridge: bridge === ethers.ZeroAddress ? null : bridge,
|
||||
lienMode: Number(lienMode),
|
||||
};
|
||||
}
|
||||
|
||||
async updateTokenPolicy(tokenAddress: string, updates: {
|
||||
paused?: boolean;
|
||||
bridgeOnly?: boolean;
|
||||
bridge?: string;
|
||||
lienMode?: number;
|
||||
}) {
|
||||
if (!this.policyManager || !this.signer) {
|
||||
throw new Error('PolicyManager contract not initialized or signer not available');
|
||||
}
|
||||
const txs = [];
|
||||
if (updates.paused !== undefined) {
|
||||
txs.push(this.policyManager.setPaused(tokenAddress, updates.paused));
|
||||
}
|
||||
if (updates.bridgeOnly !== undefined) {
|
||||
txs.push(this.policyManager.setBridgeOnly(tokenAddress, updates.bridgeOnly));
|
||||
}
|
||||
if (updates.bridge !== undefined) {
|
||||
txs.push(this.policyManager.setBridge(tokenAddress, updates.bridge || ethers.ZeroAddress));
|
||||
}
|
||||
if (updates.lienMode !== undefined) {
|
||||
txs.push(this.policyManager.setLienMode(tokenAddress, updates.lienMode));
|
||||
}
|
||||
return await Promise.all(txs.map(tx => tx.wait()));
|
||||
}
|
||||
|
||||
// Token operations (ERC20 + custom)
|
||||
async getTokenInfo(tokenAddress: string) {
|
||||
const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.provider);
|
||||
const [name, symbol, decimals, totalSupply] = await Promise.all([
|
||||
token.name(),
|
||||
token.symbol(),
|
||||
token.decimals(),
|
||||
token.totalSupply(),
|
||||
]);
|
||||
return { name, symbol, decimals: Number(decimals), totalSupply: totalSupply.toString() };
|
||||
}
|
||||
|
||||
async getTokenBalance(tokenAddress: string, account: string): Promise<bigint> {
|
||||
const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.provider);
|
||||
return await token.balanceOf(account);
|
||||
}
|
||||
|
||||
async mintToken(tokenAddress: string, to: string, amount: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Signer not available');
|
||||
}
|
||||
const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer);
|
||||
const tx = await token.mint(to, amount);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async burnToken(tokenAddress: string, amount: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Signer not available');
|
||||
}
|
||||
const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer);
|
||||
const tx = await token.burn(amount);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async clawbackToken(tokenAddress: string, from: string, amount: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Signer not available');
|
||||
}
|
||||
const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer);
|
||||
const tx = await token.clawback(from, amount);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async forceTransferToken(tokenAddress: string, from: string, to: string, amount: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Signer not available');
|
||||
}
|
||||
const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer);
|
||||
const tx = await token.forceTransfer(from, to, amount);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
// Bridge operations
|
||||
async lockTokens(tokenAddress: string, amount: string, targetChain: string, targetRecipient: string) {
|
||||
if (!this.bridgeVault || !this.signer) {
|
||||
throw new Error('BridgeVault contract not initialized or signer not available');
|
||||
}
|
||||
const tx = await this.bridgeVault.lock(
|
||||
tokenAddress,
|
||||
amount,
|
||||
targetChain,
|
||||
targetRecipient
|
||||
);
|
||||
return await tx.wait();
|
||||
}
|
||||
|
||||
async unlockTokens(
|
||||
tokenAddress: string,
|
||||
recipient: string,
|
||||
amount: string,
|
||||
sourceChain: string,
|
||||
sourceTx: string
|
||||
) {
|
||||
if (!this.bridgeVault || !this.signer) {
|
||||
throw new Error('BridgeVault contract not initialized or signer not available');
|
||||
}
|
||||
const tx = await this.bridgeVault.unlock(
|
||||
tokenAddress,
|
||||
recipient,
|
||||
amount,
|
||||
sourceChain,
|
||||
sourceTx
|
||||
);
|
||||
return await tx.wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const blockchainClient = new BlockchainClient(
|
||||
process.env.RPC_URL || 'http://localhost:8545',
|
||||
process.env.PRIVATE_KEY,
|
||||
{
|
||||
tokenFactory: process.env.TOKEN_FACTORY_ADDRESS,
|
||||
debtRegistry: process.env.DEBT_REGISTRY_ADDRESS,
|
||||
complianceRegistry: process.env.COMPLIANCE_REGISTRY_ADDRESS,
|
||||
policyManager: process.env.POLICY_MANAGER_ADDRESS,
|
||||
bridgeVault: process.env.BRIDGE_VAULT_ADDRESS,
|
||||
}
|
||||
);
|
||||
26
api/shared/blockchain/package.json
Normal file
26
api/shared/blockchain/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@emoney/blockchain",
|
||||
"version": "1.0.0",
|
||||
"description": "Blockchain interaction layer for eMoney contracts",
|
||||
"main": "dist/contracts.js",
|
||||
"types": "dist/contracts.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/contracts.js",
|
||||
"require": "./dist/contracts.js",
|
||||
"types": "./dist/contracts.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethers": "^6.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
70
api/shared/events/event-bus.ts
Normal file
70
api/shared/events/event-bus.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Event bus client for publishing and subscribing to events
|
||||
* Supports Kafka and NATS
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export interface EventEnvelope {
|
||||
eventId: string;
|
||||
eventType: string;
|
||||
occurredAt: string;
|
||||
actorRef?: string;
|
||||
correlationId?: string;
|
||||
payload: any;
|
||||
signatures?: Array<{ signer: string; signature: string }>;
|
||||
}
|
||||
|
||||
export class EventBusClient extends EventEmitter {
|
||||
private kafkaClient: any;
|
||||
private natsClient: any;
|
||||
private subscribers: Map<string, Set<(data: any) => void>> = new Map();
|
||||
|
||||
constructor(config: { kafka?: any; nats?: any }) {
|
||||
super();
|
||||
// TODO: Initialize Kafka or NATS client based on config
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an event to the event bus
|
||||
*/
|
||||
async publish(topic: string, event: EventEnvelope): Promise<void> {
|
||||
// TODO: Publish to Kafka or NATS
|
||||
// Validate event schema before publishing
|
||||
this.emit('published', { topic, event });
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to events from a topic
|
||||
*/
|
||||
subscribe(topic: string): AsyncIterator<any> {
|
||||
// TODO: Return async iterator for GraphQL subscriptions
|
||||
const iterator = this.createAsyncIterator(topic);
|
||||
return iterator;
|
||||
}
|
||||
|
||||
private createAsyncIterator(topic: string): AsyncIterator<any> {
|
||||
// TODO: Create async iterator that yields events from topic
|
||||
return {
|
||||
async next() {
|
||||
// TODO: Wait for next event from topic
|
||||
return { done: false, value: null };
|
||||
},
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connections
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// TODO: Close Kafka/NATS connections
|
||||
}
|
||||
}
|
||||
|
||||
export const eventBusClient = new EventBusClient({
|
||||
// TODO: Load config from environment
|
||||
});
|
||||
|
||||
16
api/shared/events/package.json
Normal file
16
api/shared/events/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@emoney/events",
|
||||
"version": "1.0.0",
|
||||
"description": "Event bus client for eMoney API",
|
||||
"main": "event-bus.js",
|
||||
"types": "event-bus.d.ts",
|
||||
"dependencies": {
|
||||
"kafkajs": "^2.2.4",
|
||||
"nats": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
18
api/shared/events/tsconfig.json
Normal file
18
api/shared/events/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
16
api/shared/validation/package.json
Normal file
16
api/shared/validation/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@emoney/validation",
|
||||
"version": "1.0.0",
|
||||
"description": "Schema validation utilities for eMoney API",
|
||||
"main": "schema-validator.js",
|
||||
"types": "schema-validator.d.ts",
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
133
api/shared/validation/schema-validator.ts
Normal file
133
api/shared/validation/schema-validator.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Schema validation utilities using Ajv
|
||||
* Validates JSON payloads against canonical JSON Schemas
|
||||
*/
|
||||
|
||||
import Ajv from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||
addFormats(ajv);
|
||||
|
||||
// Schema cache
|
||||
const schemaCache = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* Load a JSON Schema from the schemas directory
|
||||
*/
|
||||
export function loadSchema(schemaName: string, version: string = 'v1'): any {
|
||||
const cacheKey = `${version}/${schemaName}`;
|
||||
|
||||
if (schemaCache.has(cacheKey)) {
|
||||
return schemaCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const schemaPath = join(
|
||||
__dirname,
|
||||
'../../packages/schemas/jsonschema',
|
||||
`${schemaName}.json`
|
||||
);
|
||||
|
||||
try {
|
||||
const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
|
||||
schemaCache.set(cacheKey, schema);
|
||||
return schema;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load schema ${schemaName}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an enum schema
|
||||
*/
|
||||
export function loadEnumSchema(enumName: string): any {
|
||||
const cacheKey = `enum/${enumName}`;
|
||||
|
||||
if (schemaCache.has(cacheKey)) {
|
||||
return schemaCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const schemaPath = join(
|
||||
__dirname,
|
||||
'../../packages/schemas/enums',
|
||||
`${enumName}.json`
|
||||
);
|
||||
|
||||
try {
|
||||
const schema = JSON.parse(readFileSync(schemaPath, 'utf-8'));
|
||||
schemaCache.set(cacheKey, schema);
|
||||
return schema;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load enum schema ${enumName}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a JSON object against a schema
|
||||
*/
|
||||
export function validate<T = any>(
|
||||
schemaName: string,
|
||||
data: unknown,
|
||||
version: string = 'v1'
|
||||
): { valid: boolean; data?: T; errors?: any[] } {
|
||||
const schema = loadSchema(schemaName, version);
|
||||
const validate = ajv.compile(schema);
|
||||
|
||||
const valid = validate(data);
|
||||
|
||||
if (valid) {
|
||||
return { valid: true, data: data as T };
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
errors: validate.errors || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate against an enum schema
|
||||
*/
|
||||
export function validateEnum(
|
||||
enumName: string,
|
||||
value: unknown
|
||||
): { valid: boolean; errors?: any[] } {
|
||||
const schema = loadEnumSchema(enumName);
|
||||
const validate = ajv.compile(schema);
|
||||
|
||||
const valid = validate(value);
|
||||
|
||||
if (valid) {
|
||||
return { valid: true };
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
errors: validate.errors || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validator function for a specific schema
|
||||
*/
|
||||
export function createValidator<T = any>(schemaName: string, version: string = 'v1') {
|
||||
return (data: unknown): { valid: boolean; data?: T; errors?: any[] } => {
|
||||
return validate<T>(schemaName, data, version);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check schema compatibility between versions
|
||||
*/
|
||||
export function checkCompatibility(
|
||||
oldVersion: string,
|
||||
newVersion: string,
|
||||
schemaName: string
|
||||
): { compatible: boolean; breakingChanges?: string[] } {
|
||||
// TODO: Implement schema compatibility checking
|
||||
// This would compare schemas and detect breaking changes
|
||||
return { compatible: true };
|
||||
}
|
||||
|
||||
19
api/shared/validation/tsconfig.json
Normal file
19
api/shared/validation/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user