Convert contracts and frontend to git submodules
This commit is contained in:
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal 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
1
contracts
Submodule
Submodule contracts added at 1a79ea1697
16
contracts/.gitignore
vendored
16
contracts/.gitignore
vendored
@@ -1,16 +0,0 @@
|
||||
# Foundry
|
||||
out/
|
||||
cache_forge/
|
||||
broadcast/
|
||||
lib/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
6
contracts/.gitmodules
vendored
6
contracts/.gitmodules
vendored
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"lib/forge-std": {
|
||||
"tag": {
|
||||
"name": "v1.12.0",
|
||||
"rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6"
|
||||
}
|
||||
},
|
||||
"lib/openzeppelin-contracts": {
|
||||
"tag": {
|
||||
"name": "v5.5.0",
|
||||
"rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}" }
|
||||
|
||||
5092
contracts/package-lock.json
generated
5092
contracts/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1
frontend
Submodule
Submodule frontend added at 3c1843a22b
41
frontend/.gitignore
vendored
41
frontend/.gitignore
vendored
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user