Convert contracts and frontend to git submodules

This commit is contained in:
defiQUG
2025-12-03 21:33:29 -08:00
parent 507d9a35b1
commit a0d7bf2e3c
112 changed files with 8 additions and 22544 deletions

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "contracts"]
path = contracts
url = https://github.com/defiQUG/asle-contracts.git
[submodule "frontend"]
path = frontend
url = https://github.com/defiQUG/asle-frontend.git

1
contracts Submodule

Submodule contracts added at 1a79ea1697

16
contracts/.gitignore vendored
View File

@@ -1,16 +0,0 @@
# Foundry
out/
cache_forge/
broadcast/
lib/
# Dependencies
node_modules/
# Environment
.env
.env.local
# IDE
.idea/
.vscode/

View File

@@ -1,6 +0,0 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts

View File

@@ -1,114 +0,0 @@
# Foundry Setup for ASLE Contracts
## Migration from Hardhat to Foundry
The ASLE project has been migrated from Hardhat to Foundry for smart contract development.
## Installation
1. Install Foundry:
```bash
curl -L https://foundry.paradigm.xyz | bash
source ~/.bashrc
foundryup
```
2. Verify installation:
```bash
forge --version
cast --version
anvil --version
```
## Project Structure
```
contracts/
├── src/ # Source contracts
│ ├── core/ # Diamond and facets
│ ├── interfaces/ # Contract interfaces
│ └── libraries/ # Utility libraries
├── test/ # Test files (*.t.sol)
├── script/ # Deployment scripts (*.s.sol)
├── lib/ # Dependencies (git submodules)
└── foundry.toml # Foundry configuration
```
## Commands
### Build
```bash
forge build
```
### Test
```bash
forge test # Run all tests
forge test -vvv # Verbose output
forge test --gas-report # With gas reporting
forge coverage # Coverage report
```
### Deploy
```bash
# Local deployment (Anvil)
anvil
forge script script/Deploy.s.sol --broadcast
# Testnet/Mainnet
forge script script/Deploy.s.sol --rpc-url <RPC_URL> --broadcast --verify
```
### Format & Lint
```bash
forge fmt # Format code
forge fmt --check # Check formatting
```
## Dependencies
Dependencies are managed via git submodules in `lib/`:
- `forge-std` - Foundry standard library
- `openzeppelin-contracts` - OpenZeppelin contracts
Install new dependencies:
```bash
forge install <github-user>/<repo>
```
## Remappings
Remappings are configured in `foundry.toml`:
- `@openzeppelin/``lib/openzeppelin-contracts/`
- `forge-std/``lib/forge-std/src/`
## Differences from Hardhat
1. **Test Files**: Use `.t.sol` extension (Solidity) instead of `.ts` (TypeScript)
2. **Scripts**: Use `.s.sol` extension (Solidity) instead of JavaScript
3. **Dependencies**: Git submodules instead of npm packages
4. **Configuration**: `foundry.toml` instead of `hardhat.config.ts`
5. **Build Output**: `out/` directory instead of `artifacts/`
## Local Development
Start local node:
```bash
anvil
```
Deploy to local node:
```bash
forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast
```
## Environment Variables
Set in `.env` file:
```
PRIVATE_KEY=your_private_key
ETHERSCAN_API_KEY=your_etherscan_key
RPC_URL=your_rpc_url
```

View File

@@ -1,14 +0,0 @@
{
"lib/forge-std": {
"tag": {
"name": "v1.12.0",
"rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6"
}
},
"lib/openzeppelin-contracts": {
"tag": {
"name": "v5.5.0",
"rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565"
}
}
}

View File

@@ -1,39 +0,0 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
test = "test"
script = "script"
broadcast = "broadcast"
cache_path = "cache_forge"
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
# Solidity version
solc_version = "0.8.24"
optimizer = true
optimizer_runs = 200
via_ir = false
# Extra output
extra_output = ["abi", "evm.bytecode", "evm.deployedBytecode"]
extra_output_files = ["abi", "evm.bytecode", "evm.deployedBytecode"]
# Fuzz testing
fuzz = { runs = 256 }
# Remappings for dependencies
remappings = [
"@openzeppelin/=lib/openzeppelin-contracts/",
"@chainlink/=lib/chainlink/",
"forge-std/=lib/forge-std/src/"
]
# Network configurations
[rpc_endpoints]
localhost = "http://127.0.0.1:8545"
anvil = "http://127.0.0.1:8545"
[etherscan]
etherscan = { key = "${ETHERSCAN_API_KEY}" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
{
"name": "contracts",
"version": "1.0.0",
"description": "ASLE Smart Contracts using Foundry",
"scripts": {
"build": "forge build",
"test": "forge test",
"test:verbose": "forge test -vvv",
"test:gas": "forge test --gas-report",
"coverage": "forge coverage",
"lint": "forge fmt --check",
"format": "forge fmt",
"snapshot": "forge snapshot",
"script:deploy": "forge script script/Deploy.s.sol:DeployScript --broadcast --verify",
"script:multichain": "forge script script/DeployMultichain.s.sol:DeployMultichainScript",
"anvil": "anvil",
"clean": "forge clean"
},
"keywords": ["solidity", "foundry", "ethereum", "defi"],
"author": "",
"license": "MIT"
}

View File

@@ -1,109 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {Diamond} from "../src/core/Diamond.sol";
import {DiamondCutFacet} from "../src/core/facets/DiamondCutFacet.sol";
import {DiamondInit} from "../src/core/DiamondInit.sol";
import {LiquidityFacet} from "../src/core/facets/LiquidityFacet.sol";
import {VaultFacet} from "../src/core/facets/VaultFacet.sol";
import {ComplianceFacet} from "../src/core/facets/ComplianceFacet.sol";
import {CCIPFacet} from "../src/core/facets/CCIPFacet.sol";
import {GovernanceFacet} from "../src/core/facets/GovernanceFacet.sol";
import {SecurityFacet} from "../src/core/facets/SecurityFacet.sol";
import {RWAFacet} from "../src/core/facets/RWAFacet.sol";
import {IDiamondCut} from "../src/interfaces/IDiamondCut.sol";
/**
* @title DeployScript
* @notice Complete deployment script for ASLE Diamond with all facets
*/
contract DeployScript is Script {
function run() external {
address deployer = vm.envAddress("DEPLOYER_ADDRESS");
if (deployer == address(0)) {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
deployer = vm.addr(deployerPrivateKey);
} else {
vm.startBroadcast(deployer);
}
console.log("Deploying ASLE Diamond and Facets...");
console.log("Deployer:", deployer);
// Deploy Diamond
Diamond diamond = new Diamond();
console.log("Diamond deployed at:", address(diamond));
// Deploy Facets
DiamondCutFacet diamondCutFacet = new DiamondCutFacet();
console.log("DiamondCutFacet deployed at:", address(diamondCutFacet));
LiquidityFacet liquidityFacet = new LiquidityFacet();
console.log("LiquidityFacet deployed at:", address(liquidityFacet));
VaultFacet vaultFacet = new VaultFacet();
console.log("VaultFacet deployed at:", address(vaultFacet));
ComplianceFacet complianceFacet = new ComplianceFacet();
console.log("ComplianceFacet deployed at:", address(complianceFacet));
CCIPFacet ccipFacet = new CCIPFacet();
console.log("CCIPFacet deployed at:", address(ccipFacet));
GovernanceFacet governanceFacet = new GovernanceFacet();
console.log("GovernanceFacet deployed at:", address(governanceFacet));
SecurityFacet securityFacet = new SecurityFacet();
console.log("SecurityFacet deployed at:", address(securityFacet));
RWAFacet rwaFacet = new RWAFacet();
console.log("RWAFacet deployed at:", address(rwaFacet));
// Deploy DiamondInit
DiamondInit diamondInit = new DiamondInit();
console.log("DiamondInit deployed at:", address(diamondInit));
// Prepare diamond cuts
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](8);
// Get function selectors for each facet
cuts[0] = _getFacetCut(address(diamondCutFacet), _getSelectors("DiamondCutFacet"));
cuts[1] = _getFacetCut(address(liquidityFacet), _getSelectors("LiquidityFacet"));
cuts[2] = _getFacetCut(address(vaultFacet), _getSelectors("VaultFacet"));
cuts[3] = _getFacetCut(address(complianceFacet), _getSelectors("ComplianceFacet"));
cuts[4] = _getFacetCut(address(ccipFacet), _getSelectors("CCIPFacet"));
cuts[5] = _getFacetCut(address(governanceFacet), _getSelectors("GovernanceFacet"));
cuts[6] = _getFacetCut(address(securityFacet), _getSelectors("SecurityFacet"));
cuts[7] = _getFacetCut(address(rwaFacet), _getSelectors("RWAFacet"));
// Initialize Diamond
bytes memory initData = abi.encodeWithSelector(DiamondInit.init.selector, deployer);
// Perform diamond cut
IDiamondCut(address(diamond)).diamondCut(cuts, address(diamondInit), initData);
console.log("\n=== Deployment Summary ===");
console.log("Diamond:", address(diamond));
console.log("All facets added and initialized!");
console.log("Owner:", deployer);
vm.stopBroadcast();
}
function _getFacetCut(address facet, bytes4[] memory selectors) internal pure returns (IDiamondCut.FacetCut memory) {
return IDiamondCut.FacetCut({
facetAddress: facet,
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: selectors
});
}
function _getSelectors(string memory facetName) internal pure returns (bytes4[] memory) {
// This is a simplified version - in production, use FacetCutHelper or similar
// For now, return empty array - selectors should be added manually or via helper
bytes4[] memory selectors = new bytes4[](0);
return selectors;
}
}

View File

@@ -1,20 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {DeployScript} from "./Deploy.s.sol";
contract DeployMultichainScript is Script {
function run() external {
// This script would deploy to multiple chains
// In production, you would:
// 1. Get chain-specific RPC URLs
// 2. Deploy to each chain
// 3. Configure CCIP routers
// 4. Set up cross-chain connections
console.log("Multi-chain deployment script");
console.log("Configure chain-specific deployments in foundry.toml");
}
}

View File

@@ -1,39 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IDiamondCut} from "../src/interfaces/IDiamondCut.sol";
/**
* @title FacetCutHelper
* @notice Helper contract to get function selectors from facet contracts
*/
library FacetCutHelper {
function getSelectors(address facet) internal view returns (bytes4[] memory) {
bytes memory facetCode = _getCreationCode(facet);
return _extractSelectors(facetCode);
}
function _getCreationCode(address contractAddress) internal view returns (bytes memory) {
uint256 size;
assembly {
size := extcodesize(contractAddress)
}
bytes memory code = new bytes(size);
assembly {
extcodecopy(contractAddress, add(code, 0x20), 0, size)
}
return code;
}
function _extractSelectors(bytes memory bytecode) internal pure returns (bytes4[] memory) {
// Simplified selector extraction - in production use proper parsing
// This is a placeholder - actual implementation would parse bytecode
bytes4[] memory selectors = new bytes4[](100); // Max selectors
uint256 count = 0;
// This is a simplified version - proper implementation would parse the bytecode
// For now, return empty and require manual selector lists
return new bytes4[](0);
}
}

View File

@@ -1,96 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IDiamondCut} from "../interfaces/IDiamondCut.sol";
import {IDiamond} from "../interfaces/IDiamond.sol";
import {LibDiamond} from "../libraries/LibDiamond.sol";
// It is expected that this contract is customized if you want to deploy your own
// diamond. For example, you can set a modifier on the `diamondCut` function to
// restrict who can call it, add a method to do upgrades, etc.
// When no data for a facet function is provided, the function selector will be
// added to the diamond as a function that does nothing (revert).
contract Diamond is IDiamond {
// Find facet for function that is called and execute the
// function if a facet is found and return any value.
fallback() external payable {
// get facet from function selector
address facet = IDiamond(address(this)).facetAddress(msg.sig);
require(facet != address(0), "Diamond: Function does not exist");
// Execute external function from facet using delegatecall and return any value.
assembly {
// copy function selector and any arguments
calldatacopy(0, 0, calldatasize())
// execute function call using the facet
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
// get any return value
returndatacopy(0, 0, returndatasize())
// return any return value or error back to the caller
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
receive() external payable {
revert("Diamond: Does not accept Ether");
}
/// @notice Gets all facets and their selectors.
/// @return facets_ Facet
function facets() external view override returns (Facet[] memory facets_) {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint256 numFacets = ds.facetAddresses.length;
facets_ = new Facet[](numFacets);
for (uint256 i; i < numFacets; i++) {
address facetAddress_ = ds.facetAddresses[i];
facets_[i].facetAddress = facetAddress_;
facets_[i].functionSelectors = ds.facetFunctionSelectors[facetAddress_].functionSelectors;
}
}
/// @notice Gets all the function selectors provided by a facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet)
external
view
override
returns (bytes4[] memory facetFunctionSelectors_)
{
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetFunctionSelectors_ = ds.facetFunctionSelectors[_facet].functionSelectors;
}
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses()
external
view
override
returns (address[] memory facetAddresses_)
{
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddresses_ = ds.facetAddresses;
}
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector)
external
view
override
returns (address facetAddress_)
{
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
facetAddress_ = ds.selectorToFacetAndPosition[_functionSelector].facetAddress;
}
}

View File

@@ -1,36 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {LibDiamond} from "../libraries/LibDiamond.sol";
import {LibAccessControl} from "../libraries/LibAccessControl.sol";
import {LibReentrancyGuard} from "../libraries/LibReentrancyGuard.sol";
/**
* @title DiamondInit
* @notice Initialization contract for ASLE Diamond
* @dev This contract is called once during Diamond deployment to initialize storage
*/
contract DiamondInit {
/**
* @notice Initialize Diamond with default settings
* @param _initOwner Address to set as initial owner
*/
function init(address _initOwner) external {
// Initialize Diamond ownership
require(!LibDiamond.isInitialized(), "DiamondInit: Already initialized");
LibDiamond.setContractOwner(_initOwner);
// Initialize access control
LibAccessControl.initializeAccessControl(_initOwner);
// Initialize reentrancy guard
LibReentrancyGuard.initialize();
// Set default timelock delay (7 days)
LibAccessControl.setTimelockDelay(7 days);
// Enable timelock by default
LibAccessControl.setTimelockEnabled(true);
}
}

View File

@@ -1,272 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ICCIPFacet} from "../../interfaces/ICCIPFacet.sol";
import {ICCIPRouter} from "../../interfaces/ICCIPRouter.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
import {ILiquidityFacet} from "../../interfaces/ILiquidityFacet.sol";
import {IVaultFacet} from "../../interfaces/IVaultFacet.sol";
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
/**
* @title CCIPFacet
* @notice Cross-chain messaging via Chainlink CCIP with state synchronization
*/
contract CCIPFacet is ICCIPFacet {
struct CCIPStorage {
ICCIPRouter ccipRouter;
mapping(uint256 => uint64) chainSelectors; // chainId => chainSelector
mapping(uint64 => uint256) selectorToChain; // chainSelector => chainId
mapping(uint256 => bool) supportedChains;
mapping(bytes32 => bool) deliveredMessages;
mapping(bytes32 => uint256) messageTimestamps;
mapping(bytes32 => MessageStatus) messageStatuses;
address authorizedSender; // Authorized sender for cross-chain messages
}
enum MessageStatus {
Pending,
Delivered,
Failed
}
bytes32 private constant CCIP_STORAGE_POSITION = keccak256("asle.ccip.storage");
event MessageExecuted(bytes32 indexed messageId, MessageType messageType, bool success);
event ChainSelectorUpdated(uint256 chainId, uint64 selector);
function ccipStorage() internal pure returns (CCIPStorage storage cs) {
bytes32 position = CCIP_STORAGE_POSITION;
assembly {
cs.slot := position
}
}
modifier onlySupportedChain(uint256 chainId) {
require(ccipStorage().supportedChains[chainId], "CCIPFacet: Chain not supported");
_;
}
modifier onlyAuthorized() {
CCIPStorage storage cs = ccipStorage();
require(
msg.sender == cs.authorizedSender ||
cs.authorizedSender == address(0) ||
LibAccessControl.hasRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender),
"CCIPFacet: Unauthorized"
);
_;
}
// ============ Liquidity Sync ============
function sendLiquiditySync(
uint256 targetChainId,
uint256 poolId
) external override onlySupportedChain(targetChainId) returns (bytes32 messageId) {
// Fetch pool data from LiquidityFacet
ILiquidityFacet liquidityFacet = ILiquidityFacet(address(this));
ILiquidityFacet.Pool memory pool = liquidityFacet.getPool(poolId);
LiquiditySyncPayload memory payload = LiquiditySyncPayload({
poolId: poolId,
baseReserve: pool.baseReserve,
quoteReserve: pool.quoteReserve,
virtualBaseReserve: pool.virtualBaseReserve,
virtualQuoteReserve: pool.virtualQuoteReserve
});
bytes memory encodedPayload = abi.encode(MessageType.LiquiditySync, payload);
messageId = _sendCCIPMessage(
targetChainId,
MessageType.LiquiditySync,
encodedPayload
);
emit CCIPMessageSent(messageId, block.chainid, targetChainId, MessageType.LiquiditySync);
}
function sendVaultRebalance(
uint256 targetChainId,
uint256 vaultId,
uint256 amount,
address asset
) external override onlySupportedChain(targetChainId) returns (bytes32 messageId) {
VaultRebalancePayload memory payload = VaultRebalancePayload({
vaultId: vaultId,
targetChainId: targetChainId,
amount: amount,
asset: asset
});
bytes memory encodedPayload = abi.encode(MessageType.VaultRebalance, payload);
messageId = _sendCCIPMessage(
targetChainId,
MessageType.VaultRebalance,
encodedPayload
);
emit VaultRebalanced(vaultId, block.chainid, targetChainId, amount);
emit CCIPMessageSent(messageId, block.chainid, targetChainId, MessageType.VaultRebalance);
}
function sendPriceDeviationWarning(
uint256 targetChainId,
uint256 poolId,
uint256 deviation
) external override onlySupportedChain(targetChainId) returns (bytes32 messageId) {
ILiquidityFacet liquidityFacet = ILiquidityFacet(address(this));
uint256 currentPrice = liquidityFacet.getPrice(poolId);
PriceDeviationPayload memory payload = PriceDeviationPayload({
poolId: poolId,
price: currentPrice,
deviation: deviation,
timestamp: block.timestamp
});
bytes memory encodedPayload = abi.encode(MessageType.PriceDeviation, payload);
messageId = _sendCCIPMessage(
targetChainId,
MessageType.PriceDeviation,
encodedPayload
);
emit CCIPMessageSent(messageId, block.chainid, targetChainId, MessageType.PriceDeviation);
}
// ============ Message Handling ============
function handleCCIPMessage(
bytes32 messageId,
uint256 sourceChainId,
bytes calldata payload
) external override onlyAuthorized {
CCIPStorage storage cs = ccipStorage();
require(!cs.deliveredMessages[messageId], "CCIPFacet: Message already processed");
cs.deliveredMessages[messageId] = true;
cs.messageTimestamps[messageId] = block.timestamp;
cs.messageStatuses[messageId] = MessageStatus.Pending;
(MessageType messageType, bytes memory data) = abi.decode(payload, (MessageType, bytes));
bool success = false;
if (messageType == MessageType.LiquiditySync) {
LiquiditySyncPayload memory syncPayload = abi.decode(data, (LiquiditySyncPayload));
success = _handleLiquiditySync(syncPayload, sourceChainId);
} else if (messageType == MessageType.VaultRebalance) {
VaultRebalancePayload memory rebalancePayload = abi.decode(data, (VaultRebalancePayload));
success = _handleVaultRebalance(rebalancePayload, sourceChainId);
} else if (messageType == MessageType.PriceDeviation) {
PriceDeviationPayload memory pricePayload = abi.decode(data, (PriceDeviationPayload));
success = _handlePriceDeviation(pricePayload, sourceChainId);
}
cs.messageStatuses[messageId] = success ? MessageStatus.Delivered : MessageStatus.Failed;
emit CCIPMessageReceived(messageId, sourceChainId, messageType);
emit MessageExecuted(messageId, messageType, success);
}
// ============ Configuration ============
function setCCIPRouter(address router) external override {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
ccipStorage().ccipRouter = ICCIPRouter(router);
}
function setSupportedChain(uint256 chainId, bool supported) external override {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
ccipStorage().supportedChains[chainId] = supported;
}
function setChainSelector(uint256 chainId, uint64 selector) external {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
CCIPStorage storage cs = ccipStorage();
cs.chainSelectors[chainId] = selector;
cs.selectorToChain[selector] = chainId;
emit ChainSelectorUpdated(chainId, selector);
}
function setAuthorizedSender(address sender) external {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
ccipStorage().authorizedSender = sender;
}
// ============ View Functions ============
function isChainSupported(uint256 chainId) external view override returns (bool) {
return ccipStorage().supportedChains[chainId];
}
function getMessageStatus(bytes32 messageId) external view override returns (bool delivered, uint256 timestamp) {
CCIPStorage storage cs = ccipStorage();
delivered = cs.deliveredMessages[messageId];
timestamp = cs.messageTimestamps[messageId];
}
function getChainSelector(uint256 chainId) external view returns (uint64) {
return ccipStorage().chainSelectors[chainId];
}
// ============ Internal Functions ============
function _sendCCIPMessage(
uint256 targetChainId,
MessageType messageType,
bytes memory payload
) internal returns (bytes32) {
CCIPStorage storage cs = ccipStorage();
require(address(cs.ccipRouter) != address(0), "CCIPFacet: Router not set");
uint64 chainSelector = cs.chainSelectors[targetChainId];
require(chainSelector != 0, "CCIPFacet: Chain selector not set");
ICCIPRouter.EVM2AnyMessage memory message = ICCIPRouter.EVM2AnyMessage({
receiver: abi.encode(address(this)),
data: payload,
tokenAmounts: new ICCIPRouter.EVMTokenAmount[](0),
extraArgs: "",
feeToken: address(0)
});
uint256 fee = cs.ccipRouter.getFee(chainSelector, message);
require(msg.value >= fee, "CCIPFacet: Insufficient fee");
return cs.ccipRouter.ccipSend{value: fee}(chainSelector, message);
}
function _handleLiquiditySync(LiquiditySyncPayload memory payload, uint256 sourceChainId) internal returns (bool) {
try this._syncPoolState(payload) {
emit LiquiditySynced(payload.poolId, sourceChainId, payload.baseReserve, payload.quoteReserve);
return true;
} catch {
return false;
}
}
function _syncPoolState(LiquiditySyncPayload memory payload) external {
require(msg.sender == address(this), "CCIPFacet: Internal only");
// In production, this would update pool virtual reserves based on cross-chain state
// For now, we emit events and let the backend handle synchronization
}
function _handleVaultRebalance(VaultRebalancePayload memory payload, uint256 sourceChainId) internal returns (bool) {
// In production, this would trigger vault rebalancing logic
// For now, emit event for backend processing
emit VaultRebalanced(payload.vaultId, sourceChainId, payload.targetChainId, payload.amount);
return true;
}
function _handlePriceDeviation(PriceDeviationPayload memory payload, uint256 sourceChainId) internal returns (bool) {
// Trigger security alerts if deviation is significant
if (payload.deviation > 500) { // 5% deviation threshold
ISecurityFacet securityFacet = ISecurityFacet(address(this));
// Could trigger circuit breaker or alert
}
return true;
}
}

View File

@@ -1,92 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IChainConfigFacet} from "../../interfaces/IChainConfigFacet.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
/**
* @title ChainConfigFacet
* @notice Manages chain-specific configurations for multi-chain operations
*/
contract ChainConfigFacet is IChainConfigFacet {
struct ChainConfigStorage {
mapping(uint256 => ChainConfig) chainConfigs;
mapping(uint256 => bool) activeChains;
}
bytes32 private constant CHAIN_CONFIG_STORAGE_POSITION = keccak256("asle.chainconfig.storage");
function chainConfigStorage() internal pure returns (ChainConfigStorage storage ccs) {
bytes32 position = CHAIN_CONFIG_STORAGE_POSITION;
assembly {
ccs.slot := position
}
}
modifier onlyAdmin() {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
_;
}
function setChainConfig(
uint256 chainId,
string calldata name,
address nativeToken,
string calldata explorerUrl,
uint256 gasLimit,
uint256 messageTimeout
) external override onlyAdmin {
ChainConfigStorage storage ccs = chainConfigStorage();
ccs.chainConfigs[chainId] = ChainConfig({
chainId: chainId,
name: name,
nativeToken: nativeToken,
explorerUrl: explorerUrl,
gasLimit: gasLimit,
messageTimeout: messageTimeout,
active: ccs.activeChains[chainId] // Preserve existing active status
});
emit ChainConfigUpdated(chainId, name, ccs.activeChains[chainId]);
}
function getChainConfig(uint256 chainId) external view override returns (ChainConfig memory) {
ChainConfigStorage storage ccs = chainConfigStorage();
ChainConfig memory config = ccs.chainConfigs[chainId];
require(config.chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
return config;
}
function setChainActive(uint256 chainId, bool active) external override onlyAdmin {
ChainConfigStorage storage ccs = chainConfigStorage();
require(ccs.chainConfigs[chainId].chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
ccs.activeChains[chainId] = active;
ccs.chainConfigs[chainId].active = active;
emit ChainConfigUpdated(chainId, ccs.chainConfigs[chainId].name, active);
}
function setChainGasLimit(uint256 chainId, uint256 gasLimit) external override onlyAdmin {
ChainConfigStorage storage ccs = chainConfigStorage();
require(ccs.chainConfigs[chainId].chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
ccs.chainConfigs[chainId].gasLimit = gasLimit;
emit ChainGasLimitUpdated(chainId, gasLimit);
}
function setChainTimeout(uint256 chainId, uint256 timeout) external override onlyAdmin {
ChainConfigStorage storage ccs = chainConfigStorage();
require(ccs.chainConfigs[chainId].chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
ccs.chainConfigs[chainId].messageTimeout = timeout;
emit ChainTimeoutUpdated(chainId, timeout);
}
function isChainActive(uint256 chainId) external view override returns (bool) {
ChainConfigStorage storage ccs = chainConfigStorage();
return ccs.activeChains[chainId];
}
}

View File

@@ -1,268 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
contract ComplianceFacet is IComplianceFacet {
struct ComplianceStorage {
mapping(address => UserCompliance) userCompliance;
mapping(uint256 => ComplianceMode) vaultComplianceMode;
mapping(address => bool) ofacSanctioned; // OFAC sanctions list
mapping(bytes32 => bool) travelRuleTransactions; // FATF Travel Rule transaction tracking
mapping(address => uint256) lastAuditTime;
mapping(address => uint256) transactionCount; // Track transaction count per address
mapping(address => uint256) dailyVolume; // Daily transaction volume
mapping(address => uint256) lastDayReset; // Last day reset timestamp
bool iso20022Enabled;
bool travelRuleEnabled;
bool automaticOFACCheck;
uint256 travelRuleThreshold; // Minimum amount for Travel Rule (in wei)
}
bytes32 private constant COMPLIANCE_STORAGE_POSITION = keccak256("asle.compliance.storage");
function complianceStorage() internal pure returns (ComplianceStorage storage cs) {
bytes32 position = COMPLIANCE_STORAGE_POSITION;
assembly {
cs.slot := position
}
}
modifier onlyComplianceAdmin() {
LibAccessControl.requireRole(LibAccessControl.COMPLIANCE_ADMIN_ROLE, msg.sender);
_;
}
modifier requireCompliance(address user, ComplianceMode requiredMode) {
require(canAccess(user, requiredMode), "ComplianceFacet: Compliance check failed");
_;
}
function setUserComplianceMode(
address user,
ComplianceMode mode
) external override onlyComplianceAdmin {
ComplianceStorage storage cs = complianceStorage();
cs.userCompliance[user].mode = mode;
cs.userCompliance[user].active = true;
emit ComplianceModeSet(user, mode);
}
function verifyKYC(address user, bool verified) external override onlyComplianceAdmin {
ComplianceStorage storage cs = complianceStorage();
cs.userCompliance[user].kycVerified = verified;
emit KYCVerified(user, verified);
}
function verifyAML(address user, bool verified) external override onlyComplianceAdmin {
ComplianceStorage storage cs = complianceStorage();
cs.userCompliance[user].amlVerified = verified;
}
function getUserCompliance(
address user
) external view override returns (UserCompliance memory) {
return complianceStorage().userCompliance[user];
}
function canAccess(
address user,
ComplianceMode requiredMode
) external view override returns (bool) {
ComplianceStorage storage cs = complianceStorage();
UserCompliance memory userComp = cs.userCompliance[user];
if (!userComp.active) {
return requiredMode == ComplianceMode.Decentralized;
}
if (requiredMode == ComplianceMode.Decentralized) {
return true; // Anyone can access decentralized mode
}
if (requiredMode == ComplianceMode.Fintech) {
return userComp.mode == ComplianceMode.Fintech || userComp.mode == ComplianceMode.Regulated;
}
if (requiredMode == ComplianceMode.Regulated) {
return userComp.mode == ComplianceMode.Regulated &&
userComp.kycVerified &&
userComp.amlVerified;
}
return false;
}
function setVaultComplianceMode(
uint256 vaultId,
ComplianceMode mode
) external override onlyComplianceAdmin {
ComplianceStorage storage cs = complianceStorage();
cs.vaultComplianceMode[vaultId] = mode;
}
function getVaultComplianceMode(
uint256 vaultId
) external view override returns (ComplianceMode) {
ComplianceStorage storage cs = complianceStorage();
return cs.vaultComplianceMode[vaultId];
}
// Phase 3: Enhanced Compliance Functions
function checkOFACSanctions(address user) external view returns (bool) {
return complianceStorage().ofacSanctioned[user];
}
function setOFACSanctioned(address user, bool sanctioned) external onlyComplianceAdmin {
complianceStorage().ofacSanctioned[user] = sanctioned;
emit IComplianceFacet.OFACCheck(user, sanctioned);
}
function recordTravelRule(
address from,
address to,
uint256 amount,
bytes32 transactionHash
) external {
ComplianceStorage storage cs = complianceStorage();
require(cs.travelRuleEnabled, "ComplianceFacet: Travel Rule not enabled");
require(amount >= cs.travelRuleThreshold, "ComplianceFacet: Amount below Travel Rule threshold");
cs.travelRuleTransactions[transactionHash] = true;
emit IComplianceFacet.TravelRuleCompliance(from, to, amount, transactionHash);
}
function getTravelRuleStatus(bytes32 transactionHash) external view returns (bool) {
return complianceStorage().travelRuleTransactions[transactionHash];
}
function setTravelRuleThreshold(uint256 threshold) external onlyComplianceAdmin {
complianceStorage().travelRuleThreshold = threshold;
}
function recordISO20022Message(
address user,
string calldata messageType,
bytes32 messageId
) external onlyComplianceAdmin {
ComplianceStorage storage cs = complianceStorage();
require(cs.iso20022Enabled, "ComplianceFacet: ISO 20022 not enabled");
// Use events instead of storage for ISO messages (storage optimization)
emit IComplianceFacet.ISO20022Message(user, messageType, messageId);
}
function enableISO20022(bool enabled) external onlyComplianceAdmin {
complianceStorage().iso20022Enabled = enabled;
}
function enableTravelRule(bool enabled) external onlyComplianceAdmin {
complianceStorage().travelRuleEnabled = enabled;
}
function recordAudit(address user) external onlyComplianceAdmin {
complianceStorage().lastAuditTime[user] = block.timestamp;
}
function getLastAuditTime(address user) external view returns (uint256) {
return complianceStorage().lastAuditTime[user];
}
function validateTransaction(
address from,
address to,
uint256 amount
) external view returns (bool) {
ComplianceStorage storage cs = complianceStorage();
// Automatic OFAC sanctions check
if (cs.automaticOFACCheck || cs.ofacSanctioned[from] || cs.ofacSanctioned[to]) {
if (cs.ofacSanctioned[from] || cs.ofacSanctioned[to]) {
return false;
}
}
// Check compliance modes
UserCompliance memory fromComp = cs.userCompliance[from];
UserCompliance memory toComp = cs.userCompliance[to];
// Both parties must meet minimum compliance requirements
if (fromComp.mode == ComplianceMode.Regulated || toComp.mode == ComplianceMode.Regulated) {
return fromComp.kycVerified && fromComp.amlVerified &&
toComp.kycVerified && toComp.amlVerified;
}
// Check Travel Rule requirements
if (cs.travelRuleEnabled && amount >= cs.travelRuleThreshold) {
// Travel Rule compliance should be checked separately via recordTravelRule
// This is a basic validation
}
return true;
}
/**
* @notice Automatic OFAC check on transaction (called by other facets)
*/
function performAutomaticOFACCheck(address user) external returns (bool) {
ComplianceStorage storage cs = complianceStorage();
if (cs.automaticOFACCheck) {
// In production, this would call an external service or oracle
// For now, just check the stored list
return !cs.ofacSanctioned[user];
}
return true;
}
/**
* @notice Batch set OFAC sanctions
*/
function batchSetOFACSanctions(address[] calldata users, bool[] calldata sanctioned) external onlyComplianceAdmin {
require(users.length == sanctioned.length, "ComplianceFacet: Arrays length mismatch");
ComplianceStorage storage cs = complianceStorage();
for (uint i = 0; i < users.length; i++) {
cs.ofacSanctioned[users[i]] = sanctioned[i];
emit IComplianceFacet.OFACCheck(users[i], sanctioned[i]);
}
}
/**
* @notice Enable/disable automatic OFAC checking
*/
function setAutomaticOFACCheck(bool enabled) external onlyComplianceAdmin {
complianceStorage().automaticOFACCheck = enabled;
}
/**
* @notice Get transaction statistics for address
*/
function getTransactionStats(address user) external view returns (uint256 count, uint256 dailyVol) {
ComplianceStorage storage cs = complianceStorage();
// Reset daily volume if new day
if (block.timestamp >= cs.lastDayReset[user] + 1 days) {
dailyVol = 0;
} else {
dailyVol = cs.dailyVolume[user];
}
return (cs.transactionCount[user], dailyVol);
}
/**
* @notice Record transaction for compliance tracking
*/
function recordTransaction(address from, address to, uint256 amount) external {
ComplianceStorage storage cs = complianceStorage();
// Reset daily volume if new day
if (block.timestamp >= cs.lastDayReset[from] + 1 days) {
cs.dailyVolume[from] = 0;
cs.lastDayReset[from] = block.timestamp;
}
cs.transactionCount[from]++;
cs.dailyVolume[from] += amount;
}
}

View File

@@ -1,24 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IDiamondCut} from "../../interfaces/IDiamondCut.sol";
import {LibDiamond, LibDiamondCut} from "../../libraries/LibDiamond.sol";
contract DiamondCutFacet is IDiamondCut {
/// @notice Add/replace/remove any number of functions and optionally execute
/// a function with delegatecall
/// @param _diamondCut Contains the facet addresses and function selectors
/// @param _init The address of the contract or facet to execute _calldata
/// @param _calldata A function call, including function selector and arguments
/// _calldata is executed with delegatecall on _init
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external override {
LibDiamond.enforceIsContractOwner();
LibDiamond.diamondCut(_diamondCut, _init, _calldata);
}
}

View File

@@ -1,394 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IGovernanceFacet} from "../../interfaces/IGovernanceFacet.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {LibDiamond} from "../../libraries/LibDiamond.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
import {IDiamondCut} from "../../interfaces/IDiamondCut.sol";
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
contract GovernanceFacet is IGovernanceFacet {
using SafeERC20 for IERC20;
struct GovernanceStorage {
mapping(uint256 => Proposal) proposals;
uint256 proposalCount;
address governanceToken; // ERC-20 token for voting
uint256 quorumThreshold; // Minimum votes required
uint256 votingPeriod; // Default voting period in seconds
uint256 timelockDelay; // Delay before execution
mapping(uint256 => uint256) proposalTimelocks; // proposalId => execution time
mapping(address => uint256) treasuryBalances; // token => balance
uint256 minProposalThreshold; // Minimum tokens required to create proposal
mapping(address => address) delegations; // delegator => delegatee
mapping(address => uint256) checkpoints; // account => voting power checkpoint
}
bytes32 private constant GOVERNANCE_STORAGE_POSITION = keccak256("asle.governance.storage");
function governanceStorage() internal pure returns (GovernanceStorage storage gs) {
bytes32 position = GOVERNANCE_STORAGE_POSITION;
assembly {
gs.slot := position
}
}
modifier onlyProposer() {
GovernanceStorage storage gs = governanceStorage();
if (gs.governanceToken != address(0)) {
uint256 balance = IERC20(gs.governanceToken).balanceOf(msg.sender);
require(balance >= gs.minProposalThreshold, "GovernanceFacet: Insufficient tokens to propose");
}
_;
}
function createProposal(
ProposalType proposalType,
string calldata description,
bytes calldata data,
uint256 votingPeriod
) external override onlyProposer returns (uint256 proposalId) {
GovernanceStorage storage gs = governanceStorage();
proposalId = gs.proposalCount;
gs.proposalCount++;
Proposal storage proposal = gs.proposals[proposalId];
proposal.id = proposalId;
proposal.proposalType = proposalType;
proposal.status = ProposalStatus.Pending;
proposal.proposer = msg.sender;
proposal.description = description;
proposal.data = data;
proposal.startTime = block.timestamp;
proposal.endTime = block.timestamp + (votingPeriod > 0 ? votingPeriod : gs.votingPeriod);
proposal.forVotes = 0;
proposal.againstVotes = 0;
// Auto-activate if voting period is immediate
if (votingPeriod == 0) {
proposal.status = ProposalStatus.Active;
}
emit ProposalCreated(proposalId, proposalType, msg.sender);
}
function vote(uint256 proposalId, bool support) external override {
GovernanceStorage storage gs = governanceStorage();
Proposal storage proposal = gs.proposals[proposalId];
require(proposal.status == ProposalStatus.Active, "GovernanceFacet: Proposal not active");
require(block.timestamp <= proposal.endTime, "GovernanceFacet: Voting period ended");
require(!proposal.hasVoted[msg.sender], "GovernanceFacet: Already voted");
address voter = msg.sender;
address delegatee = gs.delegations[voter];
// If voting power is delegated, the delegatee should vote
if (delegatee != address(0) && delegatee != voter) {
require(msg.sender == delegatee, "GovernanceFacet: Only delegatee can vote");
voter = delegatee;
}
uint256 votingPower = _getVotingPower(voter);
require(votingPower > 0, "GovernanceFacet: No voting power");
proposal.hasVoted[msg.sender] = true;
if (support) {
proposal.forVotes += votingPower;
} else {
proposal.againstVotes += votingPower;
}
emit VoteCast(proposalId, msg.sender, support, votingPower);
// Check if proposal can be passed
_checkProposalStatus(proposalId);
}
function executeProposal(uint256 proposalId) external override {
GovernanceStorage storage gs = governanceStorage();
Proposal storage proposal = gs.proposals[proposalId];
require(proposal.status == ProposalStatus.Passed, "GovernanceFacet: Proposal not passed");
require(block.timestamp > proposal.endTime, "GovernanceFacet: Voting still active");
// Check timelock
uint256 executionTime = gs.proposalTimelocks[proposalId];
if (executionTime > 0) {
require(block.timestamp >= executionTime, "GovernanceFacet: Timelock not expired");
}
proposal.status = ProposalStatus.Executed;
// Execute actions if multi-action proposal
if (proposal.actions.length > 0) {
for (uint256 i = 0; i < proposal.actions.length; i++) {
Action storage action = proposal.actions[i];
require(!action.executed, "GovernanceFacet: Action already executed");
(bool success, ) = action.target.call{value: action.value}(action.data);
require(success, "GovernanceFacet: Action execution failed");
action.executed = true;
}
} else {
// Execute proposal based on type (legacy single-action)
if (proposal.proposalType == ProposalType.TreasuryWithdrawal) {
_executeTreasuryWithdrawal(proposal.data);
} else if (proposal.proposalType == ProposalType.FacetUpgrade) {
_executeFacetUpgrade(proposal.data);
} else if (proposal.proposalType == ProposalType.EmergencyPause) {
_executeEmergencyPause(proposal.data);
} else if (proposal.proposalType == ProposalType.ComplianceChange) {
_executeComplianceChange(proposal.data);
} else if (proposal.proposalType == ProposalType.ParameterChange) {
_executeParameterChange(proposal.data);
}
}
emit ProposalExecuted(proposalId);
}
/**
* Create multi-action proposal
*/
function createMultiActionProposal(
string calldata description,
Action[] calldata actions,
uint256 votingPeriod
) external onlyProposer returns (uint256 proposalId) {
GovernanceStorage storage gs = governanceStorage();
proposalId = gs.proposalCount;
gs.proposalCount++;
Proposal storage proposal = gs.proposals[proposalId];
proposal.id = proposalId;
proposal.proposalType = ProposalType.ParameterChange; // Default type for multi-action
proposal.status = ProposalStatus.Pending;
proposal.proposer = msg.sender;
proposal.description = description;
proposal.startTime = block.timestamp;
proposal.endTime = block.timestamp + (votingPeriod > 0 ? votingPeriod : gs.votingPeriod);
proposal.forVotes = 0;
proposal.againstVotes = 0;
// Store actions
for (uint256 i = 0; i < actions.length; i++) {
proposal.actions.push(actions[i]);
}
if (votingPeriod == 0) {
proposal.status = ProposalStatus.Active;
}
emit ProposalCreated(proposalId, proposal.proposalType, msg.sender);
}
function cancelProposal(uint256 proposalId) external {
GovernanceStorage storage gs = governanceStorage();
Proposal storage proposal = gs.proposals[proposalId];
require(proposal.proposer == msg.sender || LibAccessControl.hasRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender),
"GovernanceFacet: Not authorized");
require(proposal.status == ProposalStatus.Active || proposal.status == ProposalStatus.Pending,
"GovernanceFacet: Cannot cancel");
proposal.status = ProposalStatus.Rejected;
}
function proposeTreasuryWithdrawal(
address recipient,
uint256 amount,
address token,
string calldata reason
) external override returns (uint256 proposalId) {
bytes memory data = abi.encode(recipient, amount, token, reason);
return this.createProposal(ProposalType.TreasuryWithdrawal, reason, data, 0);
}
function getProposal(uint256 proposalId) external view override returns (
uint256 id,
ProposalType proposalType,
ProposalStatus status,
address proposer,
uint256 forVotes,
uint256 againstVotes,
uint256 startTime,
uint256 endTime
) {
Proposal storage proposal = governanceStorage().proposals[proposalId];
return (
proposal.id,
proposal.proposalType,
proposal.status,
proposal.proposer,
proposal.forVotes,
proposal.againstVotes,
proposal.startTime,
proposal.endTime
);
}
function getTreasuryBalance(address token) external view override returns (uint256) {
return governanceStorage().treasuryBalances[token];
}
function _checkProposalStatus(uint256 proposalId) internal {
GovernanceStorage storage gs = governanceStorage();
Proposal storage proposal = gs.proposals[proposalId];
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
if (totalVotes >= gs.quorumThreshold) {
if (proposal.forVotes > proposal.againstVotes) {
proposal.status = ProposalStatus.Passed;
// Set timelock
if (gs.timelockDelay > 0) {
gs.proposalTimelocks[proposalId] = block.timestamp + gs.timelockDelay;
}
} else {
proposal.status = ProposalStatus.Rejected;
}
}
}
function _getVotingPower(address voter) internal view returns (uint256) {
GovernanceStorage storage gs = governanceStorage();
address delegatee = gs.delegations[voter];
address account = delegatee != address(0) ? delegatee : voter;
if (gs.governanceToken != address(0)) {
return IERC20(gs.governanceToken).balanceOf(account);
}
return 1; // Default: 1 vote per address
}
// ============ Delegation Functions ============
function delegate(address delegatee) external override {
GovernanceStorage storage gs = governanceStorage();
address currentDelegate = gs.delegations[msg.sender];
uint256 previousBalance = _getVotingPower(msg.sender);
gs.delegations[msg.sender] = delegatee;
uint256 newBalance = _getVotingPower(msg.sender);
emit DelegationChanged(msg.sender, delegatee, previousBalance, newBalance);
}
function delegateBySig(
address delegator,
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external override {
// EIP-712 signature verification would go here
// For now, simplified implementation
require(block.timestamp <= expiry, "GovernanceFacet: Signature expired");
GovernanceStorage storage gs = governanceStorage();
address currentDelegate = gs.delegations[delegator];
uint256 previousBalance = _getVotingPower(delegator);
gs.delegations[delegator] = delegatee;
uint256 newBalance = _getVotingPower(delegator);
emit DelegationChanged(delegator, delegatee, previousBalance, newBalance);
}
function delegates(address delegator) external view override returns (address) {
GovernanceStorage storage gs = governanceStorage();
address delegatee = gs.delegations[delegator];
return delegatee != address(0) ? delegatee : delegator;
}
function getCurrentVotes(address account) external view override returns (uint256) {
return _getVotingPower(account);
}
function getPriorVotes(address account, uint256 blockNumber) external view override returns (uint256) {
// Simplified: return current votes (full implementation would use checkpoints)
return _getVotingPower(account);
}
function _executeFacetUpgrade(bytes memory data) internal {
(IDiamondCut.FacetCut[] memory cuts, address init, bytes memory initData) =
abi.decode(data, (IDiamondCut.FacetCut[], address, bytes));
// Call DiamondCutFacet through Diamond
IDiamondCut(address(this)).diamondCut(cuts, init, initData);
}
function _executeEmergencyPause(bytes memory data) internal {
ISecurityFacet.PauseReason reason = abi.decode(data, (ISecurityFacet.PauseReason));
ISecurityFacet(address(this)).pauseSystem(reason);
}
function _executeComplianceChange(bytes memory data) internal {
// Compliance changes would be executed here
// This is a placeholder for compliance-related actions
}
function _executeParameterChange(bytes memory data) internal {
// Parameter changes would be executed here
// This is a placeholder for parameter updates
}
function _executeTreasuryWithdrawal(bytes memory data) internal {
(address recipient, uint256 amount, address token, ) = abi.decode(data, (address, uint256, address, string));
GovernanceStorage storage gs = governanceStorage();
require(gs.treasuryBalances[token] >= amount, "GovernanceFacet: Insufficient treasury balance");
gs.treasuryBalances[token] -= amount;
if (token == address(0)) {
(bool success, ) = payable(recipient).call{value: amount}("");
require(success, "GovernanceFacet: ETH transfer failed");
} else {
IERC20(token).safeTransfer(recipient, amount);
}
// Sync treasury balance
_syncTreasuryBalance(token);
emit TreasuryWithdrawal(recipient, amount, token);
}
function _syncTreasuryBalance(address token) internal {
GovernanceStorage storage gs = governanceStorage();
if (token == address(0)) {
gs.treasuryBalances[token] = address(this).balance;
} else {
gs.treasuryBalances[token] = IERC20(token).balanceOf(address(this));
}
}
// ============ Admin Functions ============
function setGovernanceToken(address token) external {
LibAccessControl.requireRole(LibAccessControl.GOVERNANCE_ADMIN_ROLE, msg.sender);
governanceStorage().governanceToken = token;
}
function setQuorumThreshold(uint256 threshold) external {
LibAccessControl.requireRole(LibAccessControl.GOVERNANCE_ADMIN_ROLE, msg.sender);
governanceStorage().quorumThreshold = threshold;
}
function setTimelockDelay(uint256 delay) external {
LibAccessControl.requireRole(LibAccessControl.GOVERNANCE_ADMIN_ROLE, msg.sender);
governanceStorage().timelockDelay = delay;
}
function syncTreasuryBalance(address token) external {
_syncTreasuryBalance(token);
}
}

View File

@@ -1,444 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ILiquidityFacet} from "../../interfaces/ILiquidityFacet.sol";
import {PMMMath} from "../../libraries/PMMMath.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
import {LibReentrancyGuard} from "../../libraries/LibReentrancyGuard.sol";
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
import {IOracle} from "../../interfaces/IOracle.sol";
/**
* @title LiquidityFacet
* @notice Enhanced liquidity facet with PMM, fees, access control, and compliance
* @dev This facet manages DODO PMM pools with comprehensive security features
*/
contract LiquidityFacet is ILiquidityFacet {
using PMMMath for uint256;
using SafeERC20 for IERC20;
struct LiquidityStorage {
mapping(uint256 => Pool) pools;
mapping(uint256 => PoolConfig) poolConfigs; // poolId => config
mapping(uint256 => mapping(address => uint256)) lpBalances; // poolId => user => lpShares
mapping(uint256 => uint256) totalLPSupply; // poolId => total LP supply
mapping(uint256 => address) priceFeeds; // poolId => Chainlink price feed
mapping(address => uint256) protocolFees; // token => accumulated fees
mapping(uint256 => mapping(address => uint256)) poolFees; // poolId => token => accumulated fees
uint256 poolCount;
uint256 defaultTradingFee; // Default trading fee in basis points (e.g., 30 = 0.3%)
uint256 defaultProtocolFee; // Default protocol fee in basis points
address feeCollector; // Address to receive protocol fees
}
struct PoolConfig {
uint256 tradingFee; // Trading fee in basis points (0-10000)
uint256 protocolFee; // Protocol fee in basis points (0-10000)
bool paused; // Pool-specific pause
address oracle; // Chainlink price feed address
uint256 lastOracleUpdate; // Timestamp of last oracle update
uint256 oracleUpdateInterval; // Minimum interval between oracle updates
}
bytes32 private constant LIQUIDITY_STORAGE_POSITION = keccak256("asle.liquidity.storage");
// Events
event PoolPaused(uint256 indexed poolId, bool paused);
event OraclePriceUpdated(uint256 indexed poolId, uint256 newPrice);
event TradingFeeCollected(uint256 indexed poolId, address token, uint256 amount);
event ProtocolFeeCollected(address token, uint256 amount);
event FeeCollectorUpdated(address newFeeCollector);
event PoolFeeUpdated(uint256 indexed poolId, uint256 tradingFee, uint256 protocolFee);
function liquidityStorage() internal pure returns (LiquidityStorage storage ls) {
bytes32 position = LIQUIDITY_STORAGE_POSITION;
assembly {
ls.slot := position
}
}
// ============ Access Control Modifiers ============
modifier onlyPoolCreator() {
LibAccessControl.requireRole(LibAccessControl.POOL_CREATOR_ROLE, msg.sender);
_;
}
modifier onlyAdmin() {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
_;
}
modifier whenPoolNotPaused(uint256 poolId) {
LiquidityStorage storage ls = liquidityStorage();
require(!ls.poolConfigs[poolId].paused, "LiquidityFacet: Pool is paused");
_;
}
modifier nonReentrant() {
LibReentrancyGuard.enter();
_;
LibReentrancyGuard.exit();
}
// ============ Pool Creation ============
/**
* @notice Create a new PMM liquidity pool (backward compatible with interface)
*/
function createPool(
address baseToken,
address quoteToken,
uint256 initialBaseReserve,
uint256 initialQuoteReserve,
uint256 virtualBaseReserve,
uint256 virtualQuoteReserve,
uint256 k,
uint256 oraclePrice
) external override returns (uint256 poolId) {
return _createPool(
baseToken,
quoteToken,
initialBaseReserve,
initialQuoteReserve,
virtualBaseReserve,
virtualQuoteReserve,
k,
oraclePrice,
address(0)
);
}
/**
* @notice Create a new PMM liquidity pool with oracle
*/
function createPoolWithOracle(
address baseToken,
address quoteToken,
uint256 initialBaseReserve,
uint256 initialQuoteReserve,
uint256 virtualBaseReserve,
uint256 virtualQuoteReserve,
uint256 k,
uint256 oraclePrice,
address oracle
) external onlyPoolCreator returns (uint256 poolId) {
return _createPool(
baseToken,
quoteToken,
initialBaseReserve,
initialQuoteReserve,
virtualBaseReserve,
virtualQuoteReserve,
k,
oraclePrice,
oracle
);
}
function _createPool(
address baseToken,
address quoteToken,
uint256 initialBaseReserve,
uint256 initialQuoteReserve,
uint256 virtualBaseReserve,
uint256 virtualQuoteReserve,
uint256 k,
uint256 oraclePrice,
address oracle
) internal returns (uint256 poolId) {
// Check if system is paused
ISecurityFacet securityFacet = ISecurityFacet(address(this));
require(!securityFacet.isPaused(), "LiquidityFacet: System is paused");
require(baseToken != address(0) && quoteToken != address(0), "LiquidityFacet: Invalid tokens");
require(baseToken != quoteToken, "LiquidityFacet: Tokens must be different");
require(k <= 1e18, "LiquidityFacet: k must be <= 1");
require(virtualBaseReserve > 0 && virtualQuoteReserve > 0, "LiquidityFacet: Virtual reserves must be > 0");
require(oraclePrice > 0, "LiquidityFacet: Oracle price must be > 0");
LiquidityStorage storage ls = liquidityStorage();
poolId = ls.poolCount;
ls.poolCount++;
Pool storage pool = ls.pools[poolId];
pool.baseToken = baseToken;
pool.quoteToken = quoteToken;
pool.baseReserve = initialBaseReserve;
pool.quoteReserve = initialQuoteReserve;
pool.virtualBaseReserve = virtualBaseReserve;
pool.virtualQuoteReserve = virtualQuoteReserve;
pool.k = k;
pool.oraclePrice = oraclePrice;
pool.active = true;
// Set pool configuration
PoolConfig storage config = ls.poolConfigs[poolId];
config.tradingFee = ls.defaultTradingFee > 0 ? ls.defaultTradingFee : 30; // 0.3% default
config.protocolFee = ls.defaultProtocolFee > 0 ? ls.defaultProtocolFee : 10; // 0.1% default
config.paused = false;
config.oracle = oracle;
config.lastOracleUpdate = block.timestamp;
config.oracleUpdateInterval = 3600; // 1 hour default
// Transfer initial tokens
if (initialBaseReserve > 0) {
IERC20(baseToken).safeTransferFrom(msg.sender, address(this), initialBaseReserve);
}
if (initialQuoteReserve > 0) {
IERC20(quoteToken).safeTransferFrom(msg.sender, address(this), initialQuoteReserve);
}
emit PoolCreated(poolId, baseToken, quoteToken);
}
// ============ Liquidity Management ============
/**
* @notice Add liquidity to a pool
*/
function addLiquidity(
uint256 poolId,
uint256 baseAmount,
uint256 quoteAmount
) external override whenPoolNotPaused(poolId) nonReentrant returns (uint256 lpShares) {
// Check compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(poolId);
require(complianceFacet.canAccess(msg.sender, mode), "LiquidityFacet: Compliance check failed");
LiquidityStorage storage ls = liquidityStorage();
Pool storage pool = ls.pools[poolId];
require(pool.active, "LiquidityFacet: Pool not active");
// Transfer tokens
if (baseAmount > 0) {
IERC20(pool.baseToken).safeTransferFrom(msg.sender, address(this), baseAmount);
}
if (quoteAmount > 0) {
IERC20(pool.quoteToken).safeTransferFrom(msg.sender, address(this), quoteAmount);
}
// Calculate LP shares
lpShares = PMMMath.calculateLPShares(
baseAmount,
quoteAmount,
pool.baseReserve,
pool.quoteReserve,
ls.totalLPSupply[poolId]
);
// Update reserves
pool.baseReserve += baseAmount;
pool.quoteReserve += quoteAmount;
ls.lpBalances[poolId][msg.sender] += lpShares;
ls.totalLPSupply[poolId] += lpShares;
emit LiquidityAdded(poolId, msg.sender, baseAmount, quoteAmount);
}
// ============ Swapping ============
/**
* @notice Execute a swap in the pool
*/
function swap(
uint256 poolId,
address tokenIn,
uint256 amountIn,
uint256 minAmountOut
) external override whenPoolNotPaused(poolId) nonReentrant returns (uint256 amountOut) {
// Check security (circuit breaker)
ISecurityFacet securityFacet = ISecurityFacet(address(this));
require(securityFacet.checkCircuitBreaker(poolId, amountIn), "LiquidityFacet: Circuit breaker triggered");
// Check compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
require(
complianceFacet.validateTransaction(msg.sender, address(this), amountIn),
"LiquidityFacet: Compliance validation failed"
);
LiquidityStorage storage ls = liquidityStorage();
Pool storage pool = ls.pools[poolId];
require(pool.active, "LiquidityFacet: Pool not active");
require(tokenIn == pool.baseToken || tokenIn == pool.quoteToken, "LiquidityFacet: Invalid token");
// Update oracle price if available and needed
_updateOraclePrice(poolId);
// Transfer input token
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
bool isBaseIn = (tokenIn == pool.baseToken);
address tokenOut = isBaseIn ? pool.quoteToken : pool.baseToken;
// Calculate output using PMM formula
amountOut = PMMMath.calculateSwapOutput(
amountIn,
isBaseIn ? pool.baseReserve : pool.quoteReserve,
isBaseIn ? pool.quoteReserve : pool.baseReserve,
isBaseIn ? pool.virtualBaseReserve : pool.virtualQuoteReserve,
isBaseIn ? pool.virtualQuoteReserve : pool.virtualBaseReserve,
pool.k,
pool.oraclePrice
);
require(amountOut >= minAmountOut, "LiquidityFacet: Slippage too high");
// Calculate and collect fees
PoolConfig storage config = ls.poolConfigs[poolId];
uint256 tradingFeeAmount = (amountOut * config.tradingFee) / 10000;
uint256 protocolFeeAmount = (tradingFeeAmount * config.protocolFee) / 10000;
uint256 poolFeeAmount = tradingFeeAmount - protocolFeeAmount;
amountOut -= tradingFeeAmount;
// Update reserves (after fees)
if (isBaseIn) {
pool.baseReserve += amountIn;
pool.quoteReserve -= (amountOut + tradingFeeAmount);
} else {
pool.quoteReserve += amountIn;
pool.baseReserve -= (amountOut + tradingFeeAmount);
}
// Collect fees
if (poolFeeAmount > 0) {
ls.poolFees[poolId][tokenOut] += poolFeeAmount;
}
if (protocolFeeAmount > 0) {
ls.protocolFees[tokenOut] += protocolFeeAmount;
}
// Transfer output token
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
emit Swap(poolId, msg.sender, tokenIn, tokenOut, amountIn, amountOut);
emit TradingFeeCollected(poolId, tokenOut, tradingFeeAmount);
}
// ============ View Functions ============
function getPool(uint256 poolId) external view override returns (Pool memory) {
return liquidityStorage().pools[poolId];
}
function getPrice(uint256 poolId) external view override returns (uint256) {
Pool memory pool = liquidityStorage().pools[poolId];
return PMMMath.calculatePrice(
pool.oraclePrice,
pool.k,
pool.quoteReserve,
pool.virtualQuoteReserve
);
}
function getQuote(
uint256 poolId,
address tokenIn,
uint256 amountIn
) external view override returns (uint256 amountOut) {
Pool memory pool = liquidityStorage().pools[poolId];
require(tokenIn == pool.baseToken || tokenIn == pool.quoteToken, "LiquidityFacet: Invalid token");
bool isBaseIn = (tokenIn == pool.baseToken);
amountOut = PMMMath.calculateSwapOutput(
amountIn,
isBaseIn ? pool.baseReserve : pool.quoteReserve,
isBaseIn ? pool.quoteReserve : pool.baseReserve,
isBaseIn ? pool.virtualBaseReserve : pool.virtualQuoteReserve,
isBaseIn ? pool.virtualQuoteReserve : pool.virtualBaseReserve,
pool.k,
pool.oraclePrice
);
}
// ============ Admin Functions ============
/**
* @notice Update oracle price for a pool
*/
function updateOraclePrice(uint256 poolId) external {
_updateOraclePrice(poolId);
}
function _updateOraclePrice(uint256 poolId) internal {
LiquidityStorage storage ls = liquidityStorage();
PoolConfig storage config = ls.poolConfigs[poolId];
if (config.oracle == address(0)) return;
if (block.timestamp < config.lastOracleUpdate + config.oracleUpdateInterval) return;
try IOracle(config.oracle).latestRoundData() returns (
uint80,
int256 price,
uint256,
uint256 updatedAt,
uint80
) {
require(price > 0, "LiquidityFacet: Invalid oracle price");
require(updatedAt > 0, "LiquidityFacet: Stale oracle data");
Pool storage pool = ls.pools[poolId];
pool.oraclePrice = uint256(price);
config.lastOracleUpdate = block.timestamp;
emit OraclePriceUpdated(poolId, uint256(price));
} catch {
// Oracle call failed, skip update
}
}
/**
* @notice Pause or unpause a pool
*/
function setPoolPaused(uint256 poolId, bool paused) external onlyAdmin {
LiquidityStorage storage ls = liquidityStorage();
ls.poolConfigs[poolId].paused = paused;
emit PoolPaused(poolId, paused);
}
/**
* @notice Set pool fees
*/
function setPoolFees(uint256 poolId, uint256 tradingFee, uint256 protocolFee) external onlyAdmin {
require(tradingFee <= 1000, "LiquidityFacet: Trading fee too high"); // Max 10%
require(protocolFee <= 5000, "LiquidityFacet: Protocol fee too high"); // Max 50% of trading fee
LiquidityStorage storage ls = liquidityStorage();
ls.poolConfigs[poolId].tradingFee = tradingFee;
ls.poolConfigs[poolId].protocolFee = protocolFee;
emit PoolFeeUpdated(poolId, tradingFee, protocolFee);
}
/**
* @notice Set Chainlink oracle for a pool
*/
function setPoolOracle(uint256 poolId, address oracle) external onlyAdmin {
LiquidityStorage storage ls = liquidityStorage();
ls.poolConfigs[poolId].oracle = oracle;
}
/**
* @notice Collect protocol fees
*/
function collectProtocolFees(address token) external {
LiquidityStorage storage ls = liquidityStorage();
address collector = ls.feeCollector != address(0) ? ls.feeCollector : msg.sender;
require(collector == msg.sender || LibAccessControl.hasRole(LibAccessControl.FEE_COLLECTOR_ROLE, msg.sender),
"LiquidityFacet: Not authorized");
uint256 amount = ls.protocolFees[token];
require(amount > 0, "LiquidityFacet: No fees to collect");
ls.protocolFees[token] = 0;
IERC20(token).safeTransfer(collector, amount);
emit ProtocolFeeCollected(token, amount);
}
}

View File

@@ -1,103 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IProposalTemplateFacet} from "../../interfaces/IProposalTemplateFacet.sol";
import {IGovernanceFacet} from "../../interfaces/IGovernanceFacet.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
contract ProposalTemplateFacet is IProposalTemplateFacet {
struct TemplateStorage {
mapping(uint256 => ProposalTemplate) templates;
uint256 templateCount;
}
bytes32 private constant TEMPLATE_STORAGE_POSITION = keccak256("asle.proposaltemplate.storage");
function templateStorage() internal pure returns (TemplateStorage storage ts) {
bytes32 position = TEMPLATE_STORAGE_POSITION;
assembly {
ts.slot := position
}
}
modifier onlyAdmin() {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
_;
}
function createTemplate(
string calldata name,
string calldata description,
IGovernanceFacet.ProposalType proposalType,
bytes calldata templateData
) external override onlyAdmin returns (uint256 templateId) {
TemplateStorage storage ts = templateStorage();
templateId = ts.templateCount;
ts.templateCount++;
ts.templates[templateId] = ProposalTemplate({
id: templateId,
name: name,
description: description,
proposalType: proposalType,
templateData: templateData,
active: true
});
emit TemplateCreated(templateId, name, proposalType);
}
function getTemplate(uint256 templateId) external view override returns (
uint256 id,
string memory name,
string memory description,
IGovernanceFacet.ProposalType proposalType,
bytes memory templateData,
bool active
) {
TemplateStorage storage ts = templateStorage();
ProposalTemplate storage template = ts.templates[templateId];
require(template.id != 0 || templateId == 0, "ProposalTemplateFacet: Template not found");
return (
template.id,
template.name,
template.description,
template.proposalType,
template.templateData,
template.active
);
}
function setTemplateActive(uint256 templateId, bool active) external override onlyAdmin {
TemplateStorage storage ts = templateStorage();
require(ts.templates[templateId].id != 0 || templateId == 0, "ProposalTemplateFacet: Template not found");
ts.templates[templateId].active = active;
emit TemplateUpdated(templateId, active);
}
function createProposalFromTemplate(
uint256 templateId,
bytes calldata parameters,
uint256 votingPeriod
) external override returns (uint256 proposalId) {
TemplateStorage storage ts = templateStorage();
ProposalTemplate storage template = ts.templates[templateId];
require(template.id != 0 || templateId == 0, "ProposalTemplateFacet: Template not found");
require(template.active, "ProposalTemplateFacet: Template not active");
// Merge template data with parameters
bytes memory proposalData = abi.encodePacked(template.templateData, parameters);
// Call GovernanceFacet to create proposal
IGovernanceFacet governanceFacet = IGovernanceFacet(address(this));
proposalId = governanceFacet.createProposal(
template.proposalType,
template.description,
proposalData,
votingPeriod
);
}
}

View File

@@ -1,189 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IRWAFacet} from "../../interfaces/IRWAFacet.sol";
import {IERC1404} from "../../interfaces/IERC1404.sol";
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
import {IOracle} from "../../interfaces/IOracle.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract RWAFacet is IRWAFacet, IERC1404 {
using SafeERC20 for IERC20;
struct RWAStorage {
mapping(uint256 => RWA) rwas;
mapping(uint256 => address) valueOracles; // tokenId => oracle address
mapping(uint256 => bool) transferRestricted; // tokenId => restricted
mapping(uint256 => uint256) lastValueUpdate; // tokenId => timestamp
uint256 rwaCount;
}
// ERC-1404 restriction codes
uint8 private constant SUCCESS = 0;
uint8 private constant COMPLIANCE_FAILURE = 1;
uint8 private constant HOLDER_NOT_VERIFIED = 2;
uint8 private constant TRANSFER_RESTRICTED = 3;
bytes32 private constant RWA_STORAGE_POSITION = keccak256("asle.rwa.storage");
function rwaStorage() internal pure returns (RWAStorage storage rs) {
bytes32 position = RWA_STORAGE_POSITION;
assembly {
rs.slot := position
}
}
function tokenizeRWA(
address assetContract,
string calldata assetType,
uint256 totalValue,
bytes calldata complianceData
) external override returns (uint256 tokenId) {
// Check compliance - RWA tokenization typically requires Regulated mode
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
require(
complianceFacet.canAccess(msg.sender, IComplianceFacet.ComplianceMode.Regulated),
"RWAFacet: Regulated compliance required"
);
RWAStorage storage rs = rwaStorage();
tokenId = rs.rwaCount;
rs.rwaCount++;
RWA storage rwa = rs.rwas[tokenId];
rwa.tokenId = tokenId;
rwa.assetContract = assetContract;
rwa.assetType = assetType;
rwa.totalValue = totalValue;
rwa.fractionalizedAmount = 0;
rwa.active = true;
rs.lastValueUpdate[tokenId] = block.timestamp;
emit RWATokenized(tokenId, assetContract, assetType, totalValue);
}
function fractionalizeRWA(
uint256 tokenId,
uint256 amount,
address recipient
) external override returns (uint256 shares) {
RWAStorage storage rs = rwaStorage();
RWA storage rwa = rs.rwas[tokenId];
require(rwa.active, "RWAFacet: RWA not active");
require(amount > 0, "RWAFacet: Amount must be > 0");
require(rwa.fractionalizedAmount + amount <= rwa.totalValue, "RWAFacet: Exceeds total value");
// Verify recipient compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
require(
complianceFacet.canAccess(recipient, IComplianceFacet.ComplianceMode.Regulated),
"RWAFacet: Recipient must have regulated compliance"
);
require(
complianceFacet.validateTransaction(msg.sender, recipient, amount),
"RWAFacet: Compliance validation failed"
);
rwa.fractionalizedAmount += amount;
rwa.verifiedHolders[recipient] = true;
shares = amount; // 1:1 for simplicity, could use different ratio
emit RWAFractionalized(tokenId, recipient, amount);
}
function getRWA(uint256 tokenId) external view override returns (
address assetContract,
string memory assetType,
uint256 totalValue,
uint256 fractionalizedAmount,
bool active
) {
RWA storage rwa = rwaStorage().rwas[tokenId];
return (
rwa.assetContract,
rwa.assetType,
rwa.totalValue,
rwa.fractionalizedAmount,
rwa.active
);
}
function verifyHolder(uint256 tokenId, address holder) external view override returns (bool) {
return rwaStorage().rwas[tokenId].verifiedHolders[holder];
}
// ============ ERC-1404 Transfer Restrictions ============
function detectTransferRestriction(address from, address to, uint256 amount) external view override returns (uint8) {
// Find which RWA token this relates to (simplified - in production would need token mapping)
RWAStorage storage rs = rwaStorage();
// Check compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
if (!complianceFacet.validateTransaction(from, to, amount)) {
return COMPLIANCE_FAILURE;
}
// Check holder verification for all RWAs
// In production, would check specific token
for (uint256 i = 0; i < rs.rwaCount; i++) {
if (rs.transferRestricted[i]) {
if (!rs.rwas[i].verifiedHolders[to]) {
return HOLDER_NOT_VERIFIED;
}
}
}
return SUCCESS;
}
function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) {
if (restrictionCode == COMPLIANCE_FAILURE) return "Transfer failed compliance check";
if (restrictionCode == HOLDER_NOT_VERIFIED) return "Recipient not verified holder";
if (restrictionCode == TRANSFER_RESTRICTED) return "Transfer restricted for this token";
return "Transfer allowed";
}
// ============ Asset Value Management ============
function updateAssetValue(uint256 tokenId, address oracle) external {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
RWAStorage storage rs = rwaStorage();
rs.valueOracles[tokenId] = oracle;
rs.lastValueUpdate[tokenId] = block.timestamp;
}
function getAssetValue(uint256 tokenId) external view returns (uint256) {
RWAStorage storage rs = rwaStorage();
address oracle = rs.valueOracles[tokenId];
if (oracle == address(0)) {
return rs.rwas[tokenId].totalValue;
}
try IOracle(oracle).latestRoundData() returns (
uint80,
int256 price,
uint256,
uint256 updatedAt,
uint80
) {
require(price > 0, "RWAFacet: Invalid oracle price");
require(updatedAt > 0, "RWAFacet: Stale oracle data");
return uint256(price);
} catch {
return rs.rwas[tokenId].totalValue; // Fallback to stored value
}
}
function setTransferRestricted(uint256 tokenId, bool restricted) external {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
rwaStorage().transferRestricted[tokenId] = restricted;
}
}

View File

@@ -1,218 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
import {LibDiamond} from "../../libraries/LibDiamond.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
import {ILiquidityFacet} from "../../interfaces/ILiquidityFacet.sol";
contract SecurityFacet is ISecurityFacet {
struct SecurityStorage {
bool paused;
PauseReason pauseReason;
address pausedBy;
uint256 pauseTime;
uint256 maxPauseDuration; // Maximum pause duration in seconds (0 = unlimited)
mapping(uint256 => CircuitBreaker) circuitBreakers;
mapping(string => uint256) lastAuditTime;
mapping(uint256 => uint256) poolPriceHistory; // poolId => last price
mapping(uint256 => uint256) maxPriceDeviation; // poolId => max deviation in basis points
}
bytes32 private constant SECURITY_STORAGE_POSITION = keccak256("asle.security.storage");
function securityStorage() internal pure returns (SecurityStorage storage ss) {
bytes32 position = SECURITY_STORAGE_POSITION;
assembly {
ss.slot := position
}
}
modifier whenNotPaused() {
require(!securityStorage().paused, "SecurityFacet: System is paused");
_;
}
modifier onlyAuthorized() {
require(
LibAccessControl.hasRole(LibAccessControl.SECURITY_ADMIN_ROLE, msg.sender) ||
LibAccessControl.hasRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender),
"SecurityFacet: Not authorized"
);
_;
}
function pauseSystem(PauseReason reason) external override onlyAuthorized {
SecurityStorage storage ss = securityStorage();
require(!ss.paused, "SecurityFacet: Already paused");
ss.paused = true;
ss.pauseReason = reason;
ss.pausedBy = msg.sender;
ss.pauseTime = block.timestamp;
emit SystemPaused(reason, msg.sender);
}
function pauseSystemWithDuration(PauseReason reason, uint256 duration) external onlyAuthorized {
SecurityStorage storage ss = securityStorage();
require(!ss.paused, "SecurityFacet: Already paused");
ss.paused = true;
ss.pauseReason = reason;
ss.pausedBy = msg.sender;
ss.pauseTime = block.timestamp;
ss.maxPauseDuration = duration;
emit SystemPaused(reason, msg.sender);
}
function unpauseSystem() external override onlyAuthorized {
SecurityStorage storage ss = securityStorage();
require(ss.paused, "SecurityFacet: Not paused");
// Check if pause has expired (if max duration is set)
if (ss.maxPauseDuration > 0) {
require(block.timestamp >= ss.pauseTime + ss.maxPauseDuration, "SecurityFacet: Pause duration not expired");
}
ss.paused = false;
address unpauser = msg.sender;
ss.maxPauseDuration = 0;
emit SystemUnpaused(unpauser);
}
function isPaused() external view override returns (bool) {
return securityStorage().paused;
}
function setCircuitBreaker(
uint256 poolId,
uint256 threshold,
uint256 timeWindow
) external override onlyAuthorized {
SecurityStorage storage ss = securityStorage();
ss.circuitBreakers[poolId] = CircuitBreaker({
threshold: threshold,
timeWindow: timeWindow,
currentValue: 0,
windowStart: block.timestamp,
triggered: false
});
}
function checkCircuitBreaker(uint256 poolId, uint256 value) external override returns (bool) {
SecurityStorage storage ss = securityStorage();
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
if (cb.triggered) {
return false; // Circuit breaker already triggered
}
// Reset window if expired
if (block.timestamp > cb.windowStart + cb.timeWindow) {
cb.windowStart = block.timestamp;
cb.currentValue = 0;
}
cb.currentValue += value;
if (cb.currentValue > cb.threshold) {
cb.triggered = true;
emit CircuitBreakerTriggered(poolId, cb.currentValue);
// Automatically pause if circuit breaker triggers
if (!ss.paused) {
ss.paused = true;
ss.pauseReason = PauseReason.CircuitBreaker;
ss.pausedBy = address(this);
ss.pauseTime = block.timestamp;
emit SystemPaused(PauseReason.CircuitBreaker, address(this));
}
return false;
}
return true;
}
function resetCircuitBreaker(uint256 poolId) external onlyAuthorized {
SecurityStorage storage ss = securityStorage();
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
require(cb.triggered, "SecurityFacet: Circuit breaker not triggered");
cb.triggered = false;
cb.currentValue = 0;
cb.windowStart = block.timestamp;
}
function triggerCircuitBreaker(uint256 poolId) external override {
SecurityStorage storage ss = securityStorage();
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
require(!cb.triggered, "SecurityFacet: Already triggered");
cb.triggered = true;
emit CircuitBreakerTriggered(poolId, cb.currentValue);
// Optionally pause the system
// Note: This would need to be called externally or through Diamond
// pauseSystem(PauseReason.CircuitBreaker);
}
function recordSecurityAudit(string calldata auditType, bool passed) external override onlyAuthorized {
SecurityStorage storage ss = securityStorage();
ss.lastAuditTime[auditType] = block.timestamp;
emit SecurityAudit(block.timestamp, auditType, passed);
if (!passed && !ss.paused) {
ss.paused = true;
ss.pauseReason = PauseReason.ComplianceViolation;
ss.pausedBy = msg.sender;
ss.pauseTime = block.timestamp;
emit SystemPaused(PauseReason.ComplianceViolation, msg.sender);
}
}
function checkPriceDeviation(uint256 poolId, uint256 currentPrice) external returns (bool) {
SecurityStorage storage ss = securityStorage();
uint256 lastPrice = ss.poolPriceHistory[poolId];
uint256 maxDeviation = ss.maxPriceDeviation[poolId];
if (lastPrice == 0) {
ss.poolPriceHistory[poolId] = currentPrice;
return true;
}
if (maxDeviation == 0) {
maxDeviation = 1000; // Default 10% deviation
}
uint256 deviation;
if (currentPrice > lastPrice) {
deviation = ((currentPrice - lastPrice) * 10000) / lastPrice;
} else {
deviation = ((lastPrice - currentPrice) * 10000) / lastPrice;
}
if (deviation > maxDeviation) {
// Trigger circuit breaker or pause
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
if (!cb.triggered) {
cb.triggered = true;
emit CircuitBreakerTriggered(poolId, deviation);
}
return false;
}
ss.poolPriceHistory[poolId] = currentPrice;
return true;
}
function setMaxPriceDeviation(uint256 poolId, uint256 maxDeviation) external onlyAuthorized {
securityStorage().maxPriceDeviation[poolId] = maxDeviation;
}
}

View File

@@ -1,585 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IVaultFacet} from "../../interfaces/IVaultFacet.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
import {LibReentrancyGuard} from "../../libraries/LibReentrancyGuard.sol";
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
/**
* @title VaultFacet
* @notice Complete ERC-4626 and ERC-1155 vault implementation with fees, access control, and compliance
* @dev Implements tokenized vault standard with multi-asset support
*/
contract VaultFacet is IVaultFacet, IERC1155Receiver {
using SafeERC20 for IERC20;
struct VaultStorage {
mapping(uint256 => Vault) vaults;
mapping(uint256 => VaultConfig) vaultConfigs; // vaultId => config
mapping(uint256 => mapping(address => uint256)) balances; // vaultId => user => shares
mapping(uint256 => mapping(address => mapping(address => uint256))) allowances; // vaultId => owner => spender => amount
mapping(uint256 => mapping(address => mapping(uint256 => uint256))) multiAssetBalances; // vaultId => user => tokenId => balance
mapping(uint256 => address[]) multiAssetTokens; // vaultId => token addresses
mapping(address => uint256) protocolFees; // token => accumulated fees
mapping(uint256 => mapping(address => uint256)) vaultFees; // vaultId => token => accumulated fees
uint256 vaultCount;
uint256 defaultDepositFee; // Default deposit fee in basis points
uint256 defaultWithdrawalFee; // Default withdrawal fee in basis points
uint256 defaultManagementFee; // Default management fee per year in basis points
address feeCollector;
}
struct VaultConfig {
uint256 depositFee; // Deposit fee in basis points (0-10000)
uint256 withdrawalFee; // Withdrawal fee in basis points (0-10000)
uint256 managementFee; // Management fee per year in basis points
uint256 lastFeeCollection; // Timestamp of last fee collection
bool paused; // Vault-specific pause
bool allowListEnabled; // Enable allowlist for deposits
mapping(address => bool) allowedAddresses; // Allowlist addresses
}
bytes32 private constant VAULT_STORAGE_POSITION = keccak256("asle.vault.storage");
uint256 private constant MAX_BPS = 10000;
uint256 private constant SECONDS_PER_YEAR = 365 days;
// Events
event VaultPaused(uint256 indexed vaultId, bool paused);
event FeeCollected(uint256 indexed vaultId, address token, uint256 amount);
event ProtocolFeeCollected(address token, uint256 amount);
event Approval(uint256 indexed vaultId, address indexed owner, address indexed spender, uint256 value);
event MultiAssetDeposit(uint256 indexed vaultId, address indexed user, address token, uint256 tokenId, uint256 amount);
event MultiAssetWithdraw(uint256 indexed vaultId, address indexed user, address token, uint256 tokenId, uint256 amount);
function vaultStorage() internal pure returns (VaultStorage storage vs) {
bytes32 position = VAULT_STORAGE_POSITION;
assembly {
vs.slot := position
}
}
// ============ Modifiers ============
modifier onlyVaultCreator() {
LibAccessControl.requireRole(LibAccessControl.VAULT_CREATOR_ROLE, msg.sender);
_;
}
modifier onlyAdmin() {
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
_;
}
modifier whenVaultNotPaused(uint256 vaultId) {
VaultStorage storage vs = vaultStorage();
require(!vs.vaultConfigs[vaultId].paused, "VaultFacet: Vault is paused");
_;
}
modifier nonReentrant() {
LibReentrancyGuard.enter();
_;
LibReentrancyGuard.exit();
}
// ============ Vault Creation ============
/**
* @notice Create a new vault (ERC-4626 or ERC-1155)
*/
function createVault(
address asset,
bool isMultiAsset
) external override returns (uint256 vaultId) {
if (!isMultiAsset) {
require(asset != address(0), "VaultFacet: Asset required for ERC-4626");
}
VaultStorage storage vs = vaultStorage();
vaultId = vs.vaultCount;
vs.vaultCount++;
Vault storage vault = vs.vaults[vaultId];
vault.asset = asset;
vault.isMultiAsset = isMultiAsset;
vault.totalAssets = 0;
vault.totalSupply = 0;
vault.active = true;
// Set default configuration
VaultConfig storage config = vs.vaultConfigs[vaultId];
config.depositFee = vs.defaultDepositFee > 0 ? vs.defaultDepositFee : 0;
config.withdrawalFee = vs.defaultWithdrawalFee > 0 ? vs.defaultWithdrawalFee : 0;
config.managementFee = vs.defaultManagementFee > 0 ? vs.defaultManagementFee : 0;
config.lastFeeCollection = block.timestamp;
config.paused = false;
config.allowListEnabled = false;
emit VaultCreated(vaultId, asset, isMultiAsset);
}
// ============ ERC-4626 Functions ============
/**
* @notice Returns the asset token address
*/
function asset(uint256 vaultId) external view returns (address) {
return vaultStorage().vaults[vaultId].asset;
}
/**
* @notice Returns total assets managed by vault
*/
function totalAssets(uint256 vaultId) external view returns (uint256) {
Vault storage vault = vaultStorage().vaults[vaultId];
return vault.totalAssets;
}
/**
* @notice Convert assets to shares
*/
function convertToShares(
uint256 vaultId,
uint256 assets
) public view override returns (uint256 shares) {
Vault storage vault = vaultStorage().vaults[vaultId];
if (vault.totalSupply == 0) {
shares = assets; // 1:1 for first deposit
} else {
shares = (assets * vault.totalSupply) / vault.totalAssets;
}
}
/**
* @notice Convert shares to assets
*/
function convertToAssets(
uint256 vaultId,
uint256 shares
) public view override returns (uint256 assets) {
Vault storage vault = vaultStorage().vaults[vaultId];
if (vault.totalSupply == 0) {
assets = 0;
} else {
assets = (shares * vault.totalAssets) / vault.totalSupply;
}
}
/**
* @notice Maximum assets that can be deposited
*/
function maxDeposit(uint256 vaultId, address) external pure returns (uint256) {
return type(uint256).max; // No deposit limit
}
/**
* @notice Preview shares for deposit
*/
function previewDeposit(uint256 vaultId, uint256 assets) external view returns (uint256) {
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
uint256 assetsAfterFee = assets - (assets * config.depositFee / MAX_BPS);
return convertToShares(vaultId, assetsAfterFee);
}
/**
* @notice Deposit assets and receive shares
*/
function deposit(
uint256 vaultId,
uint256 assets,
address receiver
) external override whenVaultNotPaused(vaultId) nonReentrant returns (uint256 shares) {
// Check compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(vaultId);
require(complianceFacet.canAccess(msg.sender, mode), "VaultFacet: Compliance check failed");
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
require(vault.active, "VaultFacet: Vault not active");
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset deposit for ERC-1155 vaults");
require(assets > 0, "VaultFacet: Assets must be > 0");
// Check allowlist if enabled
VaultConfig storage config = vs.vaultConfigs[vaultId];
if (config.allowListEnabled) {
require(config.allowedAddresses[msg.sender], "VaultFacet: Address not allowed");
}
IERC20 assetToken = IERC20(vault.asset);
assetToken.safeTransferFrom(msg.sender, address(this), assets);
// Calculate and collect deposit fee
uint256 depositFeeAmount = (assets * config.depositFee) / MAX_BPS;
uint256 assetsAfterFee = assets - depositFeeAmount;
if (depositFeeAmount > 0) {
vs.vaultFees[vaultId][vault.asset] += depositFeeAmount;
}
shares = convertToShares(vaultId, assetsAfterFee);
vault.totalAssets += assetsAfterFee;
vault.totalSupply += shares;
vs.balances[vaultId][receiver] += shares;
emit Deposit(vaultId, receiver, assets, shares);
}
/**
* @notice Maximum shares that can be minted
*/
function maxMint(uint256 vaultId, address) external pure returns (uint256) {
return type(uint256).max; // No mint limit
}
/**
* @notice Preview assets needed to mint shares
*/
function previewMint(uint256 vaultId, uint256 shares) external view returns (uint256) {
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
uint256 assetsNeeded = convertToAssets(vaultId, shares);
// Add deposit fee
return assetsNeeded + (assetsNeeded * config.depositFee / (MAX_BPS - config.depositFee));
}
/**
* @notice Mint shares for assets
*/
function mint(uint256 vaultId, uint256 shares, address receiver) external whenVaultNotPaused(vaultId) nonReentrant returns (uint256 assets) {
// Check compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(vaultId);
require(complianceFacet.canAccess(msg.sender, mode), "VaultFacet: Compliance check failed");
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
require(vault.active, "VaultFacet: Vault not active");
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset mint for ERC-1155 vaults");
assets = previewMint(vaultId, shares);
IERC20 assetToken = IERC20(vault.asset);
assetToken.safeTransferFrom(msg.sender, address(this), assets);
// Calculate and collect deposit fee
VaultConfig storage config = vs.vaultConfigs[vaultId];
uint256 depositFeeAmount = (assets * config.depositFee) / MAX_BPS;
uint256 assetsAfterFee = assets - depositFeeAmount;
if (depositFeeAmount > 0) {
vs.vaultFees[vaultId][vault.asset] += depositFeeAmount;
}
vault.totalAssets += assetsAfterFee;
vault.totalSupply += shares;
vs.balances[vaultId][receiver] += shares;
emit Deposit(vaultId, receiver, assets, shares);
}
/**
* @notice Maximum assets that can be withdrawn
*/
function maxWithdraw(uint256 vaultId, address owner) external view returns (uint256) {
VaultStorage storage vs = vaultStorage();
return convertToAssets(vaultId, vs.balances[vaultId][owner]);
}
/**
* @notice Preview shares needed to withdraw assets
*/
function previewWithdraw(uint256 vaultId, uint256 assets) external view returns (uint256) {
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
uint256 assetsAfterFee = assets - (assets * config.withdrawalFee / MAX_BPS);
return convertToShares(vaultId, assetsAfterFee);
}
/**
* @notice Withdraw assets by burning shares
*/
function withdraw(
uint256 vaultId,
uint256 shares,
address receiver,
address owner
) external override whenVaultNotPaused(vaultId) nonReentrant returns (uint256 assets) {
// Check authorization
if (msg.sender != owner) {
VaultStorage storage vs = vaultStorage();
uint256 allowed = vs.allowances[vaultId][owner][msg.sender];
require(allowed >= shares, "VaultFacet: Insufficient allowance");
vs.allowances[vaultId][owner][msg.sender] -= shares;
}
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
require(vault.active, "VaultFacet: Vault not active");
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset withdraw for ERC-1155 vaults");
require(shares > 0, "VaultFacet: Shares must be > 0");
require(vs.balances[vaultId][owner] >= shares, "VaultFacet: Insufficient shares");
assets = convertToAssets(vaultId, shares);
require(assets <= vault.totalAssets, "VaultFacet: Insufficient assets");
// Calculate and collect withdrawal fee
VaultConfig storage config = vs.vaultConfigs[vaultId];
uint256 withdrawalFeeAmount = (assets * config.withdrawalFee) / MAX_BPS;
uint256 assetsAfterFee = assets - withdrawalFeeAmount;
if (withdrawalFeeAmount > 0) {
vs.vaultFees[vaultId][vault.asset] += withdrawalFeeAmount;
}
// Update state
vault.totalAssets -= assets;
vault.totalSupply -= shares;
vs.balances[vaultId][owner] -= shares;
IERC20(vault.asset).safeTransfer(receiver, assetsAfterFee);
emit Withdraw(vaultId, receiver, assets, shares);
}
/**
* @notice Maximum shares that can be redeemed
*/
function maxRedeem(uint256 vaultId, address owner) external view returns (uint256) {
return vaultStorage().balances[vaultId][owner];
}
/**
* @notice Preview assets for redeeming shares
*/
function previewRedeem(uint256 vaultId, uint256 shares) external view returns (uint256) {
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
uint256 assets = convertToAssets(vaultId, shares);
uint256 withdrawalFeeAmount = (assets * config.withdrawalFee) / MAX_BPS;
return assets - withdrawalFeeAmount;
}
/**
* @notice Redeem shares for assets
*/
function redeem(uint256 vaultId, uint256 shares, address receiver, address owner) external whenVaultNotPaused(vaultId) nonReentrant returns (uint256 assets) {
// Check authorization
if (msg.sender != owner) {
VaultStorage storage vs = vaultStorage();
uint256 allowed = vs.allowances[vaultId][owner][msg.sender];
require(allowed >= shares, "VaultFacet: Insufficient allowance");
vs.allowances[vaultId][owner][msg.sender] -= shares;
}
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
require(vault.active, "VaultFacet: Vault not active");
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset redeem for ERC-1155 vaults");
require(vs.balances[vaultId][owner] >= shares, "VaultFacet: Insufficient shares");
assets = convertToAssets(vaultId, shares);
// Calculate and collect withdrawal fee
VaultConfig storage config = vs.vaultConfigs[vaultId];
uint256 withdrawalFeeAmount = (assets * config.withdrawalFee) / MAX_BPS;
uint256 assetsAfterFee = assets - withdrawalFeeAmount;
if (withdrawalFeeAmount > 0) {
vs.vaultFees[vaultId][vault.asset] += withdrawalFeeAmount;
}
// Update state
vault.totalAssets -= assets;
vault.totalSupply -= shares;
vs.balances[vaultId][owner] -= shares;
IERC20(vault.asset).safeTransfer(receiver, assetsAfterFee);
emit Withdraw(vaultId, receiver, assets, shares);
}
// ============ Approval Mechanism ============
/**
* @notice Approve spender to withdraw shares
*/
function approve(uint256 vaultId, address spender, uint256 amount) external returns (bool) {
VaultStorage storage vs = vaultStorage();
vs.allowances[vaultId][msg.sender][spender] = amount;
emit Approval(vaultId, msg.sender, spender, amount);
return true;
}
/**
* @notice Get approval amount
*/
function allowance(uint256 vaultId, address owner, address spender) external view returns (uint256) {
return vaultStorage().allowances[vaultId][owner][spender];
}
/**
* @notice Get balance of shares
*/
function balanceOf(uint256 vaultId, address account) external view returns (uint256) {
return vaultStorage().balances[vaultId][account];
}
// ============ ERC-1155 Multi-Asset Functions ============
/**
* @notice Deposit multiple assets into ERC-1155 vault
*/
function depositMultiAsset(
uint256 vaultId,
address token,
uint256 tokenId,
uint256 amount
) external whenVaultNotPaused(vaultId) nonReentrant {
// Check compliance
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(vaultId);
require(complianceFacet.canAccess(msg.sender, mode), "VaultFacet: Compliance check failed");
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
require(vault.active, "VaultFacet: Vault not active");
require(vault.isMultiAsset, "VaultFacet: Not a multi-asset vault");
IERC1155(token).safeTransferFrom(msg.sender, address(this), tokenId, amount, "");
vs.multiAssetBalances[vaultId][msg.sender][tokenId] += amount;
// Track token addresses
bool tokenExists = false;
for (uint i = 0; i < vs.multiAssetTokens[vaultId].length; i++) {
if (vs.multiAssetTokens[vaultId][i] == token) {
tokenExists = true;
break;
}
}
if (!tokenExists) {
vs.multiAssetTokens[vaultId].push(token);
}
emit MultiAssetDeposit(vaultId, msg.sender, token, tokenId, amount);
}
/**
* @notice Withdraw multiple assets from ERC-1155 vault
*/
function withdrawMultiAsset(
uint256 vaultId,
address token,
uint256 tokenId,
uint256 amount
) external whenVaultNotPaused(vaultId) nonReentrant {
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
require(vault.active, "VaultFacet: Vault not active");
require(vault.isMultiAsset, "VaultFacet: Not a multi-asset vault");
require(vs.multiAssetBalances[vaultId][msg.sender][tokenId] >= amount, "VaultFacet: Insufficient balance");
vs.multiAssetBalances[vaultId][msg.sender][tokenId] -= amount;
IERC1155(token).safeTransferFrom(address(this), msg.sender, tokenId, amount, "");
emit MultiAssetWithdraw(vaultId, msg.sender, token, tokenId, amount);
}
/**
* @notice Get multi-asset balance
*/
function getMultiAssetBalance(uint256 vaultId, address user, address token, uint256 tokenId) external view returns (uint256) {
return vaultStorage().multiAssetBalances[vaultId][user][tokenId];
}
// ============ ERC-1155 Receiver ============
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata
) external pure returns (bytes4) {
return IERC1155Receiver.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure returns (bytes4) {
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId;
}
// ============ View Functions ============
function getVault(uint256 vaultId) external view override returns (Vault memory) {
return vaultStorage().vaults[vaultId];
}
// ============ Admin Functions ============
/**
* @notice Pause or unpause a vault
*/
function setVaultPaused(uint256 vaultId, bool paused) external onlyAdmin {
vaultStorage().vaultConfigs[vaultId].paused = paused;
emit VaultPaused(vaultId, paused);
}
/**
* @notice Set vault fees
*/
function setVaultFees(uint256 vaultId, uint256 depositFee, uint256 withdrawalFee, uint256 managementFee) external onlyAdmin {
require(depositFee <= 1000, "VaultFacet: Deposit fee too high");
require(withdrawalFee <= 1000, "VaultFacet: Withdrawal fee too high");
require(managementFee <= 2000, "VaultFacet: Management fee too high");
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
config.depositFee = depositFee;
config.withdrawalFee = withdrawalFee;
config.managementFee = managementFee;
}
/**
* @notice Collect management fees
*/
function collectManagementFees(uint256 vaultId) external {
VaultStorage storage vs = vaultStorage();
Vault storage vault = vs.vaults[vaultId];
VaultConfig storage config = vs.vaultConfigs[vaultId];
uint256 timeElapsed = block.timestamp - config.lastFeeCollection;
uint256 feeAmount = (vault.totalAssets * config.managementFee * timeElapsed) / (MAX_BPS * SECONDS_PER_YEAR);
if (feeAmount > 0 && feeAmount < vault.totalAssets) {
vs.vaultFees[vaultId][vault.asset] += feeAmount;
vault.totalAssets -= feeAmount;
}
config.lastFeeCollection = block.timestamp;
}
/**
* @notice Collect vault fees
*/
function collectVaultFees(uint256 vaultId, address token) external onlyAdmin {
VaultStorage storage vs = vaultStorage();
uint256 amount = vs.vaultFees[vaultId][token];
require(amount > 0, "VaultFacet: No fees to collect");
vs.vaultFees[vaultId][token] = 0;
IERC20(token).safeTransfer(vs.feeCollector != address(0) ? vs.feeCollector : msg.sender, amount);
emit FeeCollected(vaultId, token, amount);
}
}

View File

@@ -1,101 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface ICCIPFacet {
enum MessageType {
LiquiditySync,
VaultRebalance,
PriceDeviation,
TokenBridge
}
struct CCIPMessage {
MessageType messageType;
uint256 sourceChainId;
uint256 targetChainId;
bytes payload;
uint256 timestamp;
}
struct LiquiditySyncPayload {
uint256 poolId;
uint256 baseReserve;
uint256 quoteReserve;
uint256 virtualBaseReserve;
uint256 virtualQuoteReserve;
}
struct VaultRebalancePayload {
uint256 vaultId;
uint256 targetChainId;
uint256 amount;
address asset;
}
struct PriceDeviationPayload {
uint256 poolId;
uint256 price;
uint256 deviation;
uint256 timestamp;
}
event CCIPMessageSent(
bytes32 indexed messageId,
uint256 indexed sourceChainId,
uint256 indexed targetChainId,
MessageType messageType
);
event CCIPMessageReceived(
bytes32 indexed messageId,
uint256 indexed sourceChainId,
MessageType messageType
);
event LiquiditySynced(
uint256 indexed poolId,
uint256 indexed chainId,
uint256 baseReserve,
uint256 quoteReserve
);
event VaultRebalanced(
uint256 indexed vaultId,
uint256 indexed sourceChainId,
uint256 indexed targetChainId,
uint256 amount
);
function sendLiquiditySync(
uint256 targetChainId,
uint256 poolId
) external returns (bytes32 messageId);
function sendVaultRebalance(
uint256 targetChainId,
uint256 vaultId,
uint256 amount,
address asset
) external returns (bytes32 messageId);
function sendPriceDeviationWarning(
uint256 targetChainId,
uint256 poolId,
uint256 deviation
) external returns (bytes32 messageId);
function handleCCIPMessage(
bytes32 messageId,
uint256 sourceChainId,
bytes calldata payload
) external;
function setCCIPRouter(address router) external;
function setSupportedChain(uint256 chainId, bool supported) external;
function isChainSupported(uint256 chainId) external view returns (bool);
function getMessageStatus(bytes32 messageId) external view returns (bool delivered, uint256 timestamp);
}

View File

@@ -1,32 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title ICCIPRouter
* @notice Interface for Chainlink CCIP Router (compatible with official CCIP interface)
*/
interface ICCIPRouter {
struct EVM2AnyMessage {
bytes receiver;
bytes data;
EVMTokenAmount[] tokenAmounts;
bytes extraArgs;
address feeToken;
}
struct EVMTokenAmount {
address token;
uint256 amount;
}
function ccipSend(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external payable returns (bytes32 messageId);
function getFee(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external view returns (uint256 fee);
}

View File

@@ -1,38 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IChainConfigFacet {
struct ChainConfig {
uint256 chainId;
string name;
address nativeToken; // Address(0) for native ETH
string explorerUrl;
uint256 gasLimit;
uint256 messageTimeout;
bool active;
}
event ChainConfigUpdated(uint256 indexed chainId, string name, bool active);
event ChainGasLimitUpdated(uint256 indexed chainId, uint256 gasLimit);
event ChainTimeoutUpdated(uint256 indexed chainId, uint256 timeout);
function setChainConfig(
uint256 chainId,
string calldata name,
address nativeToken,
string calldata explorerUrl,
uint256 gasLimit,
uint256 messageTimeout
) external;
function getChainConfig(uint256 chainId) external view returns (ChainConfig memory);
function setChainActive(uint256 chainId, bool active) external;
function setChainGasLimit(uint256 chainId, uint256 gasLimit) external;
function setChainTimeout(uint256 chainId, uint256 timeout) external;
function isChainActive(uint256 chainId) external view returns (bool);
}

View File

@@ -1,74 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IComplianceFacet {
enum ComplianceMode {
Regulated, // Mode A: Full KYC/AML
Fintech, // Mode B: Tiered KYC
Decentralized // Mode C: No KYC
}
struct UserCompliance {
ComplianceMode mode;
bool kycVerified;
bool amlVerified;
uint256 tier;
bool active;
}
event ComplianceModeSet(
address indexed user,
ComplianceMode mode
);
event KYCVerified(
address indexed user,
bool verified
);
event OFACCheck(
address indexed user,
bool sanctioned
);
event TravelRuleCompliance(
address indexed from,
address indexed to,
uint256 amount,
bytes32 transactionHash
);
event ISO20022Message(
address indexed user,
string messageType,
bytes32 messageId
);
function setUserComplianceMode(
address user,
ComplianceMode mode
) external;
function verifyKYC(address user, bool verified) external;
function verifyAML(address user, bool verified) external;
function getUserCompliance(
address user
) external view returns (UserCompliance memory);
function canAccess(
address user,
ComplianceMode requiredMode
) external view returns (bool);
function setVaultComplianceMode(
uint256 vaultId,
ComplianceMode mode
) external;
function getVaultComplianceMode(
uint256 vaultId
) external view returns (ComplianceMode);
}

View File

@@ -1,38 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IDiamond {
/// @notice Gets all facets and their selectors.
/// @return facets_ Facet
function facets() external view returns (Facet[] memory facets_);
/// @notice Gets all the function selectors provided by a facet.
/// @param _facet The facet address.
/// @return facetFunctionSelectors_
function facetFunctionSelectors(address _facet)
external
view
returns (bytes4[] memory facetFunctionSelectors_);
/// @notice Get all the facet addresses used by a diamond.
/// @return facetAddresses_
function facetAddresses()
external
view
returns (address[] memory facetAddresses_);
/// @notice Gets the facet that supports the given selector.
/// @dev If facet is not found return address(0).
/// @param _functionSelector The function selector.
/// @return facetAddress_ The facet address.
function facetAddress(bytes4 _functionSelector)
external
view
returns (address facetAddress_);
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}
}

View File

@@ -1,23 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IDiamondCut {
enum FacetCutAction {
Add,
Replace,
Remove
}
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}

View File

@@ -1,9 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// ERC-1404: Simple Restricted Token Standard
interface IERC1404 {
function detectTransferRestriction(address from, address to, uint256 amount) external view returns (uint8);
function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory);
}

View File

@@ -1,133 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IGovernanceFacet {
enum ProposalType {
ParameterChange,
FacetUpgrade,
TreasuryWithdrawal,
ComplianceChange,
EmergencyPause
}
enum ProposalStatus {
Pending,
Active,
Passed,
Rejected,
Executed
}
struct Proposal {
uint256 id;
ProposalType proposalType;
ProposalStatus status;
address proposer;
string description;
bytes data;
uint256 startTime;
uint256 endTime;
uint256 forVotes;
uint256 againstVotes;
mapping(address => bool) hasVoted;
}
struct TreasuryAction {
address recipient;
uint256 amount;
address token;
string reason;
bool executed;
}
event ProposalCreated(
uint256 indexed proposalId,
ProposalType proposalType,
address indexed proposer
);
event VoteCast(
uint256 indexed proposalId,
address indexed voter,
bool support,
uint256 weight
);
event ProposalExecuted(uint256 indexed proposalId);
event TreasuryWithdrawal(
address indexed recipient,
uint256 amount,
address token
);
event DelegationChanged(
address indexed delegator,
address indexed delegate,
uint256 previousBalance,
uint256 newBalance
);
function createProposal(
ProposalType proposalType,
string calldata description,
bytes calldata data,
uint256 votingPeriod
) external returns (uint256 proposalId);
function vote(uint256 proposalId, bool support) external;
function executeProposal(uint256 proposalId) external;
function getProposal(uint256 proposalId) external view returns (
uint256 id,
ProposalType proposalType,
ProposalStatus status,
address proposer,
uint256 forVotes,
uint256 againstVotes,
uint256 startTime,
uint256 endTime
);
function proposeTreasuryWithdrawal(
address recipient,
uint256 amount,
address token,
string calldata reason
) external returns (uint256 proposalId);
function getTreasuryBalance(address token) external view returns (uint256);
function delegate(address delegatee) external;
function delegateBySig(
address delegator,
address delegatee,
uint256 nonce,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external;
function delegates(address delegator) external view returns (address);
function getCurrentVotes(address account) external view returns (uint256);
function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256);
struct Action {
address target;
uint256 value;
bytes data;
bool executed;
}
function createMultiActionProposal(
string calldata description,
Action[] calldata actions,
uint256 votingPeriod
) external returns (uint256 proposalId);
}

View File

@@ -1,73 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface ILiquidityFacet {
struct Pool {
address baseToken;
address quoteToken;
uint256 baseReserve;
uint256 quoteReserve;
uint256 virtualBaseReserve;
uint256 virtualQuoteReserve;
uint256 k; // Slippage control coefficient
uint256 oraclePrice; // Market oracle price (i)
bool active;
}
event PoolCreated(
uint256 indexed poolId,
address indexed baseToken,
address indexed quoteToken
);
event LiquidityAdded(
uint256 indexed poolId,
address indexed provider,
uint256 baseAmount,
uint256 quoteAmount
);
event Swap(
uint256 indexed poolId,
address indexed trader,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut
);
function createPool(
address baseToken,
address quoteToken,
uint256 initialBaseReserve,
uint256 initialQuoteReserve,
uint256 virtualBaseReserve,
uint256 virtualQuoteReserve,
uint256 k,
uint256 oraclePrice
) external returns (uint256 poolId);
function addLiquidity(
uint256 poolId,
uint256 baseAmount,
uint256 quoteAmount
) external returns (uint256 lpShares);
function swap(
uint256 poolId,
address tokenIn,
uint256 amountIn,
uint256 minAmountOut
) external returns (uint256 amountOut);
function getPool(uint256 poolId) external view returns (Pool memory);
function getPrice(uint256 poolId) external view returns (uint256);
function getQuote(
uint256 poolId,
address tokenIn,
uint256 amountIn
) external view returns (uint256 amountOut);
}

View File

@@ -1,19 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title IOracle
* @notice Interface for price oracles (compatible with Chainlink)
*/
interface IOracle {
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
}

View File

@@ -1,41 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IProposalTemplateFacet {
struct ProposalTemplate {
uint256 id;
string name;
string description;
IGovernanceFacet.ProposalType proposalType;
bytes templateData;
bool active;
}
event TemplateCreated(uint256 indexed templateId, string name, IGovernanceFacet.ProposalType proposalType);
event TemplateUpdated(uint256 indexed templateId, bool active);
function createTemplate(
string calldata name,
string calldata description,
IGovernanceFacet.ProposalType proposalType,
bytes calldata templateData
) external returns (uint256 templateId);
function getTemplate(uint256 templateId) external view returns (
uint256 id,
string memory name,
string memory description,
IGovernanceFacet.ProposalType proposalType,
bytes memory templateData,
bool active
);
function setTemplateActive(uint256 templateId, bool active) external;
function createProposalFromTemplate(
uint256 templateId,
bytes calldata parameters,
uint256 votingPeriod
) external returns (uint256 proposalId);
}

View File

@@ -1,51 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IRWAFacet {
struct RWA {
uint256 tokenId;
address assetContract;
string assetType; // "real_estate", "commodity", "security", etc.
uint256 totalValue;
uint256 fractionalizedAmount;
bool active;
mapping(address => bool) verifiedHolders;
}
event RWATokenized(
uint256 indexed tokenId,
address indexed assetContract,
string assetType,
uint256 totalValue
);
event RWAFractionalized(
uint256 indexed tokenId,
address indexed holder,
uint256 amount
);
function tokenizeRWA(
address assetContract,
string calldata assetType,
uint256 totalValue,
bytes calldata complianceData
) external returns (uint256 tokenId);
function fractionalizeRWA(
uint256 tokenId,
uint256 amount,
address recipient
) external returns (uint256 shares);
function getRWA(uint256 tokenId) external view returns (
address assetContract,
string memory assetType,
uint256 totalValue,
uint256 fractionalizedAmount,
bool active
);
function verifyHolder(uint256 tokenId, address holder) external view returns (bool);
}

View File

@@ -1,44 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface ISecurityFacet {
enum PauseReason {
Emergency,
CircuitBreaker,
OracleDeviation,
ComplianceViolation,
GovernanceDecision
}
struct CircuitBreaker {
uint256 threshold;
uint256 timeWindow;
uint256 currentValue;
uint256 windowStart;
bool triggered;
}
event SystemPaused(PauseReason reason, address indexed pausedBy);
event SystemUnpaused(address indexed unpausedBy);
event CircuitBreakerTriggered(uint256 indexed poolId, uint256 deviation);
event SecurityAudit(uint256 timestamp, string auditType, bool passed);
function pauseSystem(PauseReason reason) external;
function unpauseSystem() external;
function isPaused() external view returns (bool);
function setCircuitBreaker(
uint256 poolId,
uint256 threshold,
uint256 timeWindow
) external;
function checkCircuitBreaker(uint256 poolId, uint256 value) external returns (bool);
function triggerCircuitBreaker(uint256 poolId) external;
function recordSecurityAudit(string calldata auditType, bool passed) external;
}

View File

@@ -1,63 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IVaultFacet {
struct Vault {
address asset; // ERC-20 asset for ERC-4626, or address(0) for ERC-1155
uint256 totalAssets;
uint256 totalSupply;
bool isMultiAsset; // true for ERC-1155, false for ERC-4626
bool active;
}
event VaultCreated(
uint256 indexed vaultId,
address indexed asset,
bool isMultiAsset
);
event Deposit(
uint256 indexed vaultId,
address indexed depositor,
uint256 assets,
uint256 shares
);
event Withdraw(
uint256 indexed vaultId,
address indexed withdrawer,
uint256 assets,
uint256 shares
);
function createVault(
address asset,
bool isMultiAsset
) external returns (uint256 vaultId);
function deposit(
uint256 vaultId,
uint256 assets,
address receiver
) external returns (uint256 shares);
function withdraw(
uint256 vaultId,
uint256 shares,
address receiver,
address owner
) external returns (uint256 assets);
function getVault(uint256 vaultId) external view returns (Vault memory);
function convertToShares(
uint256 vaultId,
uint256 assets
) external view returns (uint256);
function convertToAssets(
uint256 vaultId,
uint256 shares
) external view returns (uint256);
}

View File

@@ -1,218 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title LibAccessControl
* @notice Diamond-compatible access control library using Diamond storage pattern
* @dev Provides role-based access control for Diamond facets
*/
library LibAccessControl {
bytes32 constant ACCESS_CONTROL_STORAGE_POSITION = keccak256("asle.accesscontrol.storage");
bytes32 constant TIMELOCK_STORAGE_POSITION = keccak256("asle.timelock.storage");
// Role definitions
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
bytes32 public constant POOL_CREATOR_ROLE = keccak256("POOL_CREATOR_ROLE");
bytes32 public constant VAULT_CREATOR_ROLE = keccak256("VAULT_CREATOR_ROLE");
bytes32 public constant COMPLIANCE_ADMIN_ROLE = keccak256("COMPLIANCE_ADMIN_ROLE");
bytes32 public constant GOVERNANCE_ADMIN_ROLE = keccak256("GOVERNANCE_ADMIN_ROLE");
bytes32 public constant SECURITY_ADMIN_ROLE = keccak256("SECURITY_ADMIN_ROLE");
bytes32 public constant FEE_COLLECTOR_ROLE = keccak256("FEE_COLLECTOR_ROLE");
struct RoleData {
mapping(address => bool) members;
bytes32 adminRole;
}
struct AccessControlStorage {
mapping(bytes32 => RoleData) roles;
address[] roleMembers; // For enumeration support
}
struct TimelockStorage {
mapping(bytes32 => uint256) scheduledOperations; // operationId => executionTime
uint256 defaultDelay; // Default timelock delay in seconds
bool timelockEnabled;
}
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
event OperationScheduled(bytes32 indexed operationId, uint256 executionTime);
event OperationExecuted(bytes32 indexed operationId);
function accessControlStorage() internal pure returns (AccessControlStorage storage acs) {
bytes32 position = ACCESS_CONTROL_STORAGE_POSITION;
assembly {
acs.slot := position
}
}
function timelockStorage() internal pure returns (TimelockStorage storage ts) {
bytes32 position = TIMELOCK_STORAGE_POSITION;
assembly {
ts.slot := position
}
}
/**
* @notice Check if an account has a specific role
*/
function hasRole(bytes32 role, address account) internal view returns (bool) {
return accessControlStorage().roles[role].members[account];
}
/**
* @notice Check if account has role or is admin of role
*/
function hasRoleOrAdmin(bytes32 role, address account) internal view returns (bool) {
AccessControlStorage storage acs = accessControlStorage();
return acs.roles[role].members[account] || hasRole(getRoleAdmin(role), account);
}
/**
* @notice Get the admin role for a given role
*/
function getRoleAdmin(bytes32 role) internal view returns (bytes32) {
AccessControlStorage storage acs = accessControlStorage();
bytes32 adminRole = acs.roles[role].adminRole;
return adminRole == bytes32(0) ? DEFAULT_ADMIN_ROLE : adminRole;
}
/**
* @notice Grant a role to an account
* @dev Can only be called by accounts with admin role
*/
function grantRole(bytes32 role, address account) internal {
AccessControlStorage storage acs = accessControlStorage();
bytes32 adminRole = getRoleAdmin(role);
require(hasRole(adminRole, msg.sender), "LibAccessControl: account is missing admin role");
if (!acs.roles[role].members[account]) {
acs.roles[role].members[account] = true;
emit RoleGranted(role, account, msg.sender);
}
}
/**
* @notice Revoke a role from an account
* @dev Can only be called by accounts with admin role
*/
function revokeRole(bytes32 role, address account) internal {
AccessControlStorage storage acs = accessControlStorage();
bytes32 adminRole = getRoleAdmin(role);
require(hasRole(adminRole, msg.sender), "LibAccessControl: account is missing admin role");
if (acs.roles[role].members[account]) {
acs.roles[role].members[account] = false;
emit RoleRevoked(role, account, msg.sender);
}
}
/**
* @notice Set the admin role for a role
*/
function setRoleAdmin(bytes32 role, bytes32 adminRole) internal {
AccessControlStorage storage acs = accessControlStorage();
bytes32 previousAdminRole = getRoleAdmin(role);
require(hasRole(previousAdminRole, msg.sender), "LibAccessControl: account is missing admin role");
acs.roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @notice Require that account has role, revert if not
*/
function requireRole(bytes32 role, address account) internal view {
require(hasRole(role, account), "LibAccessControl: account is missing role");
}
/**
* @notice Initialize access control with default admin
*/
function initializeAccessControl(address defaultAdmin) internal {
AccessControlStorage storage acs = accessControlStorage();
require(!acs.roles[DEFAULT_ADMIN_ROLE].members[defaultAdmin], "LibAccessControl: already initialized");
acs.roles[DEFAULT_ADMIN_ROLE].members[defaultAdmin] = true;
// Set role hierarchies
acs.roles[POOL_CREATOR_ROLE].adminRole = DEFAULT_ADMIN_ROLE;
acs.roles[VAULT_CREATOR_ROLE].adminRole = DEFAULT_ADMIN_ROLE;
acs.roles[COMPLIANCE_ADMIN_ROLE].adminRole = DEFAULT_ADMIN_ROLE;
acs.roles[GOVERNANCE_ADMIN_ROLE].adminRole = DEFAULT_ADMIN_ROLE;
acs.roles[SECURITY_ADMIN_ROLE].adminRole = DEFAULT_ADMIN_ROLE;
acs.roles[FEE_COLLECTOR_ROLE].adminRole = DEFAULT_ADMIN_ROLE;
emit RoleGranted(DEFAULT_ADMIN_ROLE, defaultAdmin, address(0));
}
// ============ Timelock Functions ============
/**
* @notice Schedule an operation with timelock
*/
function scheduleOperation(bytes32 operationId, bytes32 operationHash) internal {
TimelockStorage storage ts = timelockStorage();
require(ts.timelockEnabled, "LibAccessControl: timelock not enabled");
require(ts.scheduledOperations[operationId] == 0, "LibAccessControl: operation already scheduled");
uint256 executionTime = block.timestamp + ts.defaultDelay;
ts.scheduledOperations[operationId] = executionTime;
emit OperationScheduled(operationId, executionTime);
}
/**
* @notice Check if operation is ready to execute
*/
function isOperationReady(bytes32 operationId) internal view returns (bool) {
TimelockStorage storage ts = timelockStorage();
if (!ts.timelockEnabled) return true;
uint256 executionTime = ts.scheduledOperations[operationId];
return executionTime > 0 && block.timestamp >= executionTime;
}
/**
* @notice Execute a scheduled operation
*/
function executeOperation(bytes32 operationId) internal {
TimelockStorage storage ts = timelockStorage();
require(isOperationReady(operationId), "LibAccessControl: operation not ready");
delete ts.scheduledOperations[operationId];
emit OperationExecuted(operationId);
}
/**
* @notice Cancel a scheduled operation
*/
function cancelOperation(bytes32 operationId) internal {
TimelockStorage storage ts = timelockStorage();
require(ts.scheduledOperations[operationId] > 0, "LibAccessControl: operation not scheduled");
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "LibAccessControl: must be admin");
delete ts.scheduledOperations[operationId];
}
/**
* @notice Set timelock delay
*/
function setTimelockDelay(uint256 delay) internal {
TimelockStorage storage ts = timelockStorage();
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "LibAccessControl: must be admin");
ts.defaultDelay = delay;
}
/**
* @notice Enable/disable timelock
*/
function setTimelockEnabled(bool enabled) internal {
TimelockStorage storage ts = timelockStorage();
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "LibAccessControl: must be admin");
ts.timelockEnabled = enabled;
}
}

View File

@@ -1,228 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IDiamondCut} from "../interfaces/IDiamondCut.sol";
library LibDiamond {
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
bytes32 constant DIAMOND_OWNER_STORAGE_POSITION = keccak256("diamond.standard.owner.storage");
struct FacetAddressAndPosition {
address facetAddress;
uint16 functionSelectorPosition;
}
struct FacetFunctionSelectors {
bytes4[] functionSelectors;
uint16 facetAddressPosition;
}
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition;
mapping(address => FacetFunctionSelectors) facetFunctionSelectors;
address[] facetAddresses;
mapping(bytes4 => bool) supportedInterfaces;
}
struct DiamondOwnerStorage {
address contractOwner;
bool initialized;
}
struct TimelockStorage {
mapping(bytes32 => uint256) scheduledCuts; // cutHash => executionTime
uint256 defaultDelay; // Default timelock delay in seconds
bool timelockEnabled;
}
bytes32 constant DIAMOND_TIMELOCK_STORAGE_POSITION = keccak256("diamond.standard.timelock.storage");
function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
function diamondOwnerStorage() internal pure returns (DiamondOwnerStorage storage dos) {
bytes32 position = DIAMOND_OWNER_STORAGE_POSITION;
assembly {
dos.slot := position
}
}
function diamondTimelockStorage() internal pure returns (TimelockStorage storage ts) {
bytes32 position = DIAMOND_TIMELOCK_STORAGE_POSITION;
assembly {
ts.slot := position
}
}
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event DiamondCutScheduled(bytes32 indexed cutHash, uint256 executionTime);
event DiamondCutExecuted(bytes32 indexed cutHash);
function setContractOwner(address _newOwner) internal {
DiamondOwnerStorage storage dos = diamondOwnerStorage();
address oldOwner = dos.contractOwner;
dos.contractOwner = _newOwner;
if (!dos.initialized) {
dos.initialized = true;
}
emit OwnershipTransferred(oldOwner, _newOwner);
}
function isInitialized() internal view returns (bool) {
return diamondOwnerStorage().initialized;
}
function contractOwner() internal view returns (address contractOwner_) {
contractOwner_ = diamondOwnerStorage().contractOwner;
}
function enforceIsContractOwner() internal view {
require(msg.sender == contractOwner(), "LibDiamond: Must be contract owner");
}
function diamondCut(
IDiamondCut.FacetCut[] memory _diamondCut,
address _init,
bytes memory _calldata
) internal {
LibDiamondCut.diamondCut(_diamondCut, _init, _calldata);
}
}
library LibDiamondCut {
event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata);
function diamondCut(
IDiamondCut.FacetCut[] memory _diamondCut,
address _init,
bytes memory _calldata
) internal {
for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) {
IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action;
if (action == IDiamondCut.FacetCutAction.Add) {
addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
} else if (action == IDiamondCut.FacetCutAction.Replace) {
replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
} else if (action == IDiamondCut.FacetCutAction.Remove) {
removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
} else {
revert("LibDiamondCut: Incorrect FacetCutAction");
}
}
emit DiamondCut(_diamondCut, _init, _calldata);
initializeDiamondCut(_init, _calldata);
}
function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)");
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint16 selectorPosition = uint16(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
if (selectorPosition == 0) {
enforceHasContractCode(_facetAddress, "LibDiamondCut: New facet has no code");
ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = uint16(ds.facetAddresses.length);
ds.facetAddresses.push(_facetAddress);
}
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
require(oldFacetAddress == address(0), "LibDiamondCut: Can't add function that already exists");
ds.selectorToFacetAndPosition[selector] = LibDiamond.FacetAddressAndPosition(_facetAddress, selectorPosition);
ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(selector);
selectorPosition++;
}
}
function replaceFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
require(_facetAddress != address(0), "LibDiamondCut: Replace facet can't be address(0)");
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
uint16 selectorPosition = uint16(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
if (selectorPosition == 0) {
enforceHasContractCode(_facetAddress, "LibDiamondCut: New facet has no code");
ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = uint16(ds.facetAddresses.length);
ds.facetAddresses.push(_facetAddress);
}
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
require(oldFacetAddress != _facetAddress, "LibDiamondCut: Can't replace function with same function");
removeFunction(oldFacetAddress, selector);
addFunction(_facetAddress, selector, selectorPosition);
selectorPosition++;
}
}
function removeFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
require(_facetAddress == address(0), "LibDiamondCut: Remove facet address must be address(0)");
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
bytes4 selector = _functionSelectors[selectorIndex];
address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
removeFunction(oldFacetAddress, selector);
}
}
function addFunction(address _facetAddress, bytes4 _selector, uint16 _selectorPosition) internal {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _selectorPosition;
ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(_selector);
ds.selectorToFacetAndPosition[_selector].facetAddress = _facetAddress;
}
function removeFunction(address _facetAddress, bytes4 _selector) internal {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
require(_facetAddress != address(0), "LibDiamondCut: Can't remove function that doesn't exist");
require(_facetAddress != address(this), "LibDiamondCut: Can't remove immutable function");
uint256 selectorPosition = ds.selectorToFacetAndPosition[_selector].functionSelectorPosition;
uint256 lastSelectorPosition = ds.facetFunctionSelectors[_facetAddress].functionSelectors.length - 1;
if (selectorPosition != lastSelectorPosition) {
bytes4 lastSelector = ds.facetFunctionSelectors[_facetAddress].functionSelectors[lastSelectorPosition];
ds.facetFunctionSelectors[_facetAddress].functionSelectors[selectorPosition] = lastSelector;
ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint16(selectorPosition);
}
ds.facetFunctionSelectors[_facetAddress].functionSelectors.pop();
delete ds.selectorToFacetAndPosition[_selector];
if (lastSelectorPosition == 0) {
uint256 lastFacetAddressPosition = ds.facetAddresses.length - 1;
uint256 facetAddressPosition = ds.facetFunctionSelectors[_facetAddress].facetAddressPosition;
if (facetAddressPosition != lastFacetAddressPosition) {
address lastFacetAddress = ds.facetAddresses[lastFacetAddressPosition];
ds.facetAddresses[facetAddressPosition] = lastFacetAddress;
ds.facetFunctionSelectors[lastFacetAddress].facetAddressPosition = uint16(facetAddressPosition);
}
ds.facetAddresses.pop();
delete ds.facetFunctionSelectors[_facetAddress].facetAddressPosition;
}
}
function initializeDiamondCut(address _init, bytes memory _calldata) internal {
if (_init == address(0)) {
require(_calldata.length == 0, "LibDiamondCut: _init is address(0) but _calldata is not empty");
} else {
require(_calldata.length > 0, "LibDiamondCut: _calldata is empty but _init is not address(0)");
if (_init != address(this)) {
enforceHasContractCode(_init, "LibDiamondCut: _init has no code");
}
(bool success, bytes memory error) = _init.delegatecall(_calldata);
if (!success) {
if (error.length > 0) {
revert(string(error));
} else {
revert("LibDiamondCut: _init function reverted");
}
}
}
}
function enforceHasContractCode(address _contract, string memory _errorMessage) internal view {
uint256 contractSize;
assembly {
contractSize := extcodesize(_contract)
}
require(contractSize > 0, _errorMessage);
}
}

View File

@@ -1,58 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title LibReentrancyGuard
* @notice Diamond-compatible reentrancy guard using Diamond storage pattern
* @dev Provides reentrancy protection for Diamond facets
*/
library LibReentrancyGuard {
bytes32 constant REENTRANCY_GUARD_STORAGE_POSITION = keccak256("asle.reentrancyguard.storage");
struct ReentrancyGuardStorage {
uint256 status; // 1 = locked, 2 = unlocked
}
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
function reentrancyGuardStorage() internal pure returns (ReentrancyGuardStorage storage rgs) {
bytes32 position = REENTRANCY_GUARD_STORAGE_POSITION;
assembly {
rgs.slot := position
}
}
/**
* @notice Initialize reentrancy guard
*/
function initialize() internal {
ReentrancyGuardStorage storage rgs = reentrancyGuardStorage();
require(rgs.status == 0, "LibReentrancyGuard: already initialized");
rgs.status = _NOT_ENTERED;
}
/**
* @notice Enter a non-reentrant function
*/
function enter() internal {
ReentrancyGuardStorage storage rgs = reentrancyGuardStorage();
// Initialize if not already done
if (rgs.status == 0) {
rgs.status = _NOT_ENTERED;
}
require(rgs.status != _ENTERED, "LibReentrancyGuard: reentrant call");
rgs.status = _ENTERED;
}
/**
* @notice Exit a non-reentrant function
*/
function exit() internal {
ReentrancyGuardStorage storage rgs = reentrancyGuardStorage();
rgs.status = _NOT_ENTERED;
}
}

View File

@@ -1,161 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
library PMMMath {
/**
* @dev Calculate price using DODO PMM formula
* @param i Oracle price
* @param k Slippage control coefficient (0-1, typically 0.1-0.3)
* @param Q Current quote token reserve
* @param vQ Virtual quote token reserve
* @return price Calculated price
*/
function calculatePrice(
uint256 i,
uint256 k,
uint256 Q,
uint256 vQ
) internal pure returns (uint256 price) {
require(vQ > 0, "PMMMath: vQ must be > 0");
require(k <= 1e18, "PMMMath: k must be <= 1");
// p = i * (1 + k * (Q - vQ) / vQ)
// Using fixed-point arithmetic with 1e18 precision
uint256 priceAdjustment = (Q > vQ)
? (k * (Q - vQ) * 1e18) / vQ
: (k * (vQ - Q) * 1e18) / vQ;
if (Q > vQ) {
price = (i * (1e18 + priceAdjustment)) / 1e18;
} else {
price = (i * (1e18 - priceAdjustment)) / 1e18;
}
}
/**
* @dev Calculate output amount for a swap using DODO PMM formula
* PMM Formula: R = i - (i * k * (B - B0) / B0)
* Where: i = oracle price, k = slippage coefficient, B = current balance, B0 = target balance
* @param amountIn Input amount
* @param reserveIn Input token reserve
* @param reserveOut Output token reserve
* @param virtualReserveIn Virtual input reserve (target balance)
* @param virtualReserveOut Virtual output reserve (target balance)
* @param k Slippage coefficient (0-1e18, typically 0.1e18-0.3e18)
* @param oraclePrice Oracle price (i) - price of quote/base in 1e18 precision
* @return amountOut Output amount
*/
function calculateSwapOutput(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut,
uint256 virtualReserveIn,
uint256 virtualReserveOut,
uint256 k,
uint256 oraclePrice
) internal pure returns (uint256 amountOut) {
require(amountIn > 0, "PMMMath: amountIn must be > 0");
require(virtualReserveIn > 0 && virtualReserveOut > 0, "PMMMath: virtual reserves must be > 0");
require(k <= 1e18, "PMMMath: k must be <= 1");
require(oraclePrice > 0, "PMMMath: oraclePrice must be > 0");
// Use virtual reserves for PMM calculation
uint256 newReserveIn = reserveIn + amountIn;
// Calculate new price after input
// Price formula: P = i * (1 + k * (Q - Q0) / Q0)
// Where Q is current quote reserve, Q0 is virtual quote reserve
// For base token: we calculate how much quote we get
// Calculate effective reserves (use virtual if larger)
uint256 effectiveBase = virtualReserveIn > reserveIn ? virtualReserveIn : reserveIn;
uint256 effectiveQuote = virtualReserveOut > reserveOut ? virtualReserveOut : reserveOut;
// DODO PMM: when buying quote with base
// New quote reserve = Q0 - (i * (B1 - B0) / (1 + k * (B1 - B0) / B0))
// Simplified: use constant product with virtual reserves adjusted by k
// Calculate price impact using PMM curve
// The curve ensures that as reserves move away from target, price adjusts
uint256 baseDiff = newReserveIn > virtualReserveIn
? newReserveIn - virtualReserveIn
: virtualReserveIn - newReserveIn;
// Calculate price adjustment factor
uint256 priceAdjustment = (baseDiff * k) / virtualReserveIn;
// Calculate output using PMM formula
// AmountOut = (amountIn * oraclePrice) / (1 + k * deviation)
uint256 baseAmountIn = newReserveIn - reserveIn;
// Convert to quote using oracle price with slippage
uint256 quoteValue = (baseAmountIn * oraclePrice) / 1e18;
// Apply PMM curve: reduce output as reserves deviate from target
if (newReserveIn > virtualReserveIn) {
// Price goes up (sell premium)
uint256 adjustedPrice = (oraclePrice * (1e18 + priceAdjustment)) / 1e18;
quoteValue = (baseAmountIn * adjustedPrice) / 1e18;
} else {
// Price goes down (buy discount)
uint256 adjustedPrice = (oraclePrice * (1e18 - priceAdjustment)) / 1e18;
quoteValue = (baseAmountIn * adjustedPrice) / 1e18;
}
// Ensure output doesn't exceed available reserves
amountOut = quoteValue < reserveOut ? quoteValue : reserveOut;
// Apply constant product as fallback for edge cases
if (amountOut == 0 || amountOut >= reserveOut) {
uint256 constantProduct = effectiveBase * effectiveQuote;
uint256 newEffectiveBase = effectiveBase + amountIn;
uint256 newEffectiveQuote = constantProduct / newEffectiveBase;
amountOut = effectiveQuote > newEffectiveQuote ? effectiveQuote - newEffectiveQuote : 0;
}
require(amountOut > 0, "PMMMath: insufficient liquidity");
require(amountOut <= reserveOut, "PMMMath: output exceeds reserves");
}
/**
* @dev Calculate LP shares for liquidity addition
* @param baseAmount Base token amount
* @param quoteAmount Quote token amount
* @param totalBaseReserve Total base reserve
* @param totalQuoteReserve Total quote reserve
* @param totalSupply Current total LP supply
* @return shares LP shares to mint
*/
function calculateLPShares(
uint256 baseAmount,
uint256 quoteAmount,
uint256 totalBaseReserve,
uint256 totalQuoteReserve,
uint256 totalSupply
) internal pure returns (uint256 shares) {
if (totalSupply == 0) {
// First liquidity provider
shares = sqrt(baseAmount * quoteAmount);
} else {
// Calculate shares proportionally
uint256 baseShares = (baseAmount * totalSupply) / totalBaseReserve;
uint256 quoteShares = (quoteAmount * totalSupply) / totalQuoteReserve;
shares = baseShares < quoteShares ? baseShares : quoteShares;
}
}
/**
* @dev Calculate square root using Babylonian method
*/
function sqrt(uint256 x) internal pure returns (uint256) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
uint256 y = x;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
return y;
}
}

View File

@@ -1,26 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Diamond} from "../src/core/Diamond.sol";
import {DiamondCutFacet} from "../src/core/facets/DiamondCutFacet.sol";
contract DiamondTest is Test {
Diamond public diamond;
DiamondCutFacet public diamondCutFacet;
function setUp() public {
diamond = new Diamond();
diamondCutFacet = new DiamondCutFacet();
}
function testDiamondDeployment() public {
assertTrue(address(diamond) != address(0));
}
function testFacetManagement() public {
// Test facet addition
assertTrue(true);
}
}

View File

@@ -1,20 +0,0 @@
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Diamond", function () {
it("Should deploy Diamond", async function () {
// Test Diamond deployment
expect(true).to.be.true;
});
it("Should add facets", async function () {
// Test facet addition
expect(true).to.be.true;
});
it("Should route function calls to correct facet", async function () {
// Test function routing
expect(true).to.be.true;
});
});

View File

@@ -1,153 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Diamond} from "../src/core/Diamond.sol";
import {DiamondCutFacet} from "../src/core/facets/DiamondCutFacet.sol";
import {DiamondInit} from "../src/core/DiamondInit.sol";
import {LiquidityFacet} from "../src/core/facets/LiquidityFacet.sol";
import {IDiamondCut} from "../src/interfaces/IDiamondCut.sol";
import {ILiquidityFacet} from "../src/interfaces/ILiquidityFacet.sol";
contract LiquidityFacetTest is Test {
Diamond public diamond;
DiamondCutFacet public diamondCutFacet;
DiamondInit public diamondInit;
LiquidityFacet public liquidityFacet;
address public owner;
address public user;
function setUp() public {
owner = address(this);
user = address(0x1);
// Deploy facets
diamondCutFacet = new DiamondCutFacet();
liquidityFacet = new LiquidityFacet();
diamondInit = new DiamondInit();
// Deploy diamond
diamond = new Diamond();
// Prepare cuts
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](2);
// Add DiamondCutFacet
bytes4[] memory diamondCutSelectors = new bytes4[](1);
diamondCutSelectors[0] = IDiamondCut.diamondCut.selector;
cuts[0] = IDiamondCut.FacetCut({
facetAddress: address(diamondCutFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: diamondCutSelectors
});
// Add LiquidityFacet (simplified selector list)
bytes4[] memory liquiditySelectors = new bytes4[](6);
liquiditySelectors[0] = ILiquidityFacet.createPool.selector;
liquiditySelectors[1] = ILiquidityFacet.getPool.selector;
liquiditySelectors[2] = ILiquidityFacet.getPrice.selector;
liquiditySelectors[3] = ILiquidityFacet.addLiquidity.selector;
liquiditySelectors[4] = ILiquidityFacet.swap.selector;
liquiditySelectors[5] = ILiquidityFacet.getQuote.selector;
cuts[1] = IDiamondCut.FacetCut({
facetAddress: address(liquidityFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: liquiditySelectors
});
// Initialize
bytes memory initData = abi.encodeWithSelector(DiamondInit.init.selector, owner);
IDiamondCut(address(diamond)).diamondCut(cuts, address(diamondInit), initData);
}
function testCreatePool() public {
address baseToken = address(0x100);
address quoteToken = address(0x200);
uint256 initialBaseReserve = 1000 ether;
uint256 initialQuoteReserve = 2000 ether;
uint256 virtualBaseReserve = 5000 ether;
uint256 virtualQuoteReserve = 10000 ether;
uint256 k = 5000; // 50% in basis points
uint256 oraclePrice = 2 ether;
uint256 poolId = ILiquidityFacet(address(diamond)).createPool(
baseToken,
quoteToken,
initialBaseReserve,
initialQuoteReserve,
virtualBaseReserve,
virtualQuoteReserve,
k,
oraclePrice,
address(0) // No oracle for now
);
assertEq(poolId, 0, "First pool should have ID 0");
ILiquidityFacet.Pool memory pool = ILiquidityFacet(address(diamond)).getPool(poolId);
assertEq(pool.baseToken, baseToken);
assertEq(pool.quoteToken, quoteToken);
assertEq(pool.baseReserve, initialBaseReserve);
assertEq(pool.quoteReserve, initialQuoteReserve);
assertTrue(pool.active, "Pool should be active");
}
function testGetPrice() public {
// Create pool first
address baseToken = address(0x100);
address quoteToken = address(0x200);
uint256 poolId = ILiquidityFacet(address(diamond)).createPool(
baseToken,
quoteToken,
1000 ether,
2000 ether,
5000 ether,
10000 ether,
5000,
2 ether,
address(0)
);
uint256 price = ILiquidityFacet(address(diamond)).getPrice(poolId);
assertGt(price, 0, "Price should be greater than 0");
}
function testMultiplePools() public {
address baseToken1 = address(0x100);
address quoteToken1 = address(0x200);
uint256 poolId1 = ILiquidityFacet(address(diamond)).createPool(
baseToken1,
quoteToken1,
1000 ether,
2000 ether,
5000 ether,
10000 ether,
5000,
2 ether,
address(0)
);
uint256 poolId2 = ILiquidityFacet(address(diamond)).createPool(
address(0x300),
address(0x400),
500 ether,
1000 ether,
2500 ether,
5000 ether,
5000,
2 ether,
address(0)
);
assertEq(poolId1, 0);
assertEq(poolId2, 1);
ILiquidityFacet.Pool memory pool1 = ILiquidityFacet(address(diamond)).getPool(poolId1);
ILiquidityFacet.Pool memory pool2 = ILiquidityFacet(address(diamond)).getPool(poolId2);
assertEq(pool1.baseToken, baseToken1);
assertEq(pool2.baseToken, address(0x300));
}
}

View File

@@ -1,32 +0,0 @@
import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract } from "ethers";
describe("LiquidityFacet", function () {
let diamond: Contract;
let liquidityFacet: Contract;
let baseToken: Contract;
let quoteToken: Contract;
beforeEach(async function () {
// Deploy mock ERC20 tokens
const ERC20Factory = await ethers.getContractFactory("ERC20Mock");
baseToken = await ERC20Factory.deploy("Base Token", "BASE", ethers.parseEther("1000000"));
quoteToken = await ERC20Factory.deploy("Quote Token", "QUOTE", ethers.parseEther("1000000"));
// Deploy Diamond and facets (simplified for testing)
// In production, you would deploy the full Diamond setup
});
it("Should create a pool", async function () {
// Test pool creation
// This is a placeholder - actual implementation would test the full flow
expect(true).to.be.true;
});
it("Should calculate price correctly", async function () {
// Test PMM price calculation
expect(true).to.be.true;
});
});

View File

@@ -1,28 +0,0 @@
import { expect } from "chai";
import { ethers } from "hardhat";
import { PMMMath } from "../libraries/PMMMath.sol";
describe("PMMMath", function () {
it("Should calculate price correctly", async function () {
// Test PMM price formula: p = i * (1 + k * (Q - vQ) / vQ)
const i = ethers.parseEther("1"); // Oracle price
const k = ethers.parseEther("0.1"); // 10% slippage coefficient
const Q = ethers.parseEther("2000"); // Current quote reserve
const vQ = ethers.parseEther("1000"); // Virtual quote reserve
// Expected: p = 1 * (1 + 0.1 * (2000 - 1000) / 1000) = 1.1
// This is a placeholder - actual test would use a test contract
expect(true).to.be.true;
});
it("Should calculate swap output correctly", async function () {
// Test swap calculation
expect(true).to.be.true;
});
it("Should calculate LP shares correctly", async function () {
// Test LP share calculation
expect(true).to.be.true;
});
});

View File

@@ -1,104 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Diamond} from "../src/core/Diamond.sol";
import {DiamondCutFacet} from "../src/core/facets/DiamondCutFacet.sol";
import {DiamondInit} from "../src/core/DiamondInit.sol";
import {VaultFacet} from "../src/core/facets/VaultFacet.sol";
import {IDiamondCut} from "../src/interfaces/IDiamondCut.sol";
import {IVaultFacet} from "../src/interfaces/IVaultFacet.sol";
import {ERC20Mock} from "./mocks/ERC20Mock.sol";
contract VaultFacetTest is Test {
Diamond public diamond;
DiamondCutFacet public diamondCutFacet;
DiamondInit public diamondInit;
VaultFacet public vaultFacet;
ERC20Mock public asset;
address public owner;
address public user;
function setUp() public {
owner = address(this);
user = address(0x1);
// Deploy mock ERC20
asset = new ERC20Mock("Test Asset", "TA", 18);
// Deploy facets
diamondCutFacet = new DiamondCutFacet();
vaultFacet = new VaultFacet();
diamondInit = new DiamondInit();
// Deploy diamond
diamond = new Diamond();
// Prepare cuts
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](2);
// Add DiamondCutFacet
bytes4[] memory diamondCutSelectors = new bytes4[](1);
diamondCutSelectors[0] = IDiamondCut.diamondCut.selector;
cuts[0] = IDiamondCut.FacetCut({
facetAddress: address(diamondCutFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: diamondCutSelectors
});
// Add VaultFacet
bytes4[] memory vaultSelectors = new bytes4[](5);
vaultSelectors[0] = IVaultFacet.createVault.selector;
vaultSelectors[1] = IVaultFacet.getVault.selector;
vaultSelectors[2] = IVaultFacet.deposit.selector;
vaultSelectors[3] = IVaultFacet.convertToShares.selector;
vaultSelectors[4] = IVaultFacet.convertToAssets.selector;
cuts[1] = IDiamondCut.FacetCut({
facetAddress: address(vaultFacet),
action: IDiamondCut.FacetCutAction.Add,
functionSelectors: vaultSelectors
});
// Initialize
bytes memory initData = abi.encodeWithSelector(DiamondInit.init.selector, owner);
IDiamondCut(address(diamond)).diamondCut(cuts, address(diamondInit), initData);
}
function testCreateVault() public {
uint256 vaultId = IVaultFacet(address(diamond)).createVault(address(asset), false);
assertEq(vaultId, 0, "First vault should have ID 0");
IVaultFacet.Vault memory vault = IVaultFacet(address(diamond)).getVault(vaultId);
assertEq(vault.asset, address(asset));
assertFalse(vault.isMultiAsset);
assertTrue(vault.active);
}
function testCreateMultiAssetVault() public {
uint256 vaultId = IVaultFacet(address(diamond)).createVault(address(0), true);
IVaultFacet.Vault memory vault = IVaultFacet(address(diamond)).getVault(vaultId);
assertTrue(vault.isMultiAsset);
assertTrue(vault.active);
}
function testConvertToShares() public {
uint256 vaultId = IVaultFacet(address(diamond)).createVault(address(asset), false);
// First deposit - should be 1:1
uint256 assets = 1000 ether;
uint256 shares = IVaultFacet(address(diamond)).convertToShares(vaultId, assets);
assertEq(shares, assets, "First deposit should be 1:1");
}
function testConvertToAssets() public {
uint256 vaultId = IVaultFacet(address(diamond)).createVault(address(asset), false);
uint256 shares = 1000 ether;
uint256 assets = IVaultFacet(address(diamond)).convertToAssets(vaultId, shares);
// Empty vault should return 0
assertEq(assets, 0);
}
}

View File

@@ -1,23 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor(
string memory name,
string memory symbol,
uint8 decimals
) ERC20(name, symbol) {
_mint(msg.sender, 1000000 * 10 ** decimals);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
_burn(from, amount);
}
}

1
frontend Submodule

Submodule frontend added at 3c1843a22b

41
frontend/.gitignore vendored
View File

@@ -1,41 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,39 +0,0 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]

View File

@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -1,64 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
export default function AdminAuditPage() {
const [logs, setLogs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchLogs();
}, []);
const fetchLogs = async () => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/audit-logs', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setLogs(data);
} catch (error) {
console.error('Failed to fetch logs:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <div className="text-center py-12">Loading...</div>;
}
return (
<div className="px-4 py-6 sm:px-0">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Audit Logs</h1>
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{logs.map((log) => (
<li key={log.id} className="px-4 py-4 sm:px-6">
<div className="flex justify-between items-start">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">
{log.action} - {log.resource || 'N/A'}
</p>
<p className="text-sm text-gray-500 mt-1">
{log.adminUser?.email} | {new Date(log.timestamp).toLocaleString()}
</p>
{log.details && (
<pre className="mt-2 text-xs bg-gray-50 p-2 rounded">
{JSON.stringify(log.details, null, 2)}
</pre>
)}
</div>
</div>
</li>
))}
</ul>
</div>
</div>
);
}

View File

@@ -1,125 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
export default function AdminConfigPage() {
const [configs, setConfigs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [editingKey, setEditingKey] = useState<string | null>(null);
const [editValue, setEditValue] = useState('');
useEffect(() => {
fetchConfigs();
}, []);
const fetchConfigs = async () => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/config', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setConfigs(data);
} catch (error) {
console.error('Failed to fetch configs:', error);
} finally {
setLoading(false);
}
};
const handleSave = async (key: string) => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
key,
value: JSON.parse(editValue),
}),
});
if (res.ok) {
setEditingKey(null);
fetchConfigs();
}
} catch (error) {
console.error('Failed to save config:', error);
}
};
if (loading) {
return <div className="text-center py-12">Loading...</div>;
}
return (
<div className="px-4 py-6 sm:px-0">
<h1 className="text-3xl font-bold text-gray-900 mb-6">System Configuration</h1>
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{configs.map((config) => (
<li key={config.key} className="px-4 py-4 sm:px-6">
<div className="flex justify-between items-start">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{config.key}</p>
<p className="text-sm text-gray-500 mt-1">
{config.description || 'No description'}
</p>
{editingKey === config.key ? (
<textarea
className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-md"
rows={3}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
/>
) : (
<pre className="mt-2 text-xs bg-gray-50 p-2 rounded">
{JSON.stringify(config.value, null, 2)}
</pre>
)}
</div>
<div className="ml-4">
{editingKey === config.key ? (
<div className="flex space-x-2">
<button
onClick={() => handleSave(config.key)}
className="text-blue-600 hover:text-blue-900 text-sm"
>
Save
</button>
<button
onClick={() => {
setEditingKey(null);
setEditValue('');
}}
className="text-gray-600 hover:text-gray-900 text-sm"
>
Cancel
</button>
</div>
) : (
<button
onClick={() => {
setEditingKey(config.key);
setEditValue(JSON.stringify(config.value, null, 2));
}}
className="text-blue-600 hover:text-blue-900 text-sm"
>
Edit
</button>
)}
</div>
</div>
</li>
))}
</ul>
</div>
</div>
);
}

View File

@@ -1,132 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
export default function AdminDeploymentsPage() {
const [deployments, setDeployments] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedDeployment, setSelectedDeployment] = useState<any>(null);
useEffect(() => {
fetchDeployments();
}, []);
const fetchDeployments = async () => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/deployments', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setDeployments(data);
} catch (error) {
console.error('Failed to fetch deployments:', error);
} finally {
setLoading(false);
}
};
const handleDeploy = async (id: string) => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch(`/api/admin/deployments/${id}/status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ status: 'deploying' }),
});
if (res.ok) {
fetchDeployments();
}
} catch (error) {
console.error('Failed to deploy:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'success':
return 'bg-green-100 text-green-800';
case 'failed':
return 'bg-red-100 text-red-800';
case 'deploying':
return 'bg-yellow-100 text-yellow-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (loading) {
return <div className="text-center py-12">Loading...</div>;
}
return (
<div className="px-4 py-6 sm:px-0">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Deployments</h1>
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{deployments.map((deployment) => (
<li key={deployment.id} className="px-4 py-4 sm:px-6">
<div className="flex justify-between items-center">
<div>
<p className="text-sm font-medium text-gray-900">
{deployment.name} - {deployment.environment}
</p>
<p className="text-sm text-gray-500">
Version: {deployment.version} | Created: {new Date(deployment.createdAt).toLocaleDateString()}
</p>
</div>
<div className="flex items-center space-x-4">
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(
deployment.status
)}`}
>
{deployment.status}
</span>
{deployment.status === 'pending' && (
<button
onClick={() => handleDeploy(deployment.id)}
className="text-blue-600 hover:text-blue-900 text-sm"
>
Deploy
</button>
)}
<button
onClick={() => setSelectedDeployment(deployment)}
className="text-gray-600 hover:text-gray-900 text-sm"
>
View Logs
</button>
</div>
</div>
</li>
))}
</ul>
</div>
{selectedDeployment && (
<div className="mt-6 bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium mb-4">Deployment Logs</h3>
<div className="bg-gray-50 rounded p-4 max-h-96 overflow-y-auto">
<pre className="text-xs">
{JSON.stringify(selectedDeployment, null, 2)}
</pre>
</div>
<button
onClick={() => setSelectedDeployment(null)}
className="mt-4 text-sm text-gray-600 hover:text-gray-900"
>
Close
</button>
</div>
)}
</div>
);
}

View File

@@ -1,124 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import Link from 'next/link';
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const [admin, setAdmin] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('admin_token');
if (!token) {
router.push('/admin/login');
return;
}
// Verify token
fetch('/api/admin/auth/verify', {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((res) => {
if (!res.ok) {
router.push('/admin/login');
return;
}
return res.json();
})
.then((data) => {
if (data) setAdmin(data);
setLoading(false);
})
.catch(() => {
router.push('/admin/login');
});
}, [router]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
if (!admin) return null;
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<h1 className="text-xl font-bold text-gray-900">ASLE Admin</h1>
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<Link
href="/admin"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Dashboard
</Link>
<Link
href="/admin/users"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Users
</Link>
<Link
href="/admin/config"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Config
</Link>
<Link
href="/admin/deployments"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Deployments
</Link>
<Link
href="/admin/white-label"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
White-Label
</Link>
<Link
href="/admin/audit"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Audit Logs
</Link>
</div>
</div>
<div className="flex items-center">
<span className="text-sm text-gray-700 mr-4">{admin.email}</span>
<button
onClick={() => {
localStorage.removeItem('admin_token');
router.push('/admin/login');
}}
className="text-sm text-gray-500 hover:text-gray-700"
>
Logout
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
{children}
</main>
</div>
);
}

View File

@@ -1,105 +0,0 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function AdminLoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const res = await fetch('/api/admin/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Login failed');
}
localStorage.setItem('admin_token', data.token);
router.push('/admin');
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Admin Login
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-800">{error}</div>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -1,147 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
export default function AdminDashboard() {
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('admin_token');
// Fetch dashboard stats
Promise.all([
fetch('/api/pools').then((r) => r.json()),
fetch('/api/vaults').then((r) => r.json()),
fetch('/api/compliance').then((r) => r.json()),
])
.then(([pools, vaults, compliance]) => {
setStats({
pools: Array.isArray(pools) ? pools.length : 0,
vaults: Array.isArray(vaults) ? vaults.length : 0,
complianceRecords: Array.isArray(compliance) ? compliance.length : 0,
});
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
if (loading) {
return <div className="text-center py-12">Loading...</div>;
}
return (
<div className="px-4 py-6 sm:px-0">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="mt-2 text-sm text-gray-600">
Overview of platform statistics and system health
</p>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<span className="text-white text-sm font-bold">P</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Active Pools
</dt>
<dd className="text-lg font-medium text-gray-900">
{stats?.pools || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
<span className="text-white text-sm font-bold">V</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Active Vaults
</dt>
<dd className="text-lg font-medium text-gray-900">
{stats?.vaults || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
<span className="text-white text-sm font-bold">C</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Compliance Records
</dt>
<dd className="text-lg font-medium text-gray-900">
{stats?.complianceRecords || 0}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Quick Actions
</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<a
href="/admin/users"
className="block p-4 bg-white rounded-lg shadow hover:shadow-md transition"
>
<h3 className="font-medium text-gray-900">Manage Users</h3>
<p className="text-sm text-gray-500 mt-1">
View and manage admin users
</p>
</a>
<a
href="/admin/config"
className="block p-4 bg-white rounded-lg shadow hover:shadow-md transition"
>
<h3 className="font-medium text-gray-900">System Config</h3>
<p className="text-sm text-gray-500 mt-1">
Configure system settings
</p>
</a>
<a
href="/admin/deployments"
className="block p-4 bg-white rounded-lg shadow hover:shadow-md transition"
>
<h3 className="font-medium text-gray-900">Deployments</h3>
<p className="text-sm text-gray-500 mt-1">
Manage deployments and rollbacks
</p>
</a>
</div>
</div>
</div>
);
}

View File

@@ -1,182 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
export default function AdminUsersPage() {
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: '',
role: 'admin',
permissions: [] as string[],
});
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/users', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setUsers(data);
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setLoading(false);
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(formData),
});
if (res.ok) {
setShowCreateModal(false);
setFormData({ email: '', password: '', role: 'admin', permissions: [] });
fetchUsers();
}
} catch (error) {
console.error('Failed to create user:', error);
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this user?')) return;
const token = localStorage.getItem('admin_token');
try {
const res = await fetch(`/api/admin/users/${id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
fetchUsers();
}
} catch (error) {
console.error('Failed to delete user:', error);
}
};
if (loading) {
return <div className="text-center py-12">Loading...</div>;
}
return (
<div className="px-4 py-6 sm:px-0">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Admin Users</h1>
<button
onClick={() => setShowCreateModal(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
>
Create User
</button>
</div>
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{users.map((user) => (
<li key={user.id}>
<div className="px-4 py-4 sm:px-6 flex justify-between items-center">
<div>
<p className="text-sm font-medium text-gray-900">{user.email}</p>
<p className="text-sm text-gray-500">
Role: {user.role} | Permissions: {user.permissions.length}
</p>
</div>
<button
onClick={() => handleDelete(user.id)}
className="text-red-600 hover:text-red-900 text-sm"
>
Delete
</button>
</div>
</li>
))}
</ul>
</div>
{showCreateModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<h3 className="text-lg font-bold mb-4">Create Admin User</h3>
<form onSubmit={handleCreate}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
type="password"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Role
</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md"
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
>
<option value="admin">Admin</option>
<option value="super_admin">Super Admin</option>
<option value="operator">Operator</option>
</select>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Create
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,224 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
export default function WhiteLabelPage() {
const [configs, setConfigs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [formData, setFormData] = useState({
name: '',
domain: '',
logoUrl: '',
primaryColor: '#3B82F6',
secondaryColor: '#8B5CF6',
});
useEffect(() => {
fetchConfigs();
}, []);
const fetchConfigs = async () => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/white-label', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setConfigs(data);
} catch (error) {
console.error('Failed to fetch configs:', error);
} finally {
setLoading(false);
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
const token = localStorage.getItem('admin_token');
try {
const res = await fetch('/api/admin/white-label', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(formData),
});
if (res.ok) {
setShowCreateModal(false);
setFormData({
name: '',
domain: '',
logoUrl: '',
primaryColor: '#3B82F6',
secondaryColor: '#8B5CF6',
});
fetchConfigs();
}
} catch (error) {
console.error('Failed to create config:', error);
}
};
const handleToggle = async (id: string) => {
const token = localStorage.getItem('admin_token');
try {
const res = await fetch(`/api/admin/white-label/${id}/toggle`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
fetchConfigs();
}
} catch (error) {
console.error('Failed to toggle:', error);
}
};
if (loading) {
return <div className="text-center py-12">Loading...</div>;
}
return (
<div className="px-4 py-6 sm:px-0">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">White-Label Configs</h1>
<button
onClick={() => setShowCreateModal(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
>
Create Config
</button>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{configs.map((config) => (
<div key={config.id} className="bg-white shadow rounded-lg p-6">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-medium text-gray-900">{config.name}</h3>
<p className="text-sm text-gray-500">{config.domain}</p>
</div>
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${
config.active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{config.active ? 'Active' : 'Inactive'}
</span>
</div>
{config.logoUrl && (
<img src={config.logoUrl} alt="Logo" className="h-12 mb-4" />
)}
<div className="flex space-x-2 mb-4">
<div
className="w-8 h-8 rounded"
style={{ backgroundColor: config.primaryColor }}
/>
<div
className="w-8 h-8 rounded"
style={{ backgroundColor: config.secondaryColor }}
/>
</div>
<button
onClick={() => handleToggle(config.id)}
className="text-sm text-blue-600 hover:text-blue-900"
>
{config.active ? 'Deactivate' : 'Activate'}
</button>
</div>
))}
</div>
{showCreateModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<h3 className="text-lg font-bold mb-4">Create White-Label Config</h3>
<form onSubmit={handleCreate}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Domain
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md"
value={formData.domain}
onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Logo URL
</label>
<input
type="url"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
value={formData.logoUrl}
onChange={(e) => setFormData({ ...formData, logoUrl: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Primary Color
</label>
<input
type="color"
className="w-full h-10 border border-gray-300 rounded-md"
value={formData.primaryColor}
onChange={(e) => setFormData({ ...formData, primaryColor: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Secondary Color
</label>
<input
type="color"
className="w-full h-10 border border-gray-300 rounded-md"
value={formData.secondaryColor}
onChange={(e) => setFormData({ ...formData, secondaryColor: e.target.value })}
/>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Create
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,60 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { PoolAnalytics } from '@/components/analytics/PoolAnalytics'
import { PortfolioTracker } from '@/components/analytics/PortfolioTracker'
import { PerformanceMetrics } from '@/components/analytics/PerformanceMetrics'
import { HistoricalCharts } from '@/components/analytics/HistoricalCharts'
import { RealTimeMetrics } from '@/components/analytics/RealTimeMetrics'
import { useAccount } from 'wagmi'
export default function AnalyticsPage() {
const { address } = useAccount()
const [activeTab, setActiveTab] = useState<'pools' | 'portfolio' | 'performance' | 'historical' | 'realtime'>('pools')
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Analytics Dashboard</h1>
<p className="mt-2 text-gray-600">Comprehensive analytics and insights for ASLE platform</p>
</div>
{/* Tab Navigation */}
<div className="mb-6 border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{[
{ id: 'pools', label: 'Pool Analytics' },
{ id: 'portfolio', label: 'Portfolio' },
{ id: 'performance', label: 'Performance' },
{ id: 'historical', label: 'Historical Data' },
{ id: 'realtime', label: 'Real-Time' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="mt-6">
{activeTab === 'pools' && <PoolAnalytics />}
{activeTab === 'portfolio' && <PortfolioTracker userAddress={address || ''} />}
{activeTab === 'performance' && <PerformanceMetrics />}
{activeTab === 'historical' && <HistoricalCharts />}
{activeTab === 'realtime' && <RealTimeMetrics />}
</div>
</div>
</div>
)
}

View File

@@ -1,259 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useAccount } from 'wagmi'
import { ComplianceSelector } from '@/components/ComplianceSelector'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import api from '@/lib/api'
import toast from 'react-hot-toast'
interface ComplianceRecord {
userAddress: string
complianceMode: string
kycVerified: boolean
amlVerified: boolean
kycProvider?: string
amlProvider?: string
lastKYCUpdate?: string
lastAMLUpdate?: string
}
export default function CompliancePage() {
const { address, isConnected } = useAccount()
const [loading, setLoading] = useState(false)
const [complianceRecord, setComplianceRecord] = useState<ComplianceRecord | null>(null)
const [ofacCheck, setOfacCheck] = useState<{ sanctioned: boolean; timestamp: number } | null>(null)
useEffect(() => {
if (address) {
fetchComplianceRecord()
checkOFAC()
}
}, [address])
const fetchComplianceRecord = async () => {
if (!address) return
try {
const response = await api.get(`/compliance/record/${address}`)
if (response.data.success) {
setComplianceRecord(response.data.record)
}
} catch (error: any) {
console.error('Error fetching compliance record:', error)
}
}
const checkOFAC = async () => {
if (!address) return
try {
const response = await api.post('/compliance/ofac/check', { userAddress: address })
if (response.data.success) {
setOfacCheck(response.data.result)
}
} catch (error: any) {
console.error('Error checking OFAC:', error)
}
}
const handleKYCVerification = async () => {
if (!address) {
toast.error('Please connect your wallet')
return
}
setLoading(true)
try {
const response = await api.post('/compliance/kyc/verify', {
userAddress: address,
provider: 'default'
})
if (response.data.success) {
toast.success('KYC verification initiated')
await fetchComplianceRecord()
}
} catch (error: any) {
toast.error(error.response?.data?.error || 'KYC verification failed')
} finally {
setLoading(false)
}
}
const handleAMLVerification = async () => {
if (!address) {
toast.error('Please connect your wallet')
return
}
setLoading(true)
try {
const response = await api.post('/compliance/aml/verify', {
userAddress: address,
provider: 'default'
})
if (response.data.success) {
toast.success('AML verification initiated')
await fetchComplianceRecord()
}
} catch (error: any) {
toast.error(error.response?.data?.error || 'AML verification failed')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold mb-8">Compliance Management</h1>
{!isConnected && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-yellow-800">Please connect your wallet to view compliance status</p>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComplianceSelector />
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">KYC/AML Verification</h2>
<div className="space-y-4">
<button
onClick={handleKYCVerification}
disabled={loading || !address}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{loading ? <LoadingSpinner size="sm" /> : 'Verify KYC'}
</button>
{complianceRecord && (
<div className={`p-3 rounded ${complianceRecord.kycVerified ? 'bg-green-50' : 'bg-gray-50'}`}>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium">KYC Status:</span>
<span className={`text-sm font-semibold ${complianceRecord.kycVerified ? 'text-green-600' : 'text-gray-600'}`}>
{complianceRecord.kycVerified ? 'Verified' : 'Not Verified'}
</span>
</div>
{complianceRecord.kycProvider && (
<p className="text-xs text-gray-600">Provider: {complianceRecord.kycProvider}</p>
)}
{complianceRecord.lastKYCUpdate && (
<p className="text-xs text-gray-500">
Last updated: {new Date(complianceRecord.lastKYCUpdate).toLocaleDateString()}
</p>
)}
</div>
)}
<button
onClick={handleAMLVerification}
disabled={loading || !address}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{loading ? <LoadingSpinner size="sm" /> : 'Verify AML'}
</button>
{complianceRecord && (
<div className={`p-3 rounded ${complianceRecord.amlVerified ? 'bg-green-50' : 'bg-gray-50'}`}>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium">AML Status:</span>
<span className={`text-sm font-semibold ${complianceRecord.amlVerified ? 'text-green-600' : 'text-gray-600'}`}>
{complianceRecord.amlVerified ? 'Passed' : 'Pending'}
</span>
</div>
{complianceRecord.amlProvider && (
<p className="text-xs text-gray-600">Provider: {complianceRecord.amlProvider}</p>
)}
{complianceRecord.lastAMLUpdate && (
<p className="text-xs text-gray-500">
Last updated: {new Date(complianceRecord.lastAMLUpdate).toLocaleDateString()}
</p>
)}
</div>
)}
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">OFAC Sanctions Check</h2>
{ofacCheck ? (
<div className={`p-4 rounded ${ofacCheck.sanctioned ? 'bg-red-50 border border-red-200' : 'bg-green-50 border border-green-200'}`}>
<div className="flex justify-between items-center">
<span className="font-medium">Status:</span>
<span className={`font-semibold ${ofacCheck.sanctioned ? 'text-red-600' : 'text-green-600'}`}>
{ofacCheck.sanctioned ? 'SANCTIONED' : 'CLEAR'}
</span>
</div>
<p className="text-xs text-gray-500 mt-2">
Checked: {new Date(ofacCheck.timestamp).toLocaleString()}
</p>
</div>
) : (
<p className="text-gray-500 text-sm">Connect wallet to check OFAC status</p>
)}
<button
onClick={checkOFAC}
disabled={!address}
className="mt-4 w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
>
Refresh Check
</button>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Regulatory Features</h2>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-sm">ISO 20022 Messaging</span>
<span className="text-green-600 text-sm font-semibold">Enabled</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-sm">FATF Travel Rule</span>
<span className="text-green-600 text-sm font-semibold">Enabled</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-sm">OFAC Screening</span>
<span className="text-green-600 text-sm font-semibold">Active</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-sm">Audit Trail</span>
<span className="text-green-600 text-sm font-semibold">Recording</span>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Compliance Modes</h2>
<div className="space-y-3">
<div className="p-4 border-2 border-blue-200 rounded-lg bg-blue-50">
<h3 className="font-semibold text-blue-900">Mode A: Regulated</h3>
<p className="text-sm text-blue-700 mt-1">Full KYC/AML, ISO 20022, FATF Travel Rule</p>
<ul className="text-xs text-blue-600 mt-2 list-disc list-inside">
<li>Complete identity verification</li>
<li>AML screening required</li>
<li>Travel Rule compliance</li>
</ul>
</div>
<div className="p-4 border-2 border-green-200 rounded-lg bg-green-50">
<h3 className="font-semibold text-green-900">Mode B: Fintech</h3>
<p className="text-sm text-green-700 mt-1">Tiered KYC, Risk-based monitoring</p>
<ul className="text-xs text-green-600 mt-2 list-disc list-inside">
<li>Tiered verification levels</li>
<li>Risk-based AML checks</li>
<li>Transaction monitoring</li>
</ul>
</div>
<div className="p-4 border-2 border-gray-200 rounded-lg bg-gray-50">
<h3 className="font-semibold text-gray-900">Mode C: Decentralized</h3>
<p className="text-sm text-gray-700 mt-1">Non-custodial, Permissionless</p>
<ul className="text-xs text-gray-600 mt-2 list-disc list-inside">
<li>No KYC required</li>
<li>Fully permissionless</li>
<li>User-controlled assets</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,203 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface SARReport {
id: string
reportId: string
transactionHash: string
userAddress: string
amount: string
reason: string
status: string
submittedAt?: string
jurisdiction: string
createdAt: string
}
interface CTRReport {
id: string
reportId: string
transactionHash: string
userAddress: string
amount: string
currency: string
transactionType: string
status: string
submittedAt?: string
jurisdiction: string
createdAt: string
}
export default function ComplianceReportsPage() {
const [activeTab, setActiveTab] = useState<'sar' | 'ctr'>('sar')
const [sarReports, setSarReports] = useState<SARReport[]>([])
const [ctrReports, setCtrReports] = useState<CTRReport[]>([])
const [loading, setLoading] = useState(false)
const [showCreateModal, setShowCreateModal] = useState(false)
useEffect(() => {
fetchReports()
}, [activeTab])
const fetchReports = async () => {
setLoading(true)
try {
if (activeTab === 'sar') {
const response = await axios.get(`${API_URL}/api/compliance/reports/sar`)
setSarReports(response.data)
} else {
const response = await axios.get(`${API_URL}/api/compliance/reports/ctr`)
setCtrReports(response.data)
}
} catch (error) {
console.error('Error fetching reports:', error)
} finally {
setLoading(false)
}
}
const handleSubmit = async (reportId: string, type: 'sar' | 'ctr') => {
try {
await axios.post(`${API_URL}/api/compliance/reports/${type}/${reportId}/submit`)
fetchReports()
} catch (error) {
console.error('Error submitting report:', error)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'submitted':
return 'bg-green-100 text-green-800'
case 'draft':
return 'bg-yellow-100 text-yellow-800'
case 'acknowledged':
return 'bg-blue-100 text-blue-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Regulatory Reports</h1>
<p className="mt-2 text-gray-600">Manage SAR and CTR reports</p>
</div>
<div className="mb-6 border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('sar')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'sar'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
SAR Reports ({sarReports.length})
</button>
<button
onClick={() => setActiveTab('ctr')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'ctr'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
CTR Reports ({ctrReports.length})
</button>
</nav>
</div>
{loading ? (
<div className="text-center py-12">Loading reports...</div>
) : (
<div className="bg-white shadow rounded-lg overflow-hidden">
{activeTab === 'sar' ? (
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Report ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User Address</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sarReports.map((report) => (
<tr key={report.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">{report.reportId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{report.userAddress.slice(0, 10)}...</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{report.amount}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(report.status)}`}>
{report.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{report.status === 'draft' && (
<button
onClick={() => handleSubmit(report.id, 'sar')}
className="text-blue-600 hover:text-blue-900"
>
Submit
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
) : (
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Report ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User Address</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Currency</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{ctrReports.map((report) => (
<tr key={report.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">{report.reportId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{report.userAddress.slice(0, 10)}...</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{report.amount}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{report.currency}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(report.status)}`}>
{report.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{report.status === 'draft' && (
<button
onClick={() => handleSubmit(report.id, 'ctr')}
className="text-blue-600 hover:text-blue-900"
>
Submit
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,179 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface ScreeningResult {
id: string
address: string
riskScore: number
sanctions: boolean
passed: boolean
providers: string[]
action: string
timestamp: string
}
export default function ScreeningPage() {
const [address, setAddress] = useState('')
const [results, setResults] = useState<ScreeningResult[]>([])
const [loading, setLoading] = useState(false)
const [metrics, setMetrics] = useState<any>(null)
useEffect(() => {
fetchMetrics()
fetchRecentResults()
}, [])
const fetchMetrics = async () => {
try {
const response = await axios.get(`${API_URL}/api/compliance/analytics/metrics`)
setMetrics(response.data)
} catch (error) {
console.error('Error fetching metrics:', error)
}
}
const fetchRecentResults = async () => {
try {
const response = await axios.get(`${API_URL}/api/compliance/screening/recent`)
setResults(response.data)
} catch (error) {
console.error('Error fetching results:', error)
}
}
const handleScreen = async () => {
if (!address) return
setLoading(true)
try {
const response = await axios.post(`${API_URL}/api/compliance/screening/screen`, {
address,
})
setResults([response.data, ...results])
setAddress('')
fetchMetrics()
} catch (error) {
console.error('Error screening address:', error)
} finally {
setLoading(false)
}
}
const getRiskColor = (riskScore: number) => {
if (riskScore >= 70) return 'text-red-600'
if (riskScore >= 40) return 'text-yellow-600'
return 'text-green-600'
}
const getActionColor = (action: string) => {
switch (action) {
case 'block':
return 'bg-red-100 text-red-800'
case 'review':
return 'bg-yellow-100 text-yellow-800'
default:
return 'bg-green-100 text-green-800'
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Real-Time Screening</h1>
<p className="mt-2 text-gray-600">Screen addresses for sanctions and risk</p>
</div>
{/* Metrics */}
{metrics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Screenings</h3>
<p className="text-2xl font-bold mt-2">{metrics.screeningVolume24h || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Sanctions Detected</h3>
<p className="text-2xl font-bold mt-2">{metrics.totalSanctionsDetected || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Avg Risk Score</h3>
<p className="text-2xl font-bold mt-2">{metrics.averageRiskScore?.toFixed(1) || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Compliance Rate</h3>
<p className="text-2xl font-bold mt-2">{metrics.complianceRate?.toFixed(1) || 0}%</p>
</div>
</div>
)}
{/* Screening Input */}
<div className="bg-white p-6 rounded-lg shadow mb-6">
<div className="flex gap-4">
<input
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="Enter address to screen"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md"
/>
<button
onClick={handleScreen}
disabled={loading || !address}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Screening...' : 'Screen Address'}
</button>
</div>
</div>
{/* Results */}
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Address</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Risk Score</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Sanctions</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Providers</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Timestamp</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{results.map((result) => (
<tr key={result.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">{result.address.slice(0, 10)}...</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-semibold ${getRiskColor(result.riskScore)}`}>
{result.riskScore.toFixed(1)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{result.sanctions ? (
<span className="text-red-600 font-semibold">Yes</span>
) : (
<span className="text-green-600">No</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getActionColor(result.action)}`}>
{result.action}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{result.providers.join(', ')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(result.timestamp).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

View File

@@ -1,133 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface Workflow {
id: string
name: string
description: string
steps: any[]
active: boolean
}
interface WorkflowExecution {
id: string
workflowId: string
userAddress: string
currentStep: number
status: string
results: any
}
export default function WorkflowsPage() {
const [workflows, setWorkflows] = useState<Workflow[]>([])
const [executions, setExecutions] = useState<WorkflowExecution[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchWorkflows()
fetchExecutions()
}, [])
const fetchWorkflows = async () => {
try {
const response = await axios.get(`${API_URL}/api/compliance/workflows`)
setWorkflows(response.data)
} catch (error) {
console.error('Error fetching workflows:', error)
}
}
const fetchExecutions = async () => {
try {
const response = await axios.get(`${API_URL}/api/compliance/workflows/executions`)
setExecutions(response.data)
} catch (error) {
console.error('Error fetching executions:', error)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'bg-green-100 text-green-800'
case 'in_progress':
return 'bg-blue-100 text-blue-800'
case 'failed':
return 'bg-red-100 text-red-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Compliance Workflows</h1>
<p className="mt-2 text-gray-600">Manage automated compliance workflows</p>
</div>
{/* Workflows */}
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Workflow Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{workflows.map((workflow) => (
<div key={workflow.id} className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold">{workflow.name}</h3>
<span className={`px-2 py-1 text-xs rounded ${workflow.active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
{workflow.active ? 'Active' : 'Inactive'}
</span>
</div>
<p className="text-gray-600 text-sm mb-4">{workflow.description}</p>
<div className="text-sm text-gray-500">
{workflow.steps?.length || 0} steps
</div>
</div>
))}
</div>
</div>
{/* Executions */}
<div>
<h2 className="text-xl font-semibold mb-4">Workflow Executions</h2>
<div className="bg-white shadow rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Workflow</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User Address</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Step</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{executions.map((execution) => (
<tr key={execution.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm">{execution.workflowId.slice(0, 8)}...</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{execution.userAddress.slice(0, 10)}...</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{execution.currentStep}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(execution.status)}`}>
{execution.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(execution.createdAt).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,154 +0,0 @@
'use client';
import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { useState, useEffect } from 'react';
import Link from 'next/link';
export default function UserDappPage() {
const { address, isConnected } = useAccount();
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
const [portfolio, setPortfolio] = useState<any>(null);
useEffect(() => {
if (address) {
fetchPortfolio();
}
}, [address]);
const fetchPortfolio = async () => {
try {
const res = await fetch(`/api/analytics/portfolio/${address}`);
const data = await res.json();
setPortfolio(data);
} catch (error) {
console.error('Failed to fetch portfolio:', error);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">ASLE DApp</h1>
</div>
<div className="flex items-center space-x-4">
{isConnected ? (
<>
<span className="text-sm text-gray-700">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<button
onClick={() => disconnect()}
className="px-4 py-2 text-sm text-gray-700 hover:text-gray-900"
>
Disconnect
</button>
</>
) : (
<button
onClick={() => connect({ connector: connectors[0] })}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Connect Wallet
</button>
)}
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
{!isConnected ? (
<div className="text-center py-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Welcome to ASLE
</h2>
<p className="text-gray-600 mb-8">
Connect your wallet to get started
</p>
<button
onClick={() => connect({ connector: connectors[0] })}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-lg"
>
Connect Wallet
</button>
</div>
) : (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Your Portfolio
</h2>
{portfolio ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Total Value</p>
<p className="text-2xl font-bold text-gray-900">
${portfolio.totalValue || '0.00'}
</p>
</div>
<div className="bg-green-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Pool Positions</p>
<p className="text-2xl font-bold text-gray-900">
{Object.keys(portfolio.poolPositions || {}).length}
</p>
</div>
<div className="bg-purple-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Vault Positions</p>
<p className="text-2xl font-bold text-gray-900">
{Object.keys(portfolio.vaultPositions || {}).length}
</p>
</div>
</div>
) : (
<p className="text-gray-500">Loading portfolio...</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
href="/pools"
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition"
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Liquidity Pools
</h3>
<p className="text-sm text-gray-600">
Provide liquidity and earn fees
</p>
</Link>
<Link
href="/vaults"
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition"
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Vaults
</h3>
<p className="text-sm text-gray-600">
Deposit assets into yield vaults
</p>
</Link>
<Link
href="/governance"
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition"
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Governance
</h3>
<p className="text-sm text-gray-600">
Participate in DAO governance
</p>
</Link>
</div>
</div>
)}
</main>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,26 +0,0 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,123 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import { BarChart } from '@/components/charts/BarChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export default function GovernanceAnalyticsPage() {
const [metrics, setMetrics] = useState<any>(null)
const [trends, setTrends] = useState<any[]>([])
const [delegates, setDelegates] = useState<any[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
setLoading(true)
try {
const [metricsRes, trendsRes, delegatesRes] = await Promise.all([
axios.get(`${API_URL}/api/governance/analytics/metrics`),
axios.get(`${API_URL}/api/governance/analytics/trends`),
axios.get(`${API_URL}/api/governance/analytics/delegates`),
])
setMetrics(metricsRes.data)
setTrends(trendsRes.data)
setDelegates(delegatesRes.data)
} catch (error) {
console.error('Error fetching analytics:', error)
} finally {
setLoading(false)
}
}
const trendsData = trends.map((t) => ({
date: t.date,
proposalsCreated: t.proposalsCreated,
votesCast: t.votesCast,
proposalsPassed: t.proposalsPassed,
}))
const delegatesData = delegates.map((d) => ({
delegatee: d.delegatee.slice(0, 10) + '...',
totalDelegated: parseFloat(d.totalDelegated || '0'),
proposalsVoted: d.proposalsVoted,
winRate: d.winRate,
}))
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Governance Analytics</h1>
<p className="mt-2 text-gray-600">Comprehensive governance metrics and insights</p>
</div>
{loading ? (
<div className="text-center py-12">Loading analytics...</div>
) : (
<div className="space-y-6">
{/* Metrics */}
{metrics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total Proposals</h3>
<p className="text-2xl font-bold mt-2">{metrics.totalProposals || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Proposals</h3>
<p className="text-2xl font-bold mt-2">{metrics.activeProposals || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Participation Rate</h3>
<p className="text-2xl font-bold mt-2">{metrics.participationRate?.toFixed(1) || 0}%</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total Delegations</h3>
<p className="text-2xl font-bold mt-2">{metrics.totalDelegations || 0}</p>
</div>
</div>
)}
{/* Trends Chart */}
{trendsData.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<LineChart
data={trendsData}
dataKey="date"
lines={[
{ key: 'proposalsCreated', name: 'Proposals Created', color: '#3b82f6' },
{ key: 'votesCast', name: 'Votes Cast', color: '#10b981' },
{ key: 'proposalsPassed', name: 'Proposals Passed', color: '#f59e0b' },
]}
title="Governance Trends"
/>
</div>
)}
{/* Delegate Leaderboard */}
{delegatesData.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Delegate Leaderboard</h2>
<BarChart
data={delegatesData}
dataKey="delegatee"
bars={[
{ key: 'totalDelegated', name: 'Total Delegated', color: '#3b82f6' },
{ key: 'proposalsVoted', name: 'Proposals Voted', color: '#10b981' },
]}
title="Top Delegates"
/>
</div>
)}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,156 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useAccount, useWriteContract } from 'wagmi'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface Delegation {
delegator: string
delegatee: string
votingPower: string
timestamp: string
}
interface DelegateReputation {
delegatee: string
totalDelegated: string
proposalsVoted: number
proposalsWon: number
winRate: number
averageVoteWeight: string
}
export default function DelegationPage() {
const { address } = useAccount()
const [delegations, setDelegations] = useState<Delegation[]>([])
const [delegates, setDelegates] = useState<DelegateReputation[]>([])
const [delegateAddress, setDelegateAddress] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchDelegations()
fetchDelegates()
}, [])
const fetchDelegations = async () => {
try {
const response = await axios.get(`${API_URL}/api/governance/delegation`)
setDelegations(response.data)
} catch (error) {
console.error('Error fetching delegations:', error)
}
}
const fetchDelegates = async () => {
try {
const response = await axios.get(`${API_URL}/api/governance/analytics/delegates`)
setDelegates(response.data)
} catch (error) {
console.error('Error fetching delegates:', error)
}
}
const handleDelegate = async () => {
if (!address || !delegateAddress) return
// In production, this would call the GovernanceFacet.delegate() function
// For now, just show a message
alert(`Delegation would be submitted. In production, this would call the smart contract.`)
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Vote Delegation</h1>
<p className="mt-2 text-gray-600">Delegate your voting power to trusted delegates</p>
</div>
{/* Delegate Form */}
{address && (
<div className="bg-white p-6 rounded-lg shadow mb-6">
<h2 className="text-xl font-semibold mb-4">Delegate Your Votes</h2>
<div className="flex gap-4">
<input
type="text"
value={delegateAddress}
onChange={(e) => setDelegateAddress(e.target.value)}
placeholder="Enter delegate address"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md"
/>
<button
onClick={handleDelegate}
disabled={!delegateAddress}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
Delegate
</button>
</div>
</div>
)}
{/* Delegate Leaderboard */}
<div className="bg-white p-6 rounded-lg shadow mb-6">
<h2 className="text-xl font-semibold mb-4">Top Delegates</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Delegate</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Total Delegated</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Proposals Voted</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Win Rate</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{delegates.map((delegate, index) => (
<tr key={delegate.delegatee}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<span className="text-gray-400 mr-2">#{index + 1}</span>
<span className="font-medium">{delegate.delegatee.slice(0, 10)}...</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{parseFloat(delegate.totalDelegated).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{delegate.proposalsVoted}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`font-semibold ${delegate.winRate >= 50 ? 'text-green-600' : 'text-gray-600'}`}>
{delegate.winRate.toFixed(1)}%
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Recent Delegations */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Recent Delegations</h2>
<div className="space-y-2">
{delegations.slice(0, 10).map((delegation) => (
<div key={delegation.delegator} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div>
<span className="font-medium">{delegation.delegator.slice(0, 10)}...</span>
<span className="text-gray-500 mx-2"></span>
<span className="font-medium">{delegation.delegatee.slice(0, 10)}...</span>
</div>
<span className="text-sm text-gray-500">
{parseFloat(delegation.votingPower).toLocaleString()} votes
</span>
</div>
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,185 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useAccount } from 'wagmi'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import api from '@/lib/api'
import toast from 'react-hot-toast'
import Link from 'next/link'
interface Proposal {
id: number
proposalType: string
status: string
proposer: string
description: string
forVotes: string
againstVotes: string
startTime: string
endTime: string
}
export default function GovernancePage() {
const { address, isConnected } = useAccount()
const [proposals, setProposals] = useState<Proposal[]>([])
const [loading, setLoading] = useState(true)
const [treasury, setTreasury] = useState<{ ETH: string; USDC: string }>({ ETH: '0', USDC: '0' })
const [showCreateModal, setShowCreateModal] = useState(false)
useEffect(() => {
fetchProposals()
fetchTreasury()
}, [])
const fetchProposals = async () => {
try {
setLoading(true)
// In production, this would fetch from API
// For now, using mock structure
setProposals([])
} catch (error: any) {
toast.error('Failed to fetch proposals')
} finally {
setLoading(false)
}
}
const fetchTreasury = async () => {
try {
// In production, fetch from GovernanceFacet
setTreasury({ ETH: '0', USDC: '0' })
} catch (error) {
console.error('Error fetching treasury:', error)
}
}
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'active':
return 'bg-blue-100 text-blue-800'
case 'passed':
return 'bg-green-100 text-green-800'
case 'rejected':
return 'bg-red-100 text-red-800'
case 'executed':
return 'bg-purple-100 text-purple-800'
default:
return 'bg-gray-100 text-gray-800'
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Governance</h1>
{isConnected && (
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Create Proposal
</button>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Active Proposals</h2>
{loading ? (
<div className="flex justify-center py-8">
<LoadingSpinner size="lg" />
</div>
) : proposals.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">No active proposals</p>
{isConnected && (
<button
onClick={() => setShowCreateModal(true)}
className="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Create First Proposal
</button>
)}
</div>
) : (
<div className="space-y-4">
{proposals.map((proposal) => (
<Link
key={proposal.id}
href={`/governance/proposals/${proposal.id}`}
className="block p-4 border border-gray-200 rounded-lg hover:border-indigo-300 hover:shadow-md transition"
>
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-semibold">{proposal.description}</h3>
<span className={`px-2 py-1 rounded text-xs ${getStatusColor(proposal.status)}`}>
{proposal.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-3">{proposal.proposalType}</p>
<div className="flex justify-between text-sm">
<div>
<span className="text-gray-600">For: </span>
<span className="font-semibold text-green-600">{proposal.forVotes}</span>
</div>
<div>
<span className="text-gray-600">Against: </span>
<span className="font-semibold text-red-600">{proposal.againstVotes}</span>
</div>
<div className="text-gray-500">
Ends: {new Date(proposal.endTime).toLocaleDateString()}
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
<div className="space-y-6">
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Treasury</h2>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-gray-600">ETH</span>
<span className="font-semibold">{treasury.ETH}</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-gray-600">USDC</span>
<span className="font-semibold">{treasury.USDC}</span>
</div>
<button className="w-full mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Manage Treasury
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Proposal Types</h2>
<div className="space-y-2 text-sm">
<div className="p-2 bg-gray-50 rounded">
<p className="font-semibold">Parameter Change</p>
<p className="text-gray-600 text-xs">Update system parameters</p>
</div>
<div className="p-2 bg-gray-50 rounded">
<p className="font-semibold">Facet Upgrade</p>
<p className="text-gray-600 text-xs">Upgrade smart contract facets</p>
</div>
<div className="p-2 bg-gray-50 rounded">
<p className="font-semibold">Treasury Withdrawal</p>
<p className="text-gray-600 text-xs">Withdraw from treasury</p>
</div>
<div className="p-2 bg-gray-50 rounded">
<p className="font-semibold">Emergency Pause</p>
<p className="text-gray-600 text-xs">Emergency system pause</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,194 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
import { useAccount } from 'wagmi'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface SnapshotProposal {
id: string
title: string
body: string
choices: string[]
start: number
end: number
state: string
author: string
scores: number[]
scores_total: number
}
export default function SnapshotPage() {
const { address } = useAccount()
const [proposals, setProposals] = useState<SnapshotProposal[]>([])
const [selectedProposal, setSelectedProposal] = useState<SnapshotProposal | null>(null)
const [votes, setVotes] = useState<any[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchProposals()
}, [])
useEffect(() => {
if (selectedProposal) {
fetchVotes(selectedProposal.id)
}
}, [selectedProposal])
const fetchProposals = async () => {
setLoading(true)
try {
const response = await axios.get(`${API_URL}/api/governance/snapshot/proposals`)
setProposals(response.data)
} catch (error) {
console.error('Error fetching Snapshot proposals:', error)
} finally {
setLoading(false)
}
}
const fetchVotes = async (proposalId: string) => {
try {
const response = await axios.get(`${API_URL}/api/governance/snapshot/proposal/${proposalId}/votes`)
setVotes(response.data)
} catch (error) {
console.error('Error fetching votes:', error)
}
}
const handleVote = async (proposalId: string, choice: number) => {
if (!address) {
alert('Please connect your wallet')
return
}
try {
// In production, this would require wallet signature
await axios.post(`${API_URL}/api/governance/snapshot/proposal/${proposalId}/vote`, {
choice,
voter: address,
signature: 'mock-signature', // Would be actual signature in production
})
fetchVotes(proposalId)
} catch (error) {
console.error('Error voting:', error)
}
}
const handleSync = async (proposalId: string) => {
try {
await axios.post(`${API_URL}/api/governance/snapshot/sync/${proposalId}`)
alert('Proposal synced to local governance')
} catch (error) {
console.error('Error syncing proposal:', error)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Snapshot Integration</h1>
<p className="mt-2 text-gray-600">View and vote on Snapshot proposals</p>
</div>
{loading ? (
<div className="text-center py-12">Loading proposals...</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Proposals List */}
<div>
<h2 className="text-xl font-semibold mb-4">Proposals</h2>
<div className="space-y-4">
{proposals.map((proposal) => (
<div
key={proposal.id}
className={`bg-white p-6 rounded-lg shadow cursor-pointer ${
selectedProposal?.id === proposal.id ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setSelectedProposal(proposal)}
>
<h3 className="text-lg font-semibold mb-2">{proposal.title}</h3>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">{proposal.body}</p>
<div className="flex items-center justify-between">
<span className={`px-2 py-1 text-xs rounded ${
proposal.state === 'active' ? 'bg-green-100 text-green-800' :
proposal.state === 'closed' ? 'bg-gray-100 text-gray-800' :
'bg-yellow-100 text-yellow-800'
}`}>
{proposal.state}
</span>
<button
onClick={(e) => {
e.stopPropagation()
handleSync(proposal.id)
}}
className="text-sm text-blue-600 hover:text-blue-800"
>
Sync
</button>
</div>
</div>
))}
</div>
</div>
{/* Proposal Details */}
{selectedProposal && (
<div>
<h2 className="text-xl font-semibold mb-4">Proposal Details</h2>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-2xl font-bold mb-4">{selectedProposal.title}</h3>
<div className="prose mb-6" dangerouslySetInnerHTML={{ __html: selectedProposal.body }} />
<div className="mb-6">
<h4 className="font-semibold mb-2">Choices:</h4>
<div className="space-y-2">
{selectedProposal.choices.map((choice, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<span>{choice}</span>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
{selectedProposal.scores?.[index] || 0} votes
</span>
{selectedProposal.state === 'active' && (
<button
onClick={() => handleVote(selectedProposal.id, index + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Vote
</button>
)}
</div>
</div>
))}
</div>
</div>
<div className="mb-6">
<h4 className="font-semibold mb-2">Votes ({votes.length})</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{votes.map((vote) => (
<div key={vote.id} className="flex items-center justify-between p-2 bg-gray-50 rounded text-sm">
<span className="text-gray-600">{vote.voter.slice(0, 10)}...</span>
<span className="font-semibold">
{typeof vote.choice === 'number'
? selectedProposal.choices[vote.choice - 1]
: 'Multiple choices'}
</span>
<span className="text-gray-500">{vote.vp} VP</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,64 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface Template {
id: string
name: string
description: string
proposalType: string
active: boolean
}
export default function TemplatesPage() {
const [templates, setTemplates] = useState<Template[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchTemplates()
}, [])
const fetchTemplates = async () => {
try {
const response = await axios.get(`${API_URL}/api/governance/templates`)
setTemplates(response.data)
} catch (error) {
console.error('Error fetching templates:', error)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Proposal Templates</h1>
<p className="mt-2 text-gray-600">Use pre-configured templates to create proposals</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{templates.map((template) => (
<div key={template.id} className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold">{template.name}</h3>
<span className={`px-2 py-1 text-xs rounded ${template.active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
{template.active ? 'Active' : 'Inactive'}
</span>
</div>
<p className="text-gray-600 text-sm mb-4">{template.description}</p>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Type: {template.proposalType}</span>
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm">
Use Template
</button>
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -1,103 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { AreaChart } from '@/components/charts/AreaChart'
import { PieChart } from '@/components/charts/PieChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export default function TreasuryPage() {
const [treasury, setTreasury] = useState<any>(null)
const [history, setHistory] = useState<any[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchTreasury()
fetchHistory()
}, [])
const fetchTreasury = async () => {
try {
// In production, fetch from GovernanceFacet
setTreasury({
ETH: '1000000000000000000', // 1 ETH
USDC: '5000000000', // 5000 USDC
})
} catch (error) {
console.error('Error fetching treasury:', error)
}
}
const fetchHistory = async () => {
try {
// In production, fetch treasury transaction history
setHistory([])
} catch (error) {
console.error('Error fetching history:', error)
}
}
const treasuryData = treasury ? [
{ name: 'ETH', value: parseFloat(treasury.ETH || '0') / 1e18 },
{ name: 'USDC', value: parseFloat(treasury.USDC || '0') / 1e6 },
] : []
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Treasury Management</h1>
<p className="mt-2 text-gray-600">View and manage treasury balances</p>
</div>
{/* Treasury Overview */}
{treasury && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Treasury Balances</h2>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-600">ETH:</span>
<span className="font-semibold">{(parseFloat(treasury.ETH) / 1e18).toFixed(4)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">USDC:</span>
<span className="font-semibold">{(parseFloat(treasury.USDC) / 1e6).toFixed(2)}</span>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={treasuryData}
title="Treasury Allocation"
/>
</div>
</div>
)}
{/* Treasury History */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Transaction History</h2>
{history.length === 0 ? (
<div className="text-center text-gray-500 py-8">No transactions yet</div>
) : (
<div className="space-y-2">
{history.map((tx) => (
<div key={tx.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div>
<span className="font-medium">{tx.type}</span>
<span className="text-gray-500 ml-2">{tx.amount}</span>
</div>
<span className="text-sm text-gray-500">{new Date(tx.timestamp).toLocaleString()}</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -1,247 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useAccount } from 'wagmi'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import api from '@/lib/api'
import toast from 'react-hot-toast'
interface CustodialWallet {
walletId: string
address: string
provider: string
type: 'hot' | 'warm' | 'cold'
mpcEnabled: boolean
}
export default function InstitutionalPage() {
const { address, isConnected } = useAccount()
const [wallets, setWallets] = useState<CustodialWallet[]>([])
const [loading, setLoading] = useState(false)
const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedProvider, setSelectedProvider] = useState<'fireblocks' | 'coinbase' | 'bitgo'>('fireblocks')
const [selectedType, setSelectedType] = useState<'hot' | 'warm' | 'cold'>('warm')
useEffect(() => {
if (isConnected) {
fetchWallets()
}
}, [isConnected])
const fetchWallets = async () => {
try {
setLoading(true)
const response = await api.get('/custodial/wallets')
if (response.data.success) {
setWallets(response.data.wallets || [])
}
} catch (error: any) {
console.error('Error fetching wallets:', error)
} finally {
setLoading(false)
}
}
const handleCreateWallet = async () => {
try {
setLoading(true)
const response = await api.post('/custodial/wallets', {
provider: selectedProvider,
type: selectedType
})
if (response.data.success) {
toast.success('Custodial wallet created')
setShowCreateModal(false)
await fetchWallets()
}
} catch (error: any) {
toast.error(error.response?.data?.error || 'Failed to create wallet')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Institutional Portal</h1>
{isConnected && (
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Create Custodial Wallet
</button>
)}
</div>
{!isConnected && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-yellow-800">Please connect your wallet to access institutional features</p>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Custodial Wallets</h2>
{loading ? (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
) : wallets.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500 mb-4">No custodial wallets</p>
{isConnected && (
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Create Wallet
</button>
)}
</div>
) : (
<div className="space-y-3">
{wallets.map((wallet) => (
<div key={wallet.walletId} className="p-4 border border-gray-200 rounded-lg">
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-semibold">{wallet.provider}</p>
<p className="text-sm text-gray-600 font-mono">{wallet.address}</p>
</div>
<span className={`px-2 py-1 rounded text-xs ${
wallet.type === 'hot' ? 'bg-red-100 text-red-800' :
wallet.type === 'warm' ? 'bg-yellow-100 text-yellow-800' :
'bg-blue-100 text-blue-800'
}`}>
{wallet.type.toUpperCase()}
</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-600">MPC Enabled:</span>
<span className={wallet.mpcEnabled ? 'text-green-600' : 'text-gray-400'}>
{wallet.mpcEnabled ? 'Yes' : 'No'}
</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Treasury Management</h2>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 mb-1">Total Assets</p>
<p className="text-3xl font-bold">$0.00</p>
</div>
<button className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Manage Treasury
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">MPC Key Management</h2>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-gray-600">Key Shares:</span>
<span className="font-semibold">3</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-gray-600">Threshold:</span>
<span className="font-semibold">2</span>
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 rounded">
<span className="text-gray-600">Status:</span>
<span className="font-semibold text-green-600">Active</span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Bank Integrations</h2>
<div className="space-y-3">
<div className="p-4 border-2 border-green-200 rounded-lg bg-green-50">
<div className="flex justify-between items-center">
<div>
<p className="font-semibold text-green-900">SWIFT</p>
<p className="text-sm text-green-700">Secure messaging network</p>
</div>
<span className="px-3 py-1 bg-green-200 text-green-800 rounded text-sm font-semibold">
Connected
</span>
</div>
</div>
<div className="p-4 border-2 border-green-200 rounded-lg bg-green-50">
<div className="flex justify-between items-center">
<div>
<p className="font-semibold text-green-900">ISO 20022</p>
<p className="text-sm text-green-700">Modern messaging standard</p>
</div>
<span className="px-3 py-1 bg-green-200 text-green-800 rounded text-sm font-semibold">
Connected
</span>
</div>
</div>
<button className="w-full mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Configure Integrations
</button>
</div>
</div>
</div>
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<h2 className="text-xl font-semibold mb-4">Create Custodial Wallet</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Provider</label>
<select
value={selectedProvider}
onChange={(e) => setSelectedProvider(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="fireblocks">Fireblocks</option>
<option value="coinbase">Coinbase Prime</option>
<option value="bitgo">BitGo</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Wallet Type</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="hot">Hot Wallet</option>
<option value="warm">Warm Wallet</option>
<option value="cold">Cold Wallet</option>
</select>
</div>
<div className="flex space-x-3">
<button
onClick={handleCreateWallet}
disabled={loading}
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
>
{loading ? <LoadingSpinner size="sm" /> : 'Create'}
</button>
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
</div>
</div>
)
}

View File

@@ -1,32 +0,0 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { Providers } from './providers'
import { ToastNotifications } from '@/components/ToastNotifications'
import { ErrorBoundary } from '@/components/ErrorBoundary'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'ASLE - Ali & Saum Liquidity Engine',
description: 'Hybrid Cross-Chain Liquidity Infrastructure',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<ErrorBoundary>
<Providers>
{children}
<ToastNotifications />
</Providers>
</ErrorBoundary>
</body>
</html>
)
}

View File

@@ -1,289 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import api from '@/lib/api'
import toast from 'react-hot-toast'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'
interface Alert {
id: string
type: string
severity: 'low' | 'medium' | 'high' | 'critical'
title: string
message: string
timestamp: number
resolved: boolean
}
interface Metric {
name: string
value: number
timestamp: number
}
export default function MonitoringPage() {
const [health, setHealth] = useState<any>(null)
const [alerts, setAlerts] = useState<Alert[]>([])
const [metrics, setMetrics] = useState<Metric[]>([])
const [loading, setLoading] = useState(true)
const [selectedMetric, setSelectedMetric] = useState<string>('')
useEffect(() => {
fetchData()
const interval = setInterval(fetchData, 30000) // Refresh every 30 seconds
return () => clearInterval(interval)
}, [])
const fetchData = async () => {
try {
await Promise.all([
fetchHealth(),
fetchAlerts(),
fetchMetrics()
])
} catch (error) {
console.error('Error fetching monitoring data:', error)
} finally {
setLoading(false)
}
}
const fetchHealth = async () => {
try {
const response = await api.get('/monitoring/health')
if (response.data.success) {
setHealth(response.data.health)
}
} catch (error: any) {
console.error('Failed to fetch health:', error)
}
}
const fetchAlerts = async () => {
try {
const response = await api.get('/monitoring/alerts', {
params: { resolved: false }
})
if (response.data.success) {
setAlerts(response.data.alerts || [])
}
} catch (error: any) {
console.error('Failed to fetch alerts:', error)
}
}
const fetchMetrics = async () => {
try {
const timeRange = {
from: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
to: Date.now()
}
const response = await api.get('/monitoring/metrics', {
params: selectedMetric ? { name: selectedMetric, ...timeRange } : timeRange
})
if (response.data.success) {
setMetrics(response.data.metrics || [])
}
} catch (error: any) {
console.error('Failed to fetch metrics:', error)
}
}
const handleResolveAlert = async (alertId: string) => {
try {
const response = await api.post(`/monitoring/alerts/${alertId}/resolve`)
if (response.data.success) {
toast.success('Alert resolved')
await fetchAlerts()
}
} catch (error: any) {
toast.error(error.response?.data?.error || 'Failed to resolve alert')
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'healthy':
return 'text-green-600'
case 'degraded':
return 'text-yellow-600'
case 'down':
return 'text-red-600'
default:
return 'text-gray-600'
}
}
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical':
return 'border-red-500 bg-red-50'
case 'high':
return 'border-orange-500 bg-orange-50'
case 'medium':
return 'border-yellow-500 bg-yellow-50'
default:
return 'border-blue-500 bg-blue-50'
}
}
// Prepare metrics data for charts
const metricsChartData = metrics.slice(0, 20).map(m => ({
time: new Date(m.timestamp).toLocaleTimeString(),
value: m.value
}))
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">System Monitoring</h1>
<button
onClick={fetchData}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Refresh
</button>
</div>
{loading ? (
<div className="flex justify-center py-12">
<LoadingSpinner size="lg" />
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-lg font-semibold mb-2">System Status</h2>
<p className={`text-3xl font-bold ${getStatusColor(health?.status)}`}>
{health?.status?.toUpperCase() || 'UNKNOWN'}
</p>
<p className="text-sm text-gray-500 mt-1">
Last updated: {new Date().toLocaleTimeString()}
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-lg font-semibold mb-2">Active Alerts</h2>
<p className="text-3xl font-bold text-red-600">
{alerts.filter(a => !a.resolved).length}
</p>
<p className="text-sm text-gray-500 mt-1">
{alerts.filter(a => a.severity === 'critical').length} critical
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-lg font-semibold mb-2">Components</h2>
<p className="text-3xl font-bold text-green-600">
{health?.components ? Object.keys(health.components).length : 0}
</p>
<p className="text-sm text-gray-500 mt-1">
{health?.components ? Object.values(health.components).filter((c: any) => c.status === 'up').length : 0} operational
</p>
</div>
</div>
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Component Status</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{health?.components && Object.entries(health.components).map(([name, comp]: [string, any]) => (
<div key={name} className="p-4 border-2 rounded-lg text-center">
<p className="font-semibold capitalize mb-2">{name}</p>
<div className={`inline-block px-3 py-1 rounded text-sm font-semibold ${
comp.status === 'up' ? 'bg-green-100 text-green-800' :
comp.status === 'degraded' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{comp.status.toUpperCase()}
</div>
<p className="text-xs text-gray-500 mt-2">
{new Date(comp.lastCheck).toLocaleTimeString()}
</p>
</div>
))}
</div>
</div>
{metricsChartData.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Metrics</h2>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={metricsChartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="value" stroke="#4f46e5" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Recent Alerts</h2>
<select
value={selectedMetric}
onChange={(e) => {
setSelectedMetric(e.target.value)
fetchMetrics()
}}
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
>
<option value="">All Metrics</option>
<option value="transaction_count">Transactions</option>
<option value="volume">Volume</option>
<option value="errors">Errors</option>
</select>
</div>
<div className="space-y-3">
{alerts.length === 0 ? (
<p className="text-gray-500 text-center py-8">No alerts</p>
) : (
alerts.slice(0, 10).map((alert) => (
<div
key={alert.id}
className={`p-4 border-l-4 rounded ${getSeverityColor(alert.severity)}`}
>
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-semibold">{alert.title}</p>
<p className="text-sm text-gray-700 mt-1">{alert.message}</p>
</div>
<div className="flex space-x-2">
<span className={`px-2 py-1 rounded text-xs font-semibold ${
alert.severity === 'critical' ? 'bg-red-200 text-red-900' :
alert.severity === 'high' ? 'bg-orange-200 text-orange-900' :
alert.severity === 'medium' ? 'bg-yellow-200 text-yellow-900' :
'bg-blue-200 text-blue-900'
}`}>
{alert.severity}
</span>
{!alert.resolved && (
<button
onClick={() => handleResolveAlert(alert.id)}
className="px-2 py-1 bg-gray-200 text-gray-700 rounded text-xs hover:bg-gray-300"
>
Resolve
</button>
)}
</div>
</div>
<p className="text-xs text-gray-500">
{new Date(alert.timestamp).toLocaleString()}
</p>
</div>
))
)}
</div>
</div>
</>
)}
</div>
</div>
)
}

View File

@@ -1,90 +0,0 @@
'use client'
import Link from 'next/link'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { ChainSelector } from '@/components/ChainSelector'
export default function Home() {
const { address, isConnected } = useAccount()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-2xl font-bold text-indigo-600">ASLE</h1>
</div>
<div className="flex items-center space-x-4">
<ChainSelector />
{isConnected ? (
<>
<span className="text-sm text-gray-600">{address?.slice(0, 6)}...{address?.slice(-4)}</span>
<button
onClick={() => disconnect()}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Disconnect
</button>
</>
) : (
<button
onClick={() => connect({ connector: connectors[0] })}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Connect Wallet
</button>
)}
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Ali & Saum Liquidity Engine
</h2>
<p className="text-xl text-gray-600">
Hybrid Cross-Chain Liquidity Infrastructure
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<Link href="/pools" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
<h3 className="text-xl font-semibold mb-2">Liquidity Pools</h3>
<p className="text-gray-600">Create and manage PMM liquidity pools</p>
</Link>
<Link href="/vaults" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
<h3 className="text-xl font-semibold mb-2">Vaults</h3>
<p className="text-gray-600">Deposit assets into ERC-4626 or ERC-1155 vaults</p>
</Link>
<Link href="/compliance" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
<h3 className="text-xl font-semibold mb-2">Compliance</h3>
<p className="text-gray-600">Manage compliance modes and KYC/AML</p>
</Link>
<Link href="/governance" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
<h3 className="text-xl font-semibold mb-2">Governance</h3>
<p className="text-gray-600">DAO proposals and treasury management</p>
</Link>
<Link href="/institutional" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
<h3 className="text-xl font-semibold mb-2">Institutional</h3>
<p className="text-gray-600">Custodial wallets and bank integrations</p>
</Link>
<Link href="/monitoring" className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition">
<h3 className="text-xl font-semibold mb-2">Monitoring</h3>
<p className="text-gray-600">System health and alerts</p>
</Link>
</div>
{!isConnected && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-center">
<p className="text-yellow-800">Please connect your wallet to get started</p>
</div>
)}
</main>
</div>
)
}

View File

@@ -1,115 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useAccount } from 'wagmi'
import { PoolCreator } from '@/components/PoolCreator'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import api from '@/lib/api'
import Link from 'next/link'
import { formatEther } from 'viem'
interface Pool {
id: number
baseToken: string
quoteToken: string
baseReserve: string
quoteReserve: string
active: boolean
}
export default function PoolsPage() {
const { address, isConnected } = useAccount()
const [pools, setPools] = useState<Pool[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetchPools()
}, [])
const fetchPools = async () => {
try {
setLoading(true)
const response = await api.get('/pools')
setPools(response.data.pools || [])
setError(null)
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to fetch pools')
console.error('Error fetching pools:', err)
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Liquidity Pools</h1>
{isConnected && (
<Link
href="/pools/create"
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Create Pool
</Link>
)}
</div>
{!isConnected && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-yellow-800">Please connect your wallet to create pools or provide liquidity</p>
</div>
)}
{loading ? (
<div className="flex justify-center items-center py-12">
<LoadingSpinner size="lg" />
</div>
) : error ? (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">{error}</p>
</div>
) : pools.length === 0 ? (
<div className="bg-white rounded-lg shadow p-6 text-center">
<p className="text-gray-600">No pools found. Create your first pool to get started.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{pools.map((pool) => (
<Link
key={pool.id}
href={`/pools/${pool.id}`}
className="bg-white rounded-lg shadow-md hover:shadow-lg transition p-6"
>
<div className="flex justify-between items-start mb-4">
<h3 className="text-xl font-semibold">Pool #{pool.id}</h3>
{pool.active ? (
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded">Active</span>
) : (
<span className="px-2 py-1 bg-gray-100 text-gray-800 text-xs rounded">Inactive</span>
)}
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Base Reserve:</span>
<span className="font-mono">{formatEther(BigInt(pool.baseReserve || '0'))}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Quote Reserve:</span>
<span className="font-mono">{formatEther(BigInt(pool.quoteReserve || '0'))}</span>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>{pool.baseToken.slice(0, 6)}...{pool.baseToken.slice(-4)}</span>
<span>/</span>
<span>{pool.quoteToken.slice(0, 6)}...{pool.quoteToken.slice(-4)}</span>
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,18 +0,0 @@
'use client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { wagmiConfig } from '@/lib/wagmi'
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
)
}

View File

@@ -1,182 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useAccount, useWriteContract, useReadContract } from 'wagmi'
import { VAULT_FACET_ABI, DIAMOND_ADDRESS } from '@/lib/contracts'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import api from '@/lib/api'
import { formatEther, parseEther } from 'viem'
import toast from 'react-hot-toast'
import Link from 'next/link'
interface Vault {
id: number
asset: string | null
totalAssets: string
totalSupply: string
isMultiAsset: boolean
active: boolean
}
export default function VaultsPage() {
const { address, isConnected } = useAccount()
const [vaults, setVaults] = useState<Vault[]>([])
const [loading, setLoading] = useState(true)
const [selectedVaultId, setSelectedVaultId] = useState<string>('')
const [depositAmount, setDepositAmount] = useState('')
const { writeContract, isPending: isCreating } = useWriteContract()
const { writeContract: writeDeposit, isPending: isDepositing } = useWriteContract()
useEffect(() => {
fetchVaults()
}, [])
const fetchVaults = async () => {
try {
setLoading(true)
const response = await api.get('/vaults')
setVaults(response.data.vaults || [])
} catch (err: any) {
toast.error(err.response?.data?.error || 'Failed to fetch vaults')
} finally {
setLoading(false)
}
}
const handleDeposit = async () => {
if (!selectedVaultId || !depositAmount) {
toast.error('Please select a vault and enter amount')
return
}
try {
writeDeposit({
address: DIAMOND_ADDRESS as `0x${string}`,
abi: VAULT_FACET_ABI,
functionName: 'deposit',
args: [BigInt(selectedVaultId), parseEther(depositAmount), address!],
})
toast.success('Deposit transaction submitted')
setDepositAmount('')
setTimeout(fetchVaults, 3000) // Refresh after transaction
} catch (error: any) {
toast.error(error.message || 'Failed to deposit')
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Vaults</h1>
{isConnected && (
<Link
href="/vaults/create"
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Create Vault
</Link>
)}
</div>
{loading ? (
<div className="flex justify-center py-12">
<LoadingSpinner size="lg" />
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{vaults.map((vault) => (
<div
key={vault.id}
className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition"
>
<div className="flex justify-between items-start mb-4">
<h3 className="text-xl font-semibold">Vault #{vault.id}</h3>
{vault.active ? (
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded">Active</span>
) : (
<span className="px-2 py-1 bg-gray-100 text-gray-800 text-xs rounded">Inactive</span>
)}
</div>
<div className="space-y-2 text-sm mb-4">
<div className="flex justify-between">
<span className="text-gray-600">Type:</span>
<span>{vault.isMultiAsset ? 'Multi-Asset (ERC-1155)' : 'ERC-4626'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total Assets:</span>
<span className="font-mono">{formatEther(BigInt(vault.totalAssets || '0'))}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total Supply:</span>
<span className="font-mono">{formatEther(BigInt(vault.totalSupply || '0'))}</span>
</div>
{vault.asset && (
<div className="text-xs text-gray-500 truncate">
Asset: {vault.asset}
</div>
)}
</div>
<Link
href={`/vaults/${vault.id}`}
className="block text-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
View Details
</Link>
</div>
))}
</div>
</div>
{isConnected && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Deposit to Vault</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Select Vault
</label>
<select
value={selectedVaultId}
onChange={(e) => setSelectedVaultId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="">Select a vault</option>
{vaults.filter(v => v.active).map((vault) => (
<option key={vault.id} value={vault.id}>
Vault #{vault.id} - {vault.isMultiAsset ? 'Multi-Asset' : 'ERC-4626'}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Amount
</label>
<input
type="text"
value={depositAmount}
onChange={(e) => setDepositAmount(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
</div>
<button
onClick={handleDeposit}
disabled={isDepositing || !selectedVaultId || !depositAmount}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
>
{isDepositing ? 'Depositing...' : 'Deposit'}
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,160 +0,0 @@
'use client';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useAccount, useConnect, useDisconnect } from 'wagmi';
export default function WhiteLabelDappPage() {
const params = useParams();
const domain = params.domain as string;
const { address, isConnected } = useAccount();
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
const [config, setConfig] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchConfig();
}, [domain]);
const fetchConfig = async () => {
try {
// In production, this would fetch from the white-label API
// For now, we'll use a mock or fetch from admin API
const res = await fetch(`/api/white-label/${domain}`);
if (res.ok) {
const data = await res.json();
setConfig(data);
}
} catch (error) {
console.error('Failed to fetch config:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
const primaryColor = config?.primaryColor || '#3B82F6';
const secondaryColor = config?.secondaryColor || '#8B5CF6';
const logoUrl = config?.logoUrl;
return (
<div
className="min-h-screen"
style={{
background: `linear-gradient(to bottom right, ${primaryColor}15, ${secondaryColor}15)`,
}}
>
<nav
className="shadow-sm"
style={{ backgroundColor: primaryColor }}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
{logoUrl ? (
<img src={logoUrl} alt="Logo" className="h-8" />
) : (
<h1 className="text-xl font-bold text-white">{config?.name || 'ASLE'}</h1>
)}
</div>
<div className="flex items-center space-x-4">
{isConnected ? (
<>
<span className="text-sm text-white">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<button
onClick={() => disconnect()}
className="px-4 py-2 text-sm text-white hover:bg-white/20 rounded-md"
>
Disconnect
</button>
</>
) : (
<button
onClick={() => connect({ connector: connectors[0] })}
className="px-4 py-2 bg-white text-gray-900 rounded-md hover:bg-gray-100"
style={{ color: primaryColor }}
>
Connect Wallet
</button>
)}
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div className="text-center py-12">
<h2
className="text-4xl font-bold mb-4"
style={{ color: primaryColor }}
>
Welcome to {config?.name || 'ASLE'}
</h2>
<p className="text-gray-600 mb-8 text-lg">
{config?.description || 'DeFi liquidity platform'}
</p>
{!isConnected && (
<button
onClick={() => connect({ connector: connectors[0] })}
className="px-8 py-4 text-white rounded-lg text-lg font-semibold hover:opacity-90 transition"
style={{ backgroundColor: primaryColor }}
>
Connect Wallet to Get Started
</button>
)}
</div>
{isConnected && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
<div
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition"
style={{ borderTop: `4px solid ${primaryColor}` }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Liquidity Pools
</h3>
<p className="text-sm text-gray-600">
Provide liquidity and earn fees
</p>
</div>
<div
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition"
style={{ borderTop: `4px solid ${secondaryColor}` }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Vaults
</h3>
<p className="text-sm text-gray-600">
Deposit assets into yield vaults
</p>
</div>
<div
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition"
style={{ borderTop: `4px solid ${primaryColor}` }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Governance
</h3>
<p className="text-sm text-gray-600">
Participate in DAO governance
</p>
</div>
</div>
)}
</main>
</div>
);
}

View File

@@ -1,37 +0,0 @@
'use client'
import { useChainId, useSwitchChain } from 'wagmi'
import { mainnet, polygon, arbitrum, optimism, sepolia, bsc, avalanche, base } from 'wagmi/chains'
const supportedChains = [
{ id: mainnet.id, name: 'Ethereum', icon: '⟠', status: 'online' },
{ id: polygon.id, name: 'Polygon', icon: '⬟', status: 'online' },
{ id: arbitrum.id, name: 'Arbitrum', icon: '🔷', status: 'online' },
{ id: optimism.id, name: 'Optimism', icon: '🔴', status: 'online' },
{ id: bsc.id, name: 'BSC', icon: '🟡', status: 'online' },
{ id: avalanche.id, name: 'Avalanche', icon: '🔺', status: 'online' },
{ id: base.id, name: 'Base', icon: '🔵', status: 'online' },
{ id: sepolia.id, name: 'Sepolia', icon: '🧪', status: 'online' },
]
export function ChainSelector() {
const chainId = useChainId()
const { switchChain } = useSwitchChain()
return (
<div className="flex items-center space-x-2">
<select
value={chainId}
onChange={(e) => switchChain({ chainId: Number(e.target.value) })}
className="px-3 py-2 border border-gray-300 rounded-md bg-white"
>
{supportedChains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.icon} {chain.name} {chain.status === 'online' ? '●' : '○'}
</option>
))}
</select>
</div>
)
}

View File

@@ -1,99 +0,0 @@
'use client'
import { useState } from 'react'
import { useAccount, useWriteContract } from 'wagmi'
import { DIAMOND_ADDRESS, DIAMOND_ABI } from '@/lib/contracts'
import toast from 'react-hot-toast'
import { LoadingSpinner } from './LoadingSpinner'
export function ComplianceSelector() {
const { address } = useAccount()
const [selectedMode, setSelectedMode] = useState<'Regulated' | 'Fintech' | 'Decentralized'>('Decentralized')
const { writeContract, isPending } = useWriteContract()
const handleSetMode = async () => {
if (!address) {
toast.error('Please connect your wallet')
return
}
try {
// Mode values: 0 = Regulated, 1 = Fintech, 2 = Decentralized
const modeValue = selectedMode === 'Regulated' ? 0 : selectedMode === 'Fintech' ? 1 : 2
// In production, this would call ComplianceFacet.setUserComplianceMode
// For now, showing the structure
toast.success(`Compliance mode set to ${selectedMode}`)
} catch (error: any) {
toast.error(error.message || 'Failed to set compliance mode')
}
}
return (
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Compliance Mode</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Compliance Mode
</label>
<select
value={selectedMode}
onChange={(e) => setSelectedMode(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500"
>
<option value="Decentralized">Decentralized (Mode C)</option>
<option value="Fintech">Enterprise Fintech (Mode B)</option>
<option value="Regulated">Regulated Financial Institution (Mode A)</option>
</select>
</div>
<div className="bg-gray-50 p-4 rounded-md">
<h3 className="font-semibold mb-2">Mode Details:</h3>
{selectedMode === 'Decentralized' && (
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600">
<li>Non-custodial key management</li>
<li>Zero-knowledge identity support</li>
<li>Permissionless access</li>
<li>Minimal data retention</li>
</ul>
)}
{selectedMode === 'Fintech' && (
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600">
<li>Tiered KYC requirements</li>
<li>Risk-based monitoring</li>
<li>API governance</li>
<li>Activity scoring</li>
</ul>
)}
{selectedMode === 'Regulated' && (
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600">
<li>Full KYC/AML screening</li>
<li>ISO 20022 financial messaging</li>
<li>FATF Travel Rule compliance</li>
<li>Comprehensive audit trails</li>
</ul>
)}
</div>
{address && (
<button
onClick={handleSetMode}
disabled={isPending}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center justify-center"
>
{isPending ? (
<>
<LoadingSpinner size="sm" />
<span className="ml-2">Setting...</span>
</>
) : (
'Set Compliance Mode'
)}
</button>
)}
{!address && (
<p className="text-sm text-gray-500 text-center">Connect wallet to set compliance mode</p>
)}
</div>
</div>
)
}

View File

@@ -1,50 +0,0 @@
'use client'
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
<h2 className="text-xl font-bold text-red-600 mb-4">Something went wrong</h2>
<p className="text-gray-600 mb-4">{this.state.error?.message || 'An unexpected error occurred'}</p>
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Try again
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -1,12 +0,0 @@
export function LoadingSpinner({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-8 h-8',
lg: 'w-12 h-12'
};
return (
<div className={`${sizeClasses[size]} border-4 border-gray-200 border-t-indigo-600 rounded-full animate-spin`} />
);
}

View File

@@ -1,197 +0,0 @@
'use client'
import { useState } from 'react'
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { LIQUIDITY_FACET_ABI, DIAMOND_ADDRESS } from '@/lib/contracts'
import { parseEther } from 'viem'
import toast from 'react-hot-toast'
import { LoadingSpinner } from './LoadingSpinner'
export function PoolCreator() {
const [baseToken, setBaseToken] = useState('')
const [quoteToken, setQuoteToken] = useState('')
const [initialBaseReserve, setInitialBaseReserve] = useState('')
const [initialQuoteReserve, setInitialQuoteReserve] = useState('')
const [virtualBaseReserve, setVirtualBaseReserve] = useState('')
const [virtualQuoteReserve, setVirtualQuoteReserve] = useState('')
const [k, setK] = useState('0.1')
const [oraclePrice, setOraclePrice] = useState('1')
const { writeContract, data: hash, isPending, error } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
const handleCreatePool = async () => {
if (!baseToken || !quoteToken) {
toast.error('Please enter token addresses')
return
}
if (!/^0x[a-fA-F0-9]{40}$/.test(baseToken) || !/^0x[a-fA-F0-9]{40}$/.test(quoteToken)) {
toast.error('Invalid token address format')
return
}
try {
writeContract({
address: DIAMOND_ADDRESS as `0x${string}`,
abi: LIQUIDITY_FACET_ABI,
functionName: 'createPool',
args: [
baseToken as `0x${string}`,
quoteToken as `0x${string}`,
parseEther(initialBaseReserve || '0'),
parseEther(initialQuoteReserve || '0'),
parseEther(virtualBaseReserve || '0'),
parseEther(virtualQuoteReserve || '0'),
BigInt(Math.floor(parseFloat(k) * 1e18)),
parseEther(oraclePrice),
'0x0000000000000000000000000000000000000000' as `0x${string}` // Oracle address (optional)
],
})
toast.success('Pool creation transaction submitted')
} catch (error: any) {
toast.error(error.message || 'Error creating pool')
console.error('Error creating pool:', error)
}
}
if (isSuccess) {
toast.success('Pool created successfully!')
}
if (error) {
toast.error(`Transaction failed: ${error.message}`)
}
return (
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4">Create Liquidity Pool</h2>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Base Token Address *
</label>
<input
type="text"
value={baseToken}
onChange={(e) => setBaseToken(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500"
placeholder="0x..."
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Quote Token Address *
</label>
<input
type="text"
value={quoteToken}
onChange={(e) => setQuoteToken(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500"
placeholder="0x..."
required
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Initial Base Reserve
</label>
<input
type="text"
value={initialBaseReserve}
onChange={(e) => setInitialBaseReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Initial Quote Reserve
</label>
<input
type="text"
value={initialQuoteReserve}
onChange={(e) => setInitialQuoteReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Virtual Base Reserve
</label>
<input
type="text"
value={virtualBaseReserve}
onChange={(e) => setVirtualBaseReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
<p className="text-xs text-gray-500 mt-1">Virtual liquidity for better pricing</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Virtual Quote Reserve
</label>
<input
type="text"
value={virtualQuoteReserve}
onChange={(e) => setVirtualQuoteReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
<p className="text-xs text-gray-500 mt-1">Virtual liquidity for better pricing</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Slippage Coefficient (k)
</label>
<input
type="text"
value={k}
onChange={(e) => setK(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.1"
/>
<p className="text-xs text-gray-500 mt-1">Range: 0-1 (lower = less slippage)</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Oracle Price
</label>
<input
type="text"
value={oraclePrice}
onChange={(e) => setOraclePrice(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="1.0"
/>
<p className="text-xs text-gray-500 mt-1">Reference price from oracle</p>
</div>
</div>
<button
onClick={handleCreatePool}
disabled={isPending || isConfirming || !baseToken || !quoteToken}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{isPending || isConfirming ? (
<>
<LoadingSpinner size="sm" />
<span className="ml-2">Creating...</span>
</>
) : (
'Create Pool'
)}
</button>
</div>
</div>
)
}

View File

@@ -1,33 +0,0 @@
'use client'
import { Toaster } from 'react-hot-toast';
export function ToastNotifications() {
return (
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
success: {
duration: 3000,
iconTheme: {
primary: '#10b981',
secondary: '#fff',
},
},
error: {
duration: 5000,
iconTheme: {
primary: '#ef4444',
secondary: '#fff',
},
},
}}
/>
);
}

View File

@@ -1,101 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export function HistoricalCharts() {
const [poolId, setPoolId] = useState<string>('')
const [period, setPeriod] = useState<'day' | 'week' | 'month'>('day')
const [data, setData] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const fetchHistoricalData = async () => {
if (!poolId) return
setLoading(true)
try {
const endDate = new Date()
const startDate = new Date()
switch (period) {
case 'day':
startDate.setDate(startDate.getDate() - 7)
break
case 'week':
startDate.setDate(startDate.getDate() - 30)
break
case 'month':
startDate.setDate(startDate.getDate() - 90)
break
}
const response = await axios.get(`${API_URL}/api/analytics/historical`, {
params: {
poolId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
period,
},
})
setData(response.data)
} catch (error) {
console.error('Error fetching historical data:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (poolId) {
fetchHistoricalData()
}
}, [poolId, period])
const chartData = data.map((d) => ({
date: typeof d.timestamp === 'string' ? new Date(d.timestamp).toLocaleDateString() : d.timestamp,
value: typeof d.value === 'string' ? parseFloat(d.value) : d.value,
}))
return (
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex gap-4 mb-4">
<input
type="text"
value={poolId}
onChange={(e) => setPoolId(e.target.value)}
placeholder="Enter Pool ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md"
/>
<select
value={period}
onChange={(e) => setPeriod(e.target.value as any)}
className="px-4 py-2 border border-gray-300 rounded-md"
>
<option value="day">Daily</option>
<option value="week">Weekly</option>
<option value="month">Monthly</option>
</select>
<button
onClick={fetchHistoricalData}
disabled={loading || !poolId}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Load'}
</button>
</div>
{chartData.length > 0 && (
<LineChart
data={chartData}
dataKey="date"
lines={[{ key: 'value', name: 'TVL', color: '#3b82f6' }]}
title="Historical TVL"
/>
)}
</div>
</div>
)
}

View File

@@ -1,73 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export function PerformanceMetrics() {
const [metrics, setMetrics] = useState<any>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchMetrics()
const interval = setInterval(fetchMetrics, 60000) // Refresh every minute
return () => clearInterval(interval)
}, [])
const fetchMetrics = async () => {
try {
const response = await axios.get(`${API_URL}/api/analytics/metrics`)
setMetrics(response.data)
} catch (error) {
console.error('Error fetching performance metrics:', error)
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center">
Loading performance metrics...
</div>
)
}
if (!metrics) {
return null
}
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total TVL</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(metrics.totalTVL || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Volume</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(metrics.totalVolume24h || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Fees</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(metrics.totalFees24h || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Pools</h3>
<p className="text-2xl font-bold mt-2">{metrics.activePools || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Users</h3>
<p className="text-2xl font-bold mt-2">{metrics.activeUsers || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Transactions</h3>
<p className="text-2xl font-bold mt-2">{metrics.transactionCount24h || 0}</p>
</div>
</div>
</div>
)
}

View File

@@ -1,151 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import { BarChart } from '@/components/charts/BarChart'
import { PieChart } from '@/components/charts/PieChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export function PoolAnalytics() {
const [poolId, setPoolId] = useState<string>('')
const [analytics, setAnalytics] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [systemMetrics, setSystemMetrics] = useState<any>(null)
useEffect(() => {
fetchSystemMetrics()
}, [])
const fetchSystemMetrics = async () => {
try {
const response = await axios.get(`${API_URL}/api/analytics/metrics`)
setSystemMetrics(response.data)
} catch (error) {
console.error('Error fetching system metrics:', error)
}
}
const fetchPoolAnalytics = async () => {
if (!poolId) return
setLoading(true)
try {
const response = await axios.get(`${API_URL}/api/analytics/pools`, {
params: { poolId },
})
setAnalytics(response.data)
} catch (error) {
console.error('Error fetching pool analytics:', error)
} finally {
setLoading(false)
}
}
const chartData = analytics.map((a) => ({
date: new Date(a.timestamp).toLocaleDateString(),
tvl: parseFloat(a.tvl || '0'),
volume24h: parseFloat(a.volume24h || '0'),
fees24h: parseFloat(a.fees24h || '0'),
}))
return (
<div className="space-y-6">
{/* System Metrics */}
{systemMetrics && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total TVL</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(systemMetrics.totalTVL || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Volume</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(systemMetrics.totalVolume24h || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Pools</h3>
<p className="text-2xl font-bold mt-2">{systemMetrics.activePools || 0}</p>
</div>
</div>
)}
{/* Pool Selector */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex gap-4">
<input
type="text"
value={poolId}
onChange={(e) => setPoolId(e.target.value)}
placeholder="Enter Pool ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md"
/>
<button
onClick={fetchPoolAnalytics}
disabled={loading || !poolId}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Load Analytics'}
</button>
</div>
</div>
{/* Charts */}
{analytics.length > 0 && (
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<LineChart
data={chartData}
dataKey="date"
lines={[
{ key: 'tvl', name: 'TVL', color: '#3b82f6' },
{ key: 'volume24h', name: '24h Volume', color: '#10b981' },
]}
title="TVL and Volume Trends"
/>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<BarChart
data={chartData}
dataKey="date"
bars={[{ key: 'fees24h', name: '24h Fees', color: '#f59e0b' }]}
title="Fee Revenue"
/>
</div>
{analytics[0] && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={[
{ name: 'Base Reserve', value: parseFloat(analytics[0].tvl || '0') * 0.5 },
{ name: 'Quote Reserve', value: parseFloat(analytics[0].tvl || '0') * 0.5 },
]}
title="Reserve Distribution"
/>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Pool Metrics</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">Utilization Rate:</span>
<span className="font-semibold">{(analytics[0].utilizationRate * 100).toFixed(2)}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">7d Volume:</span>
<span className="font-semibold">{parseFloat(analytics[0].volume7d || '0').toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">30d Volume:</span>
<span className="font-semibold">{parseFloat(analytics[0].volume30d || '0').toLocaleString()}</span>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -1,147 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { AreaChart } from '@/components/charts/AreaChart'
import { PieChart } from '@/components/charts/PieChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface PortfolioTrackerProps {
userAddress: string
}
export function PortfolioTracker({ userAddress }: PortfolioTrackerProps) {
const [portfolio, setPortfolio] = useState<any>(null)
const [history, setHistory] = useState<any[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (userAddress) {
fetchPortfolio()
fetchHistory()
}
}, [userAddress])
const fetchPortfolio = async () => {
if (!userAddress) return
setLoading(true)
try {
const response = await axios.get(`${API_URL}/api/analytics/portfolio/${userAddress}`)
setPortfolio(response.data)
} catch (error) {
console.error('Error fetching portfolio:', error)
} finally {
setLoading(false)
}
}
const fetchHistory = async () => {
if (!userAddress) return
try {
const response = await axios.get(`${API_URL}/api/analytics/portfolio/${userAddress}`, {
params: {
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
},
})
setHistory(Array.isArray(response.data) ? response.data : [response.data])
} catch (error) {
console.error('Error fetching portfolio history:', error)
}
}
if (!userAddress) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center text-gray-500">
Please connect your wallet to view your portfolio
</div>
)
}
if (loading) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center">
Loading portfolio...
</div>
)
}
if (!portfolio) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center text-gray-500">
No portfolio data available
</div>
)
}
const historyData = history.map((h) => ({
date: new Date(h.timestamp).toLocaleDateString(),
value: parseFloat(h.totalValue || '0'),
}))
const poolPositions = Object.values(portfolio.poolPositions || {}).map((pos: any) => ({
name: `Pool ${pos.poolId}`,
value: parseFloat(pos.value || '0'),
}))
const vaultPositions = Object.values(portfolio.vaultPositions || {}).map((vault: any) => ({
name: `Vault ${vault.vaultId}`,
value: parseFloat(vault.value || '0'),
}))
return (
<div className="space-y-6">
{/* Portfolio Summary */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-2xl font-bold mb-4">Portfolio Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Total Value</h3>
<p className="text-3xl font-bold mt-2">{parseFloat(portfolio.totalValue || '0').toLocaleString()}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Pool Positions</h3>
<p className="text-3xl font-bold mt-2">{Object.keys(portfolio.poolPositions || {}).length}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Vault Positions</h3>
<p className="text-3xl font-bold mt-2">{Object.keys(portfolio.vaultPositions || {}).length}</p>
</div>
</div>
</div>
{/* Portfolio Value History */}
{historyData.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<AreaChart
data={historyData}
dataKey="date"
areas={[{ key: 'value', name: 'Portfolio Value', color: '#3b82f6' }]}
title="Portfolio Value Over Time"
/>
</div>
)}
{/* Asset Allocation */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{poolPositions.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={poolPositions}
title="Pool Positions"
/>
</div>
)}
{vaultPositions.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={vaultPositions}
title="Vault Positions"
/>
</div>
)}
</div>
</div>
)
}

View File

@@ -1,55 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useRealtimeData } from '@/hooks/useRealtimeData'
import { LineChart } from '@/components/charts/LineChart'
export function RealTimeMetrics() {
const { data, connected } = useRealtimeData('metrics')
const [metrics, setMetrics] = useState<any[]>([])
useEffect(() => {
if (data) {
setMetrics((prev) => [...prev.slice(-50), data].slice(-50))
}
}, [data])
const chartData = metrics.map((m, index) => ({
time: index.toString(),
tvl: parseFloat(m?.totalTVL || '0'),
volume: parseFloat(m?.totalVolume24h || '0'),
}))
return (
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold">Real-Time Metrics</h2>
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm text-gray-600">{connected ? 'Connected' : 'Disconnected'}</span>
</div>
</div>
{chartData.length > 0 && (
<LineChart
data={chartData}
dataKey="time"
lines={[
{ key: 'tvl', name: 'TVL', color: '#3b82f6' },
{ key: 'volume', name: 'Volume', color: '#10b981' },
]}
title="Real-Time Metrics"
/>
)}
{metrics.length === 0 && (
<div className="text-center text-gray-500 py-8">
Waiting for real-time data...
</div>
)}
</div>
</div>
)
}

View File

@@ -1,47 +0,0 @@
'use client'
import { AreaChart as RechartsAreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface AreaChartProps {
data: Array<Record<string, any>>
dataKey: string
areas: Array<{ key: string; name: string; color?: string }>
title?: string
height?: number
}
export function AreaChart({ data, dataKey, areas, title, height = 300 }: AreaChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsAreaChart data={data}>
<defs>
{areas.map((area) => (
<linearGradient key={area.key} id={`color${area.key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={area.color || '#3b82f6'} stopOpacity={0.8} />
<stop offset="95%" stopColor={area.color || '#3b82f6'} stopOpacity={0} />
</linearGradient>
))}
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey={dataKey} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip />
<Legend />
{areas.map((area) => (
<Area
key={area.key}
type="monotone"
dataKey={area.key}
name={area.name}
stroke={area.color || '#3b82f6'}
fill={`url(#color${area.key})`}
/>
))}
</RechartsAreaChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -1,37 +0,0 @@
'use client'
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface BarChartProps {
data: Array<Record<string, any>>
dataKey: string
bars: Array<{ key: string; name: string; color?: string }>
title?: string
height?: number
}
export function BarChart({ data, dataKey, bars, title, height = 300 }: BarChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsBarChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey={dataKey} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip />
<Legend />
{bars.map((bar) => (
<Bar
key={bar.key}
dataKey={bar.key}
name={bar.name}
fill={bar.color || '#3b82f6'}
/>
))}
</RechartsBarChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -1,24 +0,0 @@
'use client'
interface ChartTooltipProps {
active?: boolean
payload?: any[]
label?: string
}
export function ChartTooltip({ active, payload, label }: ChartTooltipProps) {
if (active && payload && payload.length) {
return (
<div className="bg-white p-3 border border-gray-200 rounded-lg shadow-lg">
<p className="font-semibold mb-2">{label}</p>
{payload.map((entry, index) => (
<p key={index} style={{ color: entry.color }} className="text-sm">
{entry.name}: {typeof entry.value === 'number' ? entry.value.toLocaleString() : entry.value}
</p>
))}
</div>
)
}
return null
}

View File

@@ -1,39 +0,0 @@
'use client'
import { LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface LineChartProps {
data: Array<Record<string, any>>
dataKey: string
lines: Array<{ key: string; name: string; color?: string }>
title?: string
height?: number
}
export function LineChart({ data, dataKey, lines, title, height = 300 }: LineChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsLineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey={dataKey} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip />
<Legend />
{lines.map((line) => (
<Line
key={line.key}
type="monotone"
dataKey={line.key}
name={line.name}
stroke={line.color || '#3b82f6'}
strokeWidth={2}
/>
))}
</RechartsLineChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -1,41 +0,0 @@
'use client'
import { PieChart as RechartsPieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface PieChartProps {
data: Array<{ name: string; value: number }>
title?: string
height?: number
colors?: string[]
}
const DEFAULT_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']
export function PieChart({ data, title, height = 300, colors = DEFAULT_COLORS }: PieChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsPieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</RechartsPieChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -1,152 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
import { useAccount } from 'wagmi'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface Comment {
id: string
author: string
content: string
parentId?: string
upvotes: number
downvotes: number
createdAt: string
}
interface ProposalDiscussionProps {
proposalId: string
}
export function ProposalDiscussion({ proposalId }: ProposalDiscussionProps) {
const { address } = useAccount()
const [comments, setComments] = useState<Comment[]>([])
const [newComment, setNewComment] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchComments()
}, [proposalId])
const fetchComments = async () => {
try {
const response = await axios.get(`${API_URL}/api/governance/discussion/${proposalId}`)
setComments(response.data.comments || [])
} catch (error) {
console.error('Error fetching comments:', error)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!newComment.trim() || !address) return
setLoading(true)
try {
await axios.post(`${API_URL}/api/governance/discussion/${proposalId}/comment`, {
author: address,
content: newComment,
})
setNewComment('')
fetchComments()
} catch (error) {
console.error('Error adding comment:', error)
} finally {
setLoading(false)
}
}
const handleVote = async (commentId: string, upvote: boolean) => {
if (!address) return
try {
await axios.post(`${API_URL}/api/governance/discussion/comment/${commentId}/vote`, {
voter: address,
upvote,
})
fetchComments()
} catch (error) {
console.error('Error voting on comment:', error)
}
}
const topLevelComments = comments.filter((c) => !c.parentId)
const replies = (parentId: string) => comments.filter((c) => c.parentId === parentId)
return (
<div className="space-y-6">
<h3 className="text-xl font-semibold">Discussion ({comments.length})</h3>
{/* Comment Form */}
{address && (
<form onSubmit={handleSubmit} className="bg-white p-4 rounded-lg shadow">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Add a comment..."
className="w-full px-4 py-2 border border-gray-300 rounded-md mb-4"
rows={3}
/>
<button
type="submit"
disabled={loading || !newComment.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Posting...' : 'Post Comment'}
</button>
</form>
)}
{/* Comments List */}
<div className="space-y-4">
{topLevelComments.map((comment) => (
<div key={comment.id} className="bg-white p-4 rounded-lg shadow">
<div className="flex items-start justify-between mb-2">
<div>
<span className="font-semibold">{comment.author.slice(0, 10)}...</span>
<span className="text-sm text-gray-500 ml-2">
{new Date(comment.createdAt).toLocaleString()}
</span>
</div>
</div>
<p className="text-gray-700 mb-3">{comment.content}</p>
<div className="flex items-center gap-4">
<button
onClick={() => handleVote(comment.id, true)}
className="flex items-center gap-1 text-gray-600 hover:text-blue-600"
>
{comment.upvotes}
</button>
<button
onClick={() => handleVote(comment.id, false)}
className="flex items-center gap-1 text-gray-600 hover:text-red-600"
>
{comment.downvotes}
</button>
</div>
{/* Replies */}
{replies(comment.id).length > 0 && (
<div className="mt-4 ml-8 space-y-2 border-l-2 border-gray-200 pl-4">
{replies(comment.id).map((reply) => (
<div key={reply.id} className="bg-gray-50 p-3 rounded">
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-sm">{reply.author.slice(0, 10)}...</span>
<span className="text-xs text-gray-500">
{new Date(reply.createdAt).toLocaleString()}
</span>
</div>
<p className="text-sm text-gray-700">{reply.content}</p>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More