- 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
455 lines
15 KiB
TypeScript
455 lines
15 KiB
TypeScript
/**
|
|
* 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,
|
|
}
|
|
);
|