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,
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user