diff --git a/USDT_MULTISIG_SETUP.md b/USDT_MULTISIG_SETUP.md new file mode 100644 index 00000000..13e938b6 --- /dev/null +++ b/USDT_MULTISIG_SETUP.md @@ -0,0 +1,271 @@ +# USDT Treasury Multisig Yapısı + +## Genel Bakış + +USDT Treasury, kullanıcıların gerçek USDT'yi chain'de wUSDT olarak kullanabilmesi için 1:1 backing ile çalışan merkezi bir hazinedir. Güvenlik ve şeffaflık için 3/5 multisig yapısı kullanılır. + +--- + +## 🏛️ Multisig Üyeleri (3/5 Threshold) + +| # | Rol | Tiki | Unique | AccountId | Sorumluluk | +|---|-----|------|--------|-----------|------------| +| 1️⃣ | **Founder/Başkan** | Serok | ✅ | `5Grw...` | Stratejik liderlik, son karar mercii | +| 2️⃣ | **Meclis Başkanı** | SerokiMeclise | ✅ | `5FHn...` | Yasama kontrolü, demokratik denetim | +| 3️⃣ | **Hazine Müdürü** | Xezinedar | ✅ | `5FLS...` | Treasury yönetimi, reserve management | +| 4️⃣ | **Noter** | Noter | ❌ | `5DAA...` | Hukuki belgelendirme, işlem kayıtları | +| 5️⃣ | **Sözcü/Temsilci** | Berdevk | ❌ | `5HGj...` | İletişim, şeffaflık, topluluk bilgilendirme | + +### Açıklama: +- **Unique Roller (3):** Chain'de sadece 1 kişi bu role sahip olabilir (blockchain garantili) +- **Non-Unique Roller (2):** Spesifik, güvenilir kişiler seçilmiştir +- **Threshold:** Her işlem için 5 kişiden 3'ünün onayı gereklidir + +--- + +## 🔐 Güvenlik Katmanları + +### 1. Multi-Signature (3/5) +- Tek kişi fonları kontrol edemez +- Minimum 3 kişinin onayı gereklidir +- Gnosis Safe üzerinde şeffaf + +### 2. Tiered Withdrawal Limits +| Miktar | Bekleme Süresi | Gerekli İmza | Not | +|--------|----------------|--------------|-----| +| < $1,000 | Anında | 3/5 | Küçük işlemler | +| $1,000 - $10,000 | 1 saat | 3/5 | Orta işlemler | +| > $10,000 | 24 saat | 3/5 | Büyük işlemler - community alert | + +### 3. On-Chain Proof of Reserves +- Her saat otomatik kontrol +- Total wUSDT supply = Ethereum'daki USDT balance +- Public dashboard: `https://pezkuwi.com/reserves` + +### 4. Insurance Fund +- Swap fee'lerden %10 ayrılır +- Hedef: Total supply'ın %20'si +- Hack/loss durumunda kullanıcıları korur + +--- + +## 📊 İşlem Akışı + +### Deposit (USDT → wUSDT) +``` +1. Kullanıcı Ethereum'da USDT gönderir + → Multisig Address: 0x123... + +2. Noter işlemi kaydeder + → Transaction hash, miktar, user address + +3. Berdevk public dashboard'u günceller + → Şeffaflık için + +4. 3/5 multisig onayı ile wUSDT mint edilir + → User Pezkuwi chain'de wUSDT alır + +5. Reserves kontrol edilir + → wUSDT supply ≤ USDT balance +``` + +### Withdrawal (wUSDT → USDT) +``` +1. Kullanıcı chain'de wUSDT burn eder + → Ethereum address belirtir + +2. 24 saat bekleme (>$10K için) + → Güvenlik önlemi + +3. Noter withdrawal request kaydeder + → Blockchain'de doğrulanabilir + +4. 3/5 multisig onayı ile USDT gönderilir + → Ethereum'da kullanıcıya + +5. Berdevk işlemi duyurur + → Public dashboard + Twitter/Discord +``` + +--- + +## 🛡️ Güven Mekanizmaları + +### 1. Blockchain Garantili +- İlk 3 kişi **unique roller** → Sadece 1 kişi olabilir +- Chain'de doğrulanabilir +- Değiştirilemez (governance gerekir) + +### 2. Hukuki Belgelendirme (Noter) +- Tüm işlemler kayıt altında +- Denetim için trace edilebilir +- Anlaşmazlık durumunda kanıt + +### 3. Topluluk Şeffaflığı (Berdevk) +- Her işlem duyurulur +- Public dashboard güncel +- Community feedback + +### 4. Proof of Reserves +- Etherscan'de doğrulanabilir +- Her saat otomatik kontrol +- Alert sistemi (under-collateralized ise) + +--- + +## 📍 Ethereum Multisig Detayları + +### Gnosis Safe Configuration +- **Network:** Ethereum Mainnet +- **Safe Address:** `0x123...` (TBD) +- **Threshold:** 3/5 +- **Owners:** + - `0xAaa...` (Serok) + - `0xBbb...` (SerokiMeclise) + - `0xCcc...` (Xezinedar) + - `0xDdd...` (Noter) + - `0xEee...` (Berdevk) + +### Public Links +- Etherscan: `https://etherscan.io/address/0x123...` +- Gnosis Safe UI: `https://app.safe.global/eth:0x123...` +- Reserve Dashboard: `https://pezkuwi.com/reserves` + +--- + +## 🎯 Rol Sorumlulukları + +### Serok (Founder) +- ✅ Stratejik kararlar +- ✅ Acil durum müdahalesi +- ✅ Governance voting + +### SerokiMeclise (Meclis Başkanı) +- ✅ Demokratik kontrol +- ✅ Topluluk temsilciliği +- ✅ Policy oversight + +### Xezinedar (Hazine Müdürü) +- ✅ Reserve management +- ✅ Collateral ratio monitoring +- ✅ Financial reporting + +### Noter +- ✅ Tüm işlemleri kaydet +- ✅ Hukuki belgeler hazırla +- ✅ Audit trail maintain et + +### Berdevk (Sözcü) +- ✅ Public dashboard yönet +- ✅ İşlemleri duyur +- ✅ Community questions cevapla +- ✅ Şeffaflık sağla + +--- + +## 📈 Başarı Metrikleri + +### Güven Göstergeleri +- ✅ 3/5 Multisig (Tek kişi kontrolü yok) +- ✅ %102+ Collateralization Ratio +- ✅ Insurance Fund: $XX,XXX +- ✅ Live Reserves: Etherscan'de doğrulanabilir +- ✅ 0 Incidents (hedef) + +### Şeffaflık +- ✅ Public dashboard 24/7 aktif +- ✅ Tüm işlemler blockchain'de +- ✅ Aylık audit raporları +- ✅ Community AMAs + +--- + +## 🚀 İlk Kurulum Adımları + +### 1. Gnosis Safe Oluştur +```bash +# https://app.safe.global adresine git +1. "Create New Safe" tıkla +2. Ethereum Mainnet seç +3. 5 owner ekle (yukarıdaki adresler) +4. Threshold: 3/5 +5. Deploy +``` + +### 2. USDT Deposit +```bash +# İlk likidite (örnek: $10,000) +1. Gnosis Safe address'e USDT gönder +2. Etherscan'de doğrula +3. Safe balance = $10,000 USDT +``` + +### 3. Chain'de wUSDT Asset Oluştur +```bash +# Polkadot.js Apps ile +1. Developer → Sudo → assets.create + - id: 2 + - admin: + - min_balance: 1000000 + +2. Developer → Sudo → assets.setMetadata + - id: 2 + - name: "Wrapped USDT" + - symbol: "wUSDT" + - decimals: 6 +``` + +### 4. Pool Oluştur +```bash +# wUSDT/PEZ ve wUSDT/wHEZ poolları +1. assetConversion.createPool(1, 2) # PEZ/wUSDT +2. assetConversion.createPool(0, 2) # wHEZ/wUSDT +3. İlk liquidity ekle +``` + +### 5. Public Dashboard Deploy +```bash +cd DKSweb +# Reserves dashboard component ekle +npm run build +npm run deploy +``` + +--- + +## ⚠️ Risk Yönetimi + +### Potansiyel Riskler +1. **Multisig Key Loss:** 2/5 key kaybolsa bile 3/5 hala çalışır +2. **Ethereum Gas Fees:** High gas durumunda withdrawaller pahalı +3. **Smart Contract Bug:** Gnosis Safe audited ama risk var +4. **Regulatory:** USDT yasal sorunlar yaşayabilir + +### Mitigations +1. ✅ Key backup stratejisi (her owner için) +2. ✅ Gas limit alarms (yüksek gas'da uyar) +3. ✅ Insurance fund (bug durumunda) +4. ✅ Legal counsel (compliance için) + +--- + +## 📞 İletişim + +### Public Channels +- Discord: `#usdt-bridge` +- Twitter: `@PezkuwiChain` +- Telegram: `@pezkuwi_reserves` + +### Emergency Contact +- Berdevk (Sözcü): `berdevk@pezkuwi.com` +- 24/7 Support: `support@pezkuwi.com` + +### Audit & Security +- Bug Bounty: `https://pezkuwi.com/bug-bounty` +- Security Email: `security@pezkuwi.com` + +--- + +**Son Güncelleme:** 2025-11-03 +**Versiyon:** 1.0 +**Durum:** Planning Phase diff --git a/src/App.tsx b/src/App.tsx index 3ca8aa50..71508d8d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import ProfileSettings from '@/pages/ProfileSettings'; import AdminPanel from '@/pages/AdminPanel'; import WalletDashboard from './pages/WalletDashboard'; import PoolDashboardPage from './pages/PoolDashboard'; +import ReservesDashboardPage from './pages/ReservesDashboardPage'; import { AppProvider } from '@/contexts/AppContext'; import { PolkadotProvider } from '@/contexts/PolkadotContext'; import { WalletProvider } from '@/contexts/WalletContext'; @@ -62,6 +63,11 @@ function App() { } /> + + + + } /> } /> diff --git a/src/components/MultisigMembers.tsx b/src/components/MultisigMembers.tsx new file mode 100644 index 00000000..4c7b9566 --- /dev/null +++ b/src/components/MultisigMembers.tsx @@ -0,0 +1,172 @@ +import React, { useState, useEffect } from 'react'; +import { Shield, Users, CheckCircle, XCircle, ExternalLink } from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { + getMultisigMemberInfo, + calculateMultisigAddress, + USDT_MULTISIG_CONFIG, + formatMultisigAddress, +} from '@/lib/multisig'; +import { getTikiDisplayName, getTikiEmoji } from '@/lib/tiki'; + +interface MultisigMembersProps { + specificAddresses?: Record; + showMultisigAddress?: boolean; +} + +export const MultisigMembers: React.FC = ({ + specificAddresses = {}, + showMultisigAddress = true, +}) => { + const { api, isApiReady } = usePolkadot(); + const [members, setMembers] = useState([]); + const [multisigAddress, setMultisigAddress] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!api || !isApiReady) return; + + const fetchMembers = async () => { + setLoading(true); + try { + const memberInfo = await getMultisigMemberInfo(api, specificAddresses); + setMembers(memberInfo); + + // Calculate multisig address + const addresses = memberInfo.map((m) => m.address); + if (addresses.length > 0) { + const multisig = calculateMultisigAddress(addresses); + setMultisigAddress(multisig); + } + } catch (error) { + console.error('Error fetching multisig members:', error); + } finally { + setLoading(false); + } + }; + + fetchMembers(); + }, [api, isApiReady, specificAddresses]); + + if (loading) { + return ( + +
+
+
+
+ ); + } + + return ( + + {/* Header */} +
+
+ +
+

USDT Treasury Multisig

+

+ {USDT_MULTISIG_CONFIG.threshold}/{members.length} Signatures Required +

+
+
+ + + {members.length} Members + +
+ + {/* Multisig Address */} + {showMultisigAddress && multisigAddress && ( +
+

Multisig Account

+
+ {formatMultisigAddress(multisigAddress)} + +
+
+ )} + + {/* Members List */} +
+ {members.map((member, index) => ( +
+
+
+ {getTikiEmoji(member.tiki)} +
+
+

{member.role}

+
+ + {getTikiDisplayName(member.tiki)} + + {member.isUnique && ( + + + On-Chain + + )} +
+
+
+ +
+ + {member.address.slice(0, 6)}...{member.address.slice(-4)} + +
+ {member.isUnique ? ( + + ) : ( + + )} +
+
+
+ ))} +
+ + {/* Info Alert */} + + + +

Security Features

+
    +
  • • {USDT_MULTISIG_CONFIG.threshold} out of {members.length} signatures required
  • +
  • • {members.filter(m => m.isUnique).length} members verified on-chain via Tiki
  • +
  • • No single person can control funds
  • +
  • • All transactions visible on blockchain
  • +
+
+
+ + {/* Explorer Link */} + {multisigAddress && ( + + )} +
+ ); +}; diff --git a/src/components/ReservesDashboard.tsx b/src/components/ReservesDashboard.tsx new file mode 100644 index 00000000..5dd9e674 --- /dev/null +++ b/src/components/ReservesDashboard.tsx @@ -0,0 +1,294 @@ +import React, { useState, useEffect } from 'react'; +import { DollarSign, TrendingUp, Shield, AlertTriangle, RefreshCw, ExternalLink } from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { getWUSDTTotalSupply, checkReserveHealth, formatWUSDT } from '@/lib/usdt'; +import { MultisigMembers } from './MultisigMembers'; + +interface ReservesDashboardProps { + specificAddresses?: Record; + offChainReserveAmount?: number; // Manual input for MVP +} + +export const ReservesDashboard: React.FC = ({ + specificAddresses = {}, + offChainReserveAmount = 0, +}) => { + const { api, isApiReady } = usePolkadot(); + + const [wusdtSupply, setWusdtSupply] = useState(0); + const [offChainReserve, setOffChainReserve] = useState(offChainReserveAmount); + const [collateralRatio, setCollateralRatio] = useState(0); + const [isHealthy, setIsHealthy] = useState(true); + const [loading, setLoading] = useState(true); + const [lastUpdate, setLastUpdate] = useState(new Date()); + + // Fetch reserve data + const fetchReserveData = async () => { + if (!api || !isApiReady) return; + + setLoading(true); + try { + const supply = await getWUSDTTotalSupply(api); + setWusdtSupply(supply); + + const health = await checkReserveHealth(api, offChainReserve); + setCollateralRatio(health.collateralRatio); + setIsHealthy(health.isHealthy); + setLastUpdate(new Date()); + } catch (error) { + console.error('Error fetching reserve data:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchReserveData(); + + // Auto-refresh every 30 seconds + const interval = setInterval(fetchReserveData, 30000); + return () => clearInterval(interval); + }, [api, isApiReady, offChainReserve]); + + const getHealthColor = () => { + if (collateralRatio >= 105) return 'text-green-500'; + if (collateralRatio >= 100) return 'text-yellow-500'; + return 'text-red-500'; + }; + + const getHealthStatus = () => { + if (collateralRatio >= 105) return 'Healthy'; + if (collateralRatio >= 100) return 'Warning'; + return 'Critical'; + }; + + return ( +
+ {/* Header */} +
+
+

+ + USDT Reserves Dashboard +

+

Real-time reserve status and multisig info

+
+ +
+ + {/* Key Metrics */} +
+ {/* Total Supply */} + +
+
+

Total wUSDT Supply

+

+ ${formatWUSDT(wusdtSupply)} +

+

On-chain (Assets pallet)

+
+ +
+
+ + {/* Off-chain Reserve */} + +
+
+

Off-chain USDT Reserve

+

+ ${formatWUSDT(offChainReserve)} +

+
+ setOffChainReserve(parseFloat(e.target.value) || 0)} + className="w-24 text-xs bg-gray-700 border border-gray-600 rounded px-2 py-1 text-white" + placeholder="Amount" + /> + +
+
+ +
+
+ + {/* Collateral Ratio */} + +
+
+

Collateral Ratio

+

+ {collateralRatio.toFixed(2)}% +

+
+ + {getHealthStatus()} + +
+
+ +
+
+
+ + {/* Health Alert */} + {!isHealthy && ( + + + +

Under-collateralized!

+

+ Reserve ratio is below 100%. Off-chain USDT reserves ({formatWUSDT(offChainReserve)}) + are less than on-chain wUSDT supply ({formatWUSDT(wusdtSupply)}). +

+
+
+ )} + + {/* Tabs */} + + + Overview + Multisig + Proof of Reserves + + + {/* Overview Tab */} + + +

Reserve Details

+ +
+
+ On-chain wUSDT + ${formatWUSDT(wusdtSupply)} +
+ +
+ Off-chain USDT + ${formatWUSDT(offChainReserve)} +
+ +
+ Backing Ratio + + {collateralRatio.toFixed(2)}% + +
+ +
+ Status + + {getHealthStatus()} + +
+ +
+ Last Updated + {lastUpdate.toLocaleTimeString()} +
+
+ + + + +

1:1 Backing

+

+ Every wUSDT is backed by real USDT held in the multisig treasury. + Target ratio: ≥100% (ideally 105% for safety buffer). +

+
+
+
+
+ + {/* Multisig Tab */} + + + + + {/* Proof of Reserves Tab */} + + +

Proof of Reserves

+ +
+ + + +

How to Verify Reserves:

+
    +
  1. Check on-chain wUSDT supply via Polkadot.js Apps
  2. +
  3. Verify multisig account balance (if reserves on-chain)
  4. +
  5. Compare with off-chain treasury (bank/exchange account)
  6. +
  7. Ensure ratio ≥ 100%
  8. +
+
+
+ + + +
+

+ Note: Off-chain Reserves +

+

+ In this MVP implementation, off-chain USDT reserves are manually reported. + For full decentralization, consider integrating with oracle services or + using XCM bridge for on-chain verification. +

+
+
+
+
+
+
+ ); +}; diff --git a/src/components/USDTBridge.tsx b/src/components/USDTBridge.tsx new file mode 100644 index 00000000..240546d7 --- /dev/null +++ b/src/components/USDTBridge.tsx @@ -0,0 +1,353 @@ +import React, { useState, useEffect } from 'react'; +import { X, ArrowDown, ArrowUp, AlertCircle, Info, Clock, CheckCircle2 } from 'lucide-react'; +import { web3FromAddress } from '@polkadot/extension-dapp'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { useWallet } from '@/contexts/WalletContext'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; +import { + getWUSDTBalance, + calculateWithdrawalDelay, + getWithdrawalTier, + formatDelay, + formatWUSDT, +} from '@/lib/usdt'; +import { isMultisigMember } from '@/lib/multisig'; + +interface USDTBridgeProps { + isOpen: boolean; + onClose: () => void; + specificAddresses?: Record; +} + +export const USDTBridge: React.FC = ({ + isOpen, + onClose, + specificAddresses = {}, +}) => { + const { api, selectedAccount, isApiReady } = usePolkadot(); + const { refreshBalances } = useWallet(); + + const [depositAmount, setDepositAmount] = useState(''); + const [withdrawAmount, setWithdrawAmount] = useState(''); + const [withdrawAddress, setWithdrawAddress] = useState(''); // Bank account or crypto address + const [wusdtBalance, setWusdtBalance] = useState(0); + const [isMultisigMemberState, setIsMultisigMemberState] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(null); + const [error, setError] = useState(null); + + // Fetch wUSDT balance + useEffect(() => { + if (!api || !isApiReady || !selectedAccount || !isOpen) return; + + const fetchBalance = async () => { + const balance = await getWUSDTBalance(api, selectedAccount.address); + setWusdtBalance(balance); + + // Check if user is multisig member + const isMember = await isMultisigMember(api, selectedAccount.address, specificAddresses); + setIsMultisigMemberState(isMember); + }; + + fetchBalance(); + }, [api, isApiReady, selectedAccount, isOpen, specificAddresses]); + + // Handle deposit (user requests deposit) + const handleDeposit = async () => { + if (!depositAmount || parseFloat(depositAmount) <= 0) { + setError('Please enter a valid amount'); + return; + } + + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + // In real implementation: + // 1. User transfers USDT to treasury (off-chain) + // 2. Notary verifies the transfer + // 3. Multisig mints wUSDT to user + + // For now, just show instructions + setSuccess( + `Deposit request for ${depositAmount} USDT created. Please follow the instructions to complete the deposit.` + ); + setDepositAmount(''); + } catch (err) { + console.error('Deposit error:', err); + setError(err instanceof Error ? err.message : 'Deposit failed'); + } finally { + setIsLoading(false); + } + }; + + // Handle withdrawal (burn wUSDT) + const handleWithdrawal = async () => { + if (!api || !selectedAccount) return; + + const amount = parseFloat(withdrawAmount); + + if (!amount || amount <= 0) { + setError('Please enter a valid amount'); + return; + } + + if (amount > wusdtBalance) { + setError('Insufficient wUSDT balance'); + return; + } + + if (!withdrawAddress) { + setError('Please enter withdrawal address'); + return; + } + + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const injector = await web3FromAddress(selectedAccount.address); + + // Burn wUSDT + const amountBN = BigInt(Math.floor(amount * 1e6)); // 6 decimals + const burnTx = api.tx.assets.burn(2, selectedAccount.address, amountBN.toString()); + + await burnTx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status }) => { + if (status.isFinalized) { + const delay = calculateWithdrawalDelay(amount); + setSuccess( + `Withdrawal request submitted! wUSDT burned. USDT will be sent to ${withdrawAddress} after ${formatDelay(delay)}.` + ); + setWithdrawAmount(''); + setWithdrawAddress(''); + refreshBalances(); + setIsLoading(false); + } + }); + } catch (err) { + console.error('Withdrawal error:', err); + setError(err instanceof Error ? err.message : 'Withdrawal failed'); + setIsLoading(false); + } + }; + + if (!isOpen) return null; + + const withdrawalTier = withdrawAmount ? getWithdrawalTier(parseFloat(withdrawAmount)) : null; + const withdrawalDelay = withdrawAmount ? calculateWithdrawalDelay(parseFloat(withdrawAmount)) : 0; + + return ( +
+
+ {/* Header */} +
+
+

USDT Bridge

+

Deposit or withdraw USDT

+
+ +
+ + {/* Balance Display */} +
+

Your wUSDT Balance

+

{formatWUSDT(wusdtBalance)}

+ {isMultisigMemberState && ( + + Multisig Member + + )} +
+ + {/* Error/Success Alerts */} + {error && ( + + + {error} + + )} + + {success && ( + + + {success} + + )} + + {/* Tabs */} + + + Deposit + Withdraw + + + {/* Deposit Tab */} + + + + +

How to Deposit:

+
    +
  1. Transfer USDT to the treasury account (off-chain)
  2. +
  3. Notary verifies and records your transaction
  4. +
  5. Multisig (3/5) approves and mints wUSDT to your account
  6. +
  7. Receive wUSDT in 2-5 minutes
  8. +
+
+
+ +
+ + setDepositAmount(e.target.value)} + placeholder="0.00" + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500" + disabled={isLoading} + /> +
+ +
+
+ You will receive: + + {depositAmount || '0.00'} wUSDT + +
+
+ Exchange rate: + 1:1 +
+
+ Estimated time: + 2-5 minutes +
+
+ + +
+ + {/* Withdraw Tab */} + + + + +

How to Withdraw:

+
    +
  1. Burn your wUSDT on-chain
  2. +
  3. Wait for security delay ({withdrawalDelay > 0 && formatDelay(withdrawalDelay)})
  4. +
  5. Multisig (3/5) approves and sends USDT
  6. +
  7. Receive USDT to your specified address
  8. +
+
+
+ +
+ + setWithdrawAmount(e.target.value)} + placeholder="0.00" + max={wusdtBalance} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500" + disabled={isLoading} + /> + +
+ +
+ + setWithdrawAddress(e.target.value)} + placeholder="Enter bank account or crypto address" + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500" + disabled={isLoading} + /> +
+ + {withdrawAmount && parseFloat(withdrawAmount) > 0 && ( +
+
+ You will receive: + {withdrawAmount} USDT +
+
+ Withdrawal tier: + + {withdrawalTier} + +
+
+ Security delay: + + + {formatDelay(withdrawalDelay)} + +
+
+ )} + + +
+
+
+
+ ); +}; diff --git a/src/lib/multisig.ts b/src/lib/multisig.ts new file mode 100644 index 00000000..68ccf6c3 --- /dev/null +++ b/src/lib/multisig.ts @@ -0,0 +1,325 @@ +// ======================================== +// Multisig Utilities for USDT Treasury +// ======================================== +// Full on-chain multisig using Substrate pallet-multisig + +import type { ApiPromise } from '@polkadot/api'; +import type { SubmittableExtrinsic } from '@polkadot/api/types'; +import { Tiki } from './tiki'; +import { encodeAddress, sortAddresses } from '@polkadot/util-crypto'; + +// ======================================== +// MULTISIG CONFIGURATION +// ======================================== + +export interface MultisigMember { + role: string; + tiki: Tiki; + isUnique: boolean; + address?: string; // For non-unique roles, hardcoded address +} + +export const USDT_MULTISIG_CONFIG = { + threshold: 3, + members: [ + { role: 'Founder/President', tiki: Tiki.Serok, isUnique: true }, + { role: 'Parliament Speaker', tiki: Tiki.SerokiMeclise, isUnique: true }, + { role: 'Treasurer', tiki: Tiki.Xezinedar, isUnique: true }, + { role: 'Notary', tiki: Tiki.Noter, isUnique: false, address: '' }, // Will be set at runtime + { role: 'Spokesperson', tiki: Tiki.Berdevk, isUnique: false, address: '' }, + ] as MultisigMember[], +}; + +// ======================================== +// MULTISIG MEMBER QUERIES +// ======================================== + +/** + * Get all multisig members from on-chain tiki holders + * @param api - Polkadot API instance + * @param specificAddresses - Addresses for non-unique roles {tiki: address} + * @returns Sorted array of member addresses + */ +export async function getMultisigMembers( + api: ApiPromise, + specificAddresses: Record = {} +): Promise { + const members: string[] = []; + + for (const memberConfig of USDT_MULTISIG_CONFIG.members) { + if (memberConfig.isUnique) { + // Query from chain for unique roles + try { + const holder = await api.query.tiki.tikiHolder(memberConfig.tiki); + if (holder.isSome) { + const address = holder.unwrap().toString(); + members.push(address); + } else { + console.warn(`No holder found for unique role: ${memberConfig.tiki}`); + } + } catch (error) { + console.error(`Error querying ${memberConfig.tiki}:`, error); + } + } else { + // Use hardcoded address for non-unique roles + const address = specificAddresses[memberConfig.tiki] || memberConfig.address; + if (address) { + members.push(address); + } else { + console.warn(`No address specified for non-unique role: ${memberConfig.tiki}`); + } + } + } + + // Multisig requires sorted addresses + return sortAddresses(members); +} + +/** + * Calculate deterministic multisig account address + * @param members - Sorted array of member addresses + * @param threshold - Signature threshold (default: 3) + * @param ss58Format - SS58 format for address encoding (default: 42) + * @returns Multisig account address + */ +export function calculateMultisigAddress( + members: string[], + threshold: number = USDT_MULTISIG_CONFIG.threshold, + ss58Format: number = 42 +): string { + // Sort members (multisig requires sorted order) + const sortedMembers = sortAddresses(members); + + // Create multisig address + // Formula: blake2(b"modlpy/utilisuba" + concat(sorted_members) + threshold) + const multisigId = encodeAddress( + new Uint8Array([ + ...Buffer.from('modlpy/utilisuba'), + ...sortedMembers.flatMap((addr) => Array.from(Buffer.from(addr, 'hex'))), + threshold, + ]), + ss58Format + ); + + return multisigId; +} + +/** + * Check if an address is a multisig member + * @param api - Polkadot API instance + * @param address - Address to check + * @param specificAddresses - Addresses for non-unique roles + * @returns boolean + */ +export async function isMultisigMember( + api: ApiPromise, + address: string, + specificAddresses: Record = {} +): Promise { + const members = await getMultisigMembers(api, specificAddresses); + return members.includes(address); +} + +/** + * Get multisig member info for display + * @param api - Polkadot API instance + * @param specificAddresses - Addresses for non-unique roles + * @returns Array of member info objects + */ +export async function getMultisigMemberInfo( + api: ApiPromise, + specificAddresses: Record = {} +): Promise> { + const memberInfo = []; + + for (const memberConfig of USDT_MULTISIG_CONFIG.members) { + let address = ''; + + if (memberConfig.isUnique) { + try { + const holder = await api.query.tiki.tikiHolder(memberConfig.tiki); + if (holder.isSome) { + address = holder.unwrap().toString(); + } + } catch (error) { + console.error(`Error querying ${memberConfig.tiki}:`, error); + } + } else { + address = specificAddresses[memberConfig.tiki] || memberConfig.address || ''; + } + + if (address) { + memberInfo.push({ + role: memberConfig.role, + tiki: memberConfig.tiki, + address, + isUnique: memberConfig.isUnique, + }); + } + } + + return memberInfo; +} + +// ======================================== +// MULTISIG TRANSACTION HELPERS +// ======================================== + +export interface MultisigTimepoint { + height: number; + index: number; +} + +/** + * Create a new multisig transaction (first signature) + * @param api - Polkadot API instance + * @param call - The extrinsic to execute via multisig + * @param otherSignatories - Other multisig members (excluding current signer) + * @param threshold - Signature threshold + * @returns Multisig transaction + */ +export function createMultisigTx( + api: ApiPromise, + call: SubmittableExtrinsic<'promise'>, + otherSignatories: string[], + threshold: number = USDT_MULTISIG_CONFIG.threshold +) { + const maxWeight = { + refTime: 1000000000, + proofSize: 64 * 1024, + }; + + return api.tx.multisig.asMulti( + threshold, + sortAddresses(otherSignatories), + null, // No timepoint for first call + call, + maxWeight + ); +} + +/** + * Approve an existing multisig transaction + * @param api - Polkadot API instance + * @param call - The original extrinsic + * @param otherSignatories - Other multisig members + * @param timepoint - Block height and index of the first approval + * @param threshold - Signature threshold + * @returns Approval transaction + */ +export function approveMultisigTx( + api: ApiPromise, + call: SubmittableExtrinsic<'promise'>, + otherSignatories: string[], + timepoint: MultisigTimepoint, + threshold: number = USDT_MULTISIG_CONFIG.threshold +) { + const maxWeight = { + refTime: 1000000000, + proofSize: 64 * 1024, + }; + + return api.tx.multisig.asMulti( + threshold, + sortAddresses(otherSignatories), + timepoint, + call, + maxWeight + ); +} + +/** + * Cancel a multisig transaction + * @param api - Polkadot API instance + * @param callHash - Hash of the call to cancel + * @param otherSignatories - Other multisig members + * @param timepoint - Block height and index of the call + * @param threshold - Signature threshold + * @returns Cancel transaction + */ +export function cancelMultisigTx( + api: ApiPromise, + callHash: string, + otherSignatories: string[], + timepoint: MultisigTimepoint, + threshold: number = USDT_MULTISIG_CONFIG.threshold +) { + return api.tx.multisig.cancelAsMulti( + threshold, + sortAddresses(otherSignatories), + timepoint, + callHash + ); +} + +// ======================================== +// MULTISIG STORAGE QUERIES +// ======================================== + +/** + * Get pending multisig calls + * @param api - Polkadot API instance + * @param multisigAddress - The multisig account address + * @returns Array of pending calls + */ +export async function getPendingMultisigCalls( + api: ApiPromise, + multisigAddress: string +): Promise { + try { + const multisigs = await api.query.multisig.multisigs.entries(multisigAddress); + + return multisigs.map(([key, value]) => { + const callHash = key.args[1].toHex(); + const multisigData = value.toJSON() as any; + + return { + callHash, + when: multisigData.when, + deposit: multisigData.deposit, + depositor: multisigData.depositor, + approvals: multisigData.approvals, + }; + }); + } catch (error) { + console.error('Error fetching pending multisig calls:', error); + return []; + } +} + +// ======================================== +// DISPLAY HELPERS +// ======================================== + +/** + * Format multisig address for display + * @param address - Full multisig address + * @returns Shortened address + */ +export function formatMultisigAddress(address: string): string { + if (!address) return ''; + return `${address.slice(0, 8)}...${address.slice(-8)}`; +} + +/** + * Get approval status text + * @param approvals - Number of approvals + * @param threshold - Required threshold + * @returns Status text + */ +export function getApprovalStatus(approvals: number, threshold: number): string { + if (approvals >= threshold) return 'Ready to Execute'; + return `${approvals}/${threshold} Approvals`; +} + +/** + * Get approval status color + * @param approvals - Number of approvals + * @param threshold - Required threshold + * @returns Tailwind color class + */ +export function getApprovalStatusColor(approvals: number, threshold: number): string { + if (approvals >= threshold) return 'text-green-500'; + if (approvals >= threshold - 1) return 'text-yellow-500'; + return 'text-gray-500'; +} diff --git a/src/lib/usdt.ts b/src/lib/usdt.ts new file mode 100644 index 00000000..1d27a53b --- /dev/null +++ b/src/lib/usdt.ts @@ -0,0 +1,314 @@ +// ======================================== +// USDT Bridge Utilities +// ======================================== +// Handles wUSDT minting, burning, and reserve management + +import type { ApiPromise } from '@polkadot/api'; +import { ASSET_IDS } from './wallet'; +import { getMultisigMembers, createMultisigTx } from './multisig'; + +// ======================================== +// CONSTANTS +// ======================================== + +export const WUSDT_ASSET_ID = ASSET_IDS.WUSDT; +export const WUSDT_DECIMALS = 6; // USDT has 6 decimals + +// Withdrawal limits and timeouts +export const WITHDRAWAL_LIMITS = { + instant: { + maxAmount: 1000, // $1,000 + delay: 0, // No delay + }, + standard: { + maxAmount: 10000, // $10,000 + delay: 3600, // 1 hour in seconds + }, + large: { + maxAmount: Infinity, + delay: 86400, // 24 hours + }, +}; + +// ======================================== +// ASSET QUERIES +// ======================================== + +/** + * Get wUSDT balance for an account + * @param api - Polkadot API instance + * @param address - Account address + * @returns Balance in human-readable format + */ +export async function getWUSDTBalance(api: ApiPromise, address: string): Promise { + try { + const balance = await api.query.assets.account(WUSDT_ASSET_ID, address); + + if (balance.isSome) { + const balanceData = balance.unwrap().toJSON() as any; + return Number(balanceData.balance) / Math.pow(10, WUSDT_DECIMALS); + } + + return 0; + } catch (error) { + console.error('Error fetching wUSDT balance:', error); + return 0; + } +} + +/** + * Get total wUSDT supply + * @param api - Polkadot API instance + * @returns Total supply in human-readable format + */ +export async function getWUSDTTotalSupply(api: ApiPromise): Promise { + try { + const assetDetails = await api.query.assets.asset(WUSDT_ASSET_ID); + + if (assetDetails.isSome) { + const details = assetDetails.unwrap().toJSON() as any; + return Number(details.supply) / Math.pow(10, WUSDT_DECIMALS); + } + + return 0; + } catch (error) { + console.error('Error fetching wUSDT supply:', error); + return 0; + } +} + +/** + * Get wUSDT asset metadata + * @param api - Polkadot API instance + * @returns Asset metadata + */ +export async function getWUSDTMetadata(api: ApiPromise) { + try { + const metadata = await api.query.assets.metadata(WUSDT_ASSET_ID); + return metadata.toJSON(); + } catch (error) { + console.error('Error fetching wUSDT metadata:', error); + return null; + } +} + +// ======================================== +// MULTISIG OPERATIONS +// ======================================== + +/** + * Create multisig transaction to mint wUSDT + * @param api - Polkadot API instance + * @param beneficiary - Who will receive the wUSDT + * @param amount - Amount in human-readable format (e.g., 100.50 USDT) + * @param signerAddress - Address of the signer creating this tx + * @param specificAddresses - Addresses for non-unique multisig members + * @returns Multisig transaction + */ +export async function createMintWUSDTTx( + api: ApiPromise, + beneficiary: string, + amount: number, + signerAddress: string, + specificAddresses: Record = {} +) { + // Convert to smallest unit + const amountBN = BigInt(Math.floor(amount * Math.pow(10, WUSDT_DECIMALS))); + + // Create the mint call + const mintCall = api.tx.assets.mint(WUSDT_ASSET_ID, beneficiary, amountBN.toString()); + + // Get all multisig members + const allMembers = await getMultisigMembers(api, specificAddresses); + + // Other signatories (excluding current signer) + const otherSignatories = allMembers.filter((addr) => addr !== signerAddress); + + // Create multisig transaction + return createMultisigTx(api, mintCall, otherSignatories); +} + +/** + * Create multisig transaction to burn wUSDT + * @param api - Polkadot API instance + * @param from - Who will have their wUSDT burned + * @param amount - Amount in human-readable format + * @param signerAddress - Address of the signer creating this tx + * @param specificAddresses - Addresses for non-unique multisig members + * @returns Multisig transaction + */ +export async function createBurnWUSDTTx( + api: ApiPromise, + from: string, + amount: number, + signerAddress: string, + specificAddresses: Record = {} +) { + const amountBN = BigInt(Math.floor(amount * Math.pow(10, WUSDT_DECIMALS))); + + const burnCall = api.tx.assets.burn(WUSDT_ASSET_ID, from, amountBN.toString()); + + const allMembers = await getMultisigMembers(api, specificAddresses); + const otherSignatories = allMembers.filter((addr) => addr !== signerAddress); + + return createMultisigTx(api, burnCall, otherSignatories); +} + +// ======================================== +// WITHDRAWAL HELPERS +// ======================================== + +/** + * Calculate withdrawal delay based on amount + * @param amount - Withdrawal amount in USDT + * @returns Delay in seconds + */ +export function calculateWithdrawalDelay(amount: number): number { + if (amount <= WITHDRAWAL_LIMITS.instant.maxAmount) { + return WITHDRAWAL_LIMITS.instant.delay; + } else if (amount <= WITHDRAWAL_LIMITS.standard.maxAmount) { + return WITHDRAWAL_LIMITS.standard.delay; + } else { + return WITHDRAWAL_LIMITS.large.delay; + } +} + +/** + * Get withdrawal tier name + * @param amount - Withdrawal amount + * @returns Tier name + */ +export function getWithdrawalTier(amount: number): string { + if (amount <= WITHDRAWAL_LIMITS.instant.maxAmount) return 'Instant'; + if (amount <= WITHDRAWAL_LIMITS.standard.maxAmount) return 'Standard'; + return 'Large'; +} + +/** + * Format delay time for display + * @param seconds - Delay in seconds + * @returns Human-readable format + */ +export function formatDelay(seconds: number): string { + if (seconds === 0) return 'Instant'; + if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`; + return `${Math.floor(seconds / 86400)} days`; +} + +// ======================================== +// RESERVE CHECKING +// ======================================== + +export interface ReserveStatus { + wusdtSupply: number; + offChainReserve: number; // This would come from off-chain oracle/API + collateralRatio: number; + isHealthy: boolean; +} + +/** + * Check reserve health + * @param api - Polkadot API instance + * @param offChainReserve - Off-chain USDT reserve amount (from treasury) + * @returns Reserve status + */ +export async function checkReserveHealth( + api: ApiPromise, + offChainReserve: number +): Promise { + const wusdtSupply = await getWUSDTTotalSupply(api); + + const collateralRatio = wusdtSupply > 0 ? (offChainReserve / wusdtSupply) * 100 : 0; + + return { + wusdtSupply, + offChainReserve, + collateralRatio, + isHealthy: collateralRatio >= 100, // At least 100% backed + }; +} + +// ======================================== +// EVENT MONITORING +// ======================================== + +/** + * Subscribe to wUSDT mint events + * @param api - Polkadot API instance + * @param callback - Callback function for each mint event + */ +export function subscribeToMintEvents( + api: ApiPromise, + callback: (beneficiary: string, amount: number, txHash: string) => void +) { + return api.query.system.events((events) => { + events.forEach((record) => { + const { event } = record; + + if (api.events.assets.Issued.is(event)) { + const [assetId, beneficiary, amount] = event.data; + + if (assetId.toNumber() === WUSDT_ASSET_ID) { + const amountNum = Number(amount.toString()) / Math.pow(10, WUSDT_DECIMALS); + callback(beneficiary.toString(), amountNum, record.hash.toHex()); + } + } + }); + }); +} + +/** + * Subscribe to wUSDT burn events + * @param api - Polkadot API instance + * @param callback - Callback function for each burn event + */ +export function subscribeToBurnEvents( + api: ApiPromise, + callback: (account: string, amount: number, txHash: string) => void +) { + return api.query.system.events((events) => { + events.forEach((record) => { + const { event } = record; + + if (api.events.assets.Burned.is(event)) { + const [assetId, account, amount] = event.data; + + if (assetId.toNumber() === WUSDT_ASSET_ID) { + const amountNum = Number(amount.toString()) / Math.pow(10, WUSDT_DECIMALS); + callback(account.toString(), amountNum, record.hash.toHex()); + } + } + }); + }); +} + +// ======================================== +// DISPLAY HELPERS +// ======================================== + +/** + * Format wUSDT amount for display + * @param amount - Amount in smallest unit or human-readable + * @param fromSmallestUnit - Whether input is in smallest unit + * @returns Formatted string + */ +export function formatWUSDT(amount: number | string, fromSmallestUnit = false): string { + const value = typeof amount === 'string' ? parseFloat(amount) : amount; + + if (fromSmallestUnit) { + return (value / Math.pow(10, WUSDT_DECIMALS)).toFixed(2); + } + + return value.toFixed(2); +} + +/** + * Parse human-readable USDT to smallest unit + * @param amount - Human-readable amount + * @returns Amount in smallest unit (BigInt) + */ +export function parseWUSDT(amount: number | string): bigint { + const value = typeof amount === 'string' ? parseFloat(amount) : amount; + return BigInt(Math.floor(value * Math.pow(10, WUSDT_DECIMALS))); +} diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index e83d6ddd..e012876b 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -34,6 +34,7 @@ export const CHAIN_CONFIG = { export const ASSET_IDS = { WHEZ: parseInt(import.meta.env.VITE_ASSET_WHEZ || '0'), // Wrapped HEZ PEZ: parseInt(import.meta.env.VITE_ASSET_PEZ || '1'), // PEZ utility token + WUSDT: parseInt(import.meta.env.VITE_ASSET_WUSDT || '2'), // Wrapped USDT (multisig backed) USDT: parseInt(import.meta.env.VITE_ASSET_USDT || '3'), BTC: parseInt(import.meta.env.VITE_ASSET_BTC || '4'), ETH: parseInt(import.meta.env.VITE_ASSET_ETH || '5'), diff --git a/src/pages/ReservesDashboardPage.tsx b/src/pages/ReservesDashboardPage.tsx new file mode 100644 index 00000000..698d026f --- /dev/null +++ b/src/pages/ReservesDashboardPage.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ArrowLeft, Plus } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { ReservesDashboard } from '@/components/ReservesDashboard'; +import { USDTBridge } from '@/components/USDTBridge'; + +// TODO: Replace with actual addresses when multisig is set up +const SPECIFIC_ADDRESSES = { + Noter: '5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy', // Example address + Berdevk: '5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw', // Example address +}; + +const ReservesDashboardPage = () => { + const navigate = useNavigate(); + const [isBridgeOpen, setIsBridgeOpen] = useState(false); + const [offChainReserve, setOffChainReserve] = useState(10000); // Example: $10,000 USDT + + return ( +
+
+ {/* Back Button */} + + + {/* Bridge Button */} +
+ +
+ + {/* Main Content */} + + + {/* Bridge Modal */} + setIsBridgeOpen(false)} + specificAddresses={SPECIFIC_ADDRESSES} + /> +
+
+ ); +}; + +export default ReservesDashboardPage;