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,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,
}
);

View 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"
}
}

View 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
});

View 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"
}
}

View 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"]
}

View 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"
}
}

View 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 };
}

View 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"]
}