diff --git a/shared/lib/citizenship-workflow.ts b/shared/lib/citizenship-workflow.ts index d832e72e..17b7c316 100644 --- a/shared/lib/citizenship-workflow.ts +++ b/shared/lib/citizenship-workflow.ts @@ -146,8 +146,9 @@ export async function hasPendingApplication( * Get all Tiki roles for a user */ // Tiki enum mapping from pallet-tiki +// IMPORTANT: Must match exact order in /Pezkuwi-SDK/pezkuwi/pallets/tiki/src/lib.rs const TIKI_ROLES = [ - 'Hemwelatî', 'Parlementer', 'SerokiMeclise', 'Serok', 'Wezir', 'EndameDiwane', 'Dadger', + 'Welati', 'Parlementer', 'SerokiMeclise', 'Serok', 'Wezir', 'EndameDiwane', 'Dadger', 'Dozger', 'Hiquqnas', 'Noter', 'Xezinedar', 'Bacgir', 'GerinendeyeCavkaniye', 'OperatorêTorê', 'PisporêEwlehiyaSîber', 'GerinendeyeDaneye', 'Berdevk', 'Qeydkar', 'Balyoz', 'Navbeynkar', 'ParêzvaneÇandî', 'Mufetîs', 'KalîteKontrolker', 'Mela', 'Feqî', 'Perwerdekar', 'Rewsenbîr', @@ -188,7 +189,7 @@ export async function getUserTikis( /** * Check if user has Welati (Citizen) Tiki - * Backend checks for "Hemwelatî" (actual blockchain role name) + * Blockchain uses "Welati" as the actual role name */ export async function hasCitizenTiki( api: ApiPromise, @@ -198,7 +199,6 @@ export async function hasCitizenTiki( const tikis = await getUserTikis(api, address); const citizenTiki = tikis.find(t => - t.role.toLowerCase() === 'hemwelatî' || t.role.toLowerCase() === 'welati' || t.role.toLowerCase() === 'citizen' ); @@ -227,7 +227,6 @@ export async function verifyNftOwnership( return tikis.some(tiki => tiki.id === nftNumber && ( - tiki.role.toLowerCase() === 'hemwelatî' || tiki.role.toLowerCase() === 'welati' || tiki.role.toLowerCase() === 'citizen' ) @@ -623,40 +622,128 @@ export function subscribeToKycApproval( export const FOUNDER_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; // Satoshi Qazi Muhammed +export interface AuthChallenge { + message: string; + nonce: string; + timestamp: number; +} + /** * Generate authentication challenge for existing citizens */ -export function generateAuthChallenge(tikiNumber: string): string { +export function generateAuthChallenge(tikiNumber: string): AuthChallenge { const timestamp = Date.now(); - return `pezkuwi-auth-${tikiNumber}-${timestamp}`; + const nonce = Math.random().toString(36).substring(2, 15); + const message = `Sign this message to prove you own Citizen #${tikiNumber}`; + + return { + message, + nonce: `pezkuwi-auth-${tikiNumber}-${timestamp}-${nonce}`, + timestamp + }; } /** * Sign challenge with user's account */ -export async function signChallenge(challenge: string, signer: any): Promise { - // This would use Polkadot.js signing - // For now, return placeholder - return `signed-${challenge}`; +export async function signChallenge( + account: InjectedAccountWithMeta, + challenge: AuthChallenge +): Promise { + try { + const injector = await web3FromAddress(account.address); + + if (!injector?.signer?.signRaw) { + throw new Error('Signer not available'); + } + + // Sign the challenge nonce + const signResult = await injector.signer.signRaw({ + address: account.address, + data: challenge.nonce, + type: 'bytes' + }); + + return signResult.signature; + } catch (error) { + console.error('Failed to sign challenge:', error); + throw error; + } } /** - * Verify signature + * Verify signature (simplified - in production, verify on backend) */ -export function verifySignature(challenge: string, signature: string, address: string): boolean { - // Implement signature verification - return true; +export async function verifySignature( + signature: string, + challenge: AuthChallenge, + address: string +): Promise { + try { + // For now, just check that signature exists and is valid hex + // In production, you would verify the signature cryptographically + if (!signature || signature.length < 10) { + return false; + } + + // Basic validation: signature should be hex string starting with 0x + const isValidHex = /^0x[0-9a-fA-F]+$/.test(signature); + + return isValidHex; + } catch (error) { + console.error('Signature verification error:', error); + return false; + } +} + +export interface CitizenSession { + tikiNumber: string; + walletAddress: string; + sessionToken: string; + lastAuthenticated: number; + expiresAt: number; } /** - * Save citizen session + * Save citizen session (new format) */ -export function saveCitizenSession(tikiNumber: string, address: string): void { - localStorage.setItem('pezkuwi_citizen_session', JSON.stringify({ - tikiNumber, - address, - timestamp: Date.now() - })); +export function saveCitizenSession(tikiNumber: string, address: string): void; +export function saveCitizenSession(session: CitizenSession): void; +export function saveCitizenSession(tikiNumberOrSession: string | CitizenSession, address?: string): void { + if (typeof tikiNumberOrSession === 'string') { + // Old format for backward compatibility + localStorage.setItem('pezkuwi_citizen_session', JSON.stringify({ + tikiNumber: tikiNumberOrSession, + address, + timestamp: Date.now() + })); + } else { + // New format with full session data + localStorage.setItem('pezkuwi_citizen_session', JSON.stringify(tikiNumberOrSession)); + } +} + +/** + * Get citizen session + */ +export async function getCitizenSession(): Promise { + try { + const sessionData = localStorage.getItem('pezkuwi_citizen_session'); + if (!sessionData) return null; + + const session = JSON.parse(sessionData); + + // Check if it's the new format with expiresAt + if (session.expiresAt) { + return session as CitizenSession; + } + + // Old format - return null to force re-authentication + return null; + } catch (error) { + console.error('Error retrieving citizen session:', error); + return null; + } } /** diff --git a/shared/lib/kyc.ts b/shared/lib/kyc.ts new file mode 100644 index 00000000..e03b66b4 --- /dev/null +++ b/shared/lib/kyc.ts @@ -0,0 +1,4 @@ +/** + * KYC utilities - re-exports from citizenship-workflow + */ +export { getKycStatus } from './citizenship-workflow'; diff --git a/shared/lib/tiki.ts b/shared/lib/tiki.ts index fa77e60c..3b18ef86 100644 --- a/shared/lib/tiki.ts +++ b/shared/lib/tiki.ts @@ -9,9 +9,10 @@ import type { ApiPromise } from '@polkadot/api'; // ======================================== // TIKI TYPES (from Rust enum) // ======================================== +// IMPORTANT: Must match /Pezkuwi-SDK/pezkuwi/pallets/tiki/src/lib.rs export enum Tiki { // Otomatik - KYC sonrası - Hemwelatî = 'Hemwelatî', + Welati = 'Welati', // Seçilen roller (Elected) Parlementer = 'Parlementer', @@ -81,7 +82,7 @@ export enum RoleAssignmentType { // Tiki to Display Name mapping (English) export const TIKI_DISPLAY_NAMES: Record = { - Hemwelatî: 'Citizen', + Welati: 'Citizen', Parlementer: 'Parliament Member', SerokiMeclise: 'Speaker of Parliament', Serok: 'President', @@ -171,7 +172,7 @@ export const TIKI_SCORES: Record = { Qeydkar: 25, ParêzvaneÇandî: 25, Sêwirmend: 20, - Hemwelatî: 10, + Welati: 10, Pêseng: 5, // Default for unlisted }; @@ -191,7 +192,7 @@ export const ROLE_CATEGORIES: Record = { Economic: ['Bazargan'], Leadership: ['RêveberêProjeyê', 'Pêseng'], Quality: ['KalîteKontrolker'], - Citizen: ['Hemwelatî'], + Citizen: ['Welati'], }; // ======================================== @@ -241,7 +242,7 @@ export const fetchUserTikis = async ( }; /** - * Check if user is a citizen (has Hemwelatî tiki) + * Check if user is a citizen (has Welati tiki) * @param api - Polkadot API instance * @param address - User's substrate address * @returns boolean @@ -397,3 +398,282 @@ export const getTikiBadgeVariant = (tiki: string): 'default' | 'secondary' | 'de if (score >= 70) return 'secondary'; // Gray for mid ranks return 'outline'; // Outline for low ranks }; + +// ======================================== +// NFT DETAILS FUNCTIONS +// ======================================== + +/** + * Tiki NFT Details interface + */ +export interface TikiNFTDetails { + collectionId: number; + itemId: number; + owner: string; + tikiRole: string; + tikiDisplayName: string; + tikiScore: number; + tikiColor: string; + tikiEmoji: string; + mintedAt?: number; + metadata?: any; +} + +/** + * Fetch detailed NFT information for a user's tiki roles + * @param api - Polkadot API instance + * @param address - User's substrate address + * @returns Array of TikiNFTDetails + */ +export const fetchUserTikiNFTs = async ( + api: ApiPromise, + address: string +): Promise => { + try { + if (!api || !api.query.tiki) { + console.warn('Tiki pallet not available on this chain'); + return []; + } + + // Query UserTikis storage - returns list of role enums + const userTikis = await api.query.tiki.userTikis(address); + + if (!userTikis || userTikis.isEmpty) { + return []; + } + + const tikisArray = userTikis.toJSON() as string[]; + const nftDetails: TikiNFTDetails[] = []; + + // UserTikis doesn't store NFT IDs, only roles + // We return role information here but without actual NFT collection/item IDs + for (const tikiRole of tikisArray) { + nftDetails.push({ + collectionId: 42, // Tiki collection is always 42 + itemId: 0, // We don't have individual item IDs from UserTikis storage + owner: address, + tikiRole, + tikiDisplayName: getTikiDisplayName(tikiRole), + tikiScore: TIKI_SCORES[tikiRole] || 5, + tikiColor: getTikiColor(tikiRole), + tikiEmoji: getTikiEmoji(tikiRole), + metadata: null + }); + } + + return nftDetails; + + } catch (error) { + console.error('Error fetching user tiki NFTs:', error); + return []; + } +}; + +/** + * Fetch citizen NFT details for a user + * @param api - Polkadot API instance + * @param address - User's substrate address + * @returns TikiNFTDetails for citizen NFT or null + */ +export const getCitizenNFTDetails = async ( + api: ApiPromise, + address: string +): Promise => { + try { + if (!api || !api.query.tiki) { + return null; + } + + // Query CitizenNft storage - returns only item ID (u32) + const citizenNft = await api.query.tiki.citizenNft(address); + + if (citizenNft.isEmpty) { + return null; + } + + // CitizenNft returns just the item ID (u32), collection is always 42 + const itemId = citizenNft.toJSON() as number; + const collectionId = 42; // Tiki collection is hardcoded as 42 + + if (typeof itemId !== 'number') { + return null; + } + + // Try to fetch metadata + let metadata: any = null; + try { + const nftMetadata = await api.query.nfts.item(collectionId, itemId); + if (nftMetadata && !nftMetadata.isEmpty) { + metadata = nftMetadata.toJSON(); + } + } catch (e) { + console.warn('Could not fetch citizen NFT metadata:', e); + } + + return { + collectionId, + itemId, + owner: address, + tikiRole: 'Welati', + tikiDisplayName: getTikiDisplayName('Welati'), + tikiScore: TIKI_SCORES['Welati'] || 10, + tikiColor: getTikiColor('Welati'), + tikiEmoji: getTikiEmoji('Welati'), + metadata + }; + + } catch (error) { + console.error('Error fetching citizen NFT details:', error); + return null; + } +}; + +/** + * Fetch all NFT details including collection and item IDs + * @param api - Polkadot API instance + * @param address - User's substrate address + * @returns Complete NFT details with collection/item IDs + */ +export const getAllTikiNFTDetails = async ( + api: ApiPromise, + address: string +): Promise<{ + citizenNFT: TikiNFTDetails | null; + roleNFTs: TikiNFTDetails[]; + totalNFTs: number; +}> => { + try { + // Only fetch citizen NFT because it's the only one with stored item ID + // Role assignments in UserTikis don't have associated NFT item IDs + const citizenNFT = await getCitizenNFTDetails(api, address); + + return { + citizenNFT, + roleNFTs: [], // Don't show role NFTs because UserTikis doesn't store item IDs + totalNFTs: citizenNFT ? 1 : 0 + }; + + } catch (error) { + console.error('Error fetching all tiki NFT details:', error); + return { + citizenNFT: null, + roleNFTs: [], + totalNFTs: 0 + }; + } +}; + +/** + * Generates a deterministic 6-digit Citizen Number + * Formula: Based on owner address + collection ID + item ID + * Always returns the same number for the same inputs (deterministic) + */ +export const generateCitizenNumber = ( + ownerAddress: string, + collectionId: number, + itemId: number +): string => { + // Create a simple hash from the inputs + let hash = 0; + + // Hash the address + for (let i = 0; i < ownerAddress.length; i++) { + hash = ((hash << 5) - hash) + ownerAddress.charCodeAt(i); + hash = hash & hash; // Convert to 32bit integer + } + + // Add collection ID and item ID to the hash + hash += collectionId * 1000 + itemId; + + // Ensure positive number + hash = Math.abs(hash); + + // Get last 6 digits and pad with zeros if needed + const sixDigit = (hash % 1000000).toString().padStart(6, '0'); + + return sixDigit; +}; + +/** + * Verifies Citizen Number by checking if it matches the user's NFT data + * Format: #collectionId-itemId-6digitNumber + * Example: #42-0-123456 + */ +export const verifyCitizenNumber = async ( + api: any, + citizenNumber: string, + walletAddress: string +): Promise => { + try { + console.log('🔍 Verifying Citizen Number...'); + console.log(' Input:', citizenNumber); + console.log(' Wallet:', walletAddress); + + // Parse citizen number: #42-0-123456 + const cleanNumber = citizenNumber.trim().replace('#', ''); + const parts = cleanNumber.split('-'); + console.log(' Parsed parts:', parts); + + if (parts.length !== 3) { + console.error('❌ Invalid citizen number format. Expected: #collectionId-itemId-6digits'); + return false; + } + + const collectionId = parseInt(parts[0]); + const itemId = parseInt(parts[1]); + const providedSixDigit = parts[2]; + console.log(' Collection ID:', collectionId); + console.log(' Item ID:', itemId); + console.log(' Provided 6-digit:', providedSixDigit); + + // Validate parts + if (isNaN(collectionId) || isNaN(itemId) || providedSixDigit.length !== 6) { + console.error('❌ Invalid citizen number format'); + return false; + } + + // Get user's NFT data from blockchain + console.log(' Querying blockchain for wallet:', walletAddress); + const itemIdResult = await api.query.tiki.citizenNft(walletAddress); + console.log(' Blockchain query result:', itemIdResult.toString()); + console.log(' Blockchain query result (JSON):', itemIdResult.toJSON()); + + if (itemIdResult.isEmpty) { + console.error('❌ No citizen NFT found for this address'); + return false; + } + + // Handle Option type - check if it's Some or None + const actualItemId = itemIdResult.isSome ? itemIdResult.unwrap().toNumber() : null; + + if (actualItemId === null) { + console.error('❌ No citizen NFT found for this address (None value)'); + return false; + } + + console.log(' Actual Item ID from blockchain:', actualItemId); + + // Check if collection and item IDs match + if (collectionId !== 42 || itemId !== actualItemId) { + console.error(`❌ NFT mismatch. Provided: #${collectionId}-${itemId}, Blockchain has: #42-${actualItemId}`); + return false; + } + + // Generate expected citizen number + const expectedSixDigit = generateCitizenNumber(walletAddress, collectionId, itemId); + console.log(' Expected 6-digit:', expectedSixDigit); + console.log(' Provided 6-digit:', providedSixDigit); + + // Compare provided vs expected + if (providedSixDigit !== expectedSixDigit) { + console.error(`❌ Citizen number mismatch. Expected: ${expectedSixDigit}, Got: ${providedSixDigit}`); + return false; + } + + console.log('✅ Citizen Number verified successfully!'); + return true; + } catch (error) { + console.error('❌ Error verifying citizen number:', error); + return false; + } +}; diff --git a/shared/utils/dex.ts b/shared/utils/dex.ts index 10b141d6..e4ae4864 100644 --- a/shared/utils/dex.ts +++ b/shared/utils/dex.ts @@ -1,5 +1,5 @@ import { ApiPromise } from '@polkadot/api'; -import { KNOWN_TOKENS, PoolInfo, SwapQuote } from '@pezkuwi/types/dex'; +import { KNOWN_TOKENS, PoolInfo, SwapQuote, UserLiquidityPosition } from '@pezkuwi/types/dex'; /** * Format balance with proper decimals @@ -168,6 +168,48 @@ export const fetchPools = async (api: ApiPromise): Promise => { const reserve1 = reserve1Data.isSome ? reserve1Data.unwrap().balance.toString() : '0'; const reserve2 = reserve2Data.isSome ? reserve2Data.unwrap().balance.toString() : '0'; + // Get LP token supply + // Substrate's asset-conversion pallet creates LP tokens using poolAssets pallet + // The LP token ID can be derived from the pool's asset pair + // Try to query using poolAssets first, fallback to calculating total from reserves + let lpTokenSupply = '0'; + try { + // First attempt: Use poolAssets if available + if (api.query.poolAssets && api.query.poolAssets.asset) { + // LP token ID in poolAssets is typically the pool pair encoded + // Try a simple encoding: combine asset IDs + const lpTokenId = (asset1 << 16) | asset2; // Simple bit-shift encoding + const lpAssetDetails = await api.query.poolAssets.asset(lpTokenId); + if (lpAssetDetails.isSome) { + lpTokenSupply = lpAssetDetails.unwrap().supply.toString(); + } + } + + // Second attempt: Calculate from reserves using constant product formula + // LP supply ≈ sqrt(reserve1 * reserve2) for initial mint + // For existing pools, we'd need historical data + if (lpTokenSupply === '0' && BigInt(reserve1) > BigInt(0) && BigInt(reserve2) > BigInt(0)) { + // Simplified calculation: geometric mean of reserves + // This is an approximation - actual LP supply should be queried from chain + const r1 = BigInt(reserve1); + const r2 = BigInt(reserve2); + const product = r1 * r2; + + // Integer square root approximation + let sqrt = BigInt(1); + let prev = BigInt(0); + while (sqrt !== prev) { + prev = sqrt; + sqrt = (sqrt + product / sqrt) / BigInt(2); + } + + lpTokenSupply = sqrt.toString(); + } + } catch (error) { + console.warn('Could not query LP token supply:', error); + // Fallback to '0' is already set + } + // Get token info const token1 = KNOWN_TOKENS[asset1] || { id: asset1, @@ -192,7 +234,7 @@ export const fetchPools = async (api: ApiPromise): Promise => { asset2Decimals: token2.decimals, reserve1, reserve2, - lpTokenSupply: '0', // TODO: Query LP token supply + lpTokenSupply, feeRate: '0.3', // Default 0.3% }); } @@ -240,3 +282,291 @@ export const getTokenSymbol = (assetId: number): string => { export const getTokenDecimals = (assetId: number): number => { return KNOWN_TOKENS[assetId]?.decimals || 12; }; + +/** + * Calculate TVL (Total Value Locked) for a pool + * @param reserve1 - Reserve of first token + * @param reserve2 - Reserve of second token + * @param decimals1 - Decimals of first token + * @param decimals2 - Decimals of second token + * @param price1USD - Price of first token in USD (optional) + * @param price2USD - Price of second token in USD (optional) + * @returns TVL in USD as string, or reserves sum if prices not available + */ +export const calculatePoolTVL = ( + reserve1: string, + reserve2: string, + decimals1: number = 12, + decimals2: number = 12, + price1USD?: number, + price2USD?: number +): string => { + try { + const r1 = BigInt(reserve1); + const r2 = BigInt(reserve2); + + if (price1USD && price2USD) { + // Convert reserves to human-readable amounts + const amount1 = Number(r1) / Math.pow(10, decimals1); + const amount2 = Number(r2) / Math.pow(10, decimals2); + + // Calculate USD value + const value1 = amount1 * price1USD; + const value2 = amount2 * price2USD; + const totalTVL = value1 + value2; + + return totalTVL.toFixed(2); + } + + // Fallback: return sum of reserves (not USD value) + // This is useful for display even without price data + const total = r1 + r2; + return formatTokenBalance(total.toString(), decimals1, 2); + } catch (error) { + console.error('Error calculating TVL:', error); + return '0'; + } +}; + +/** + * Calculate APR (Annual Percentage Rate) for a pool + * @param feesEarned24h - Fees earned in last 24 hours (in smallest unit) + * @param totalLiquidity - Total liquidity in pool (in smallest unit) + * @param decimals - Token decimals + * @returns APR as percentage string + */ +export const calculatePoolAPR = ( + feesEarned24h: string, + totalLiquidity: string, + decimals: number = 12 +): string => { + try { + const fees24h = BigInt(feesEarned24h); + const liquidity = BigInt(totalLiquidity); + + if (liquidity === BigInt(0)) { + return '0.00'; + } + + // Daily rate = fees24h / totalLiquidity + // APR = daily rate * 365 * 100 (for percentage) + const dailyRate = (fees24h * BigInt(100000)) / liquidity; // Multiply by 100000 for precision + const apr = (dailyRate * BigInt(365)) / BigInt(1000); // Divide by 1000 to get percentage + + return (Number(apr) / 100).toFixed(2); + } catch (error) { + console.error('Error calculating APR:', error); + return '0.00'; + } +}; + +/** + * Find best swap route using multi-hop + * @param api - Polkadot API instance + * @param assetIn - Input asset ID + * @param assetOut - Output asset ID + * @param amountIn - Amount to swap in + * @returns Best swap route with quote + */ +export const findBestSwapRoute = async ( + api: ApiPromise, + assetIn: number, + assetOut: number, + amountIn: string +): Promise => { + try { + // Get all available pools + const pools = await fetchPools(api); + + // Direct swap path + const directPool = pools.find( + (p) => + (p.asset1 === assetIn && p.asset2 === assetOut) || + (p.asset1 === assetOut && p.asset2 === assetIn) + ); + + let bestQuote: SwapQuote = { + amountIn, + amountOut: '0', + path: [assetIn, assetOut], + priceImpact: '0', + minimumReceived: '0', + route: `${getTokenSymbol(assetIn)} → ${getTokenSymbol(assetOut)}`, + }; + + // Try direct swap + if (directPool) { + const isForward = directPool.asset1 === assetIn; + const reserveIn = isForward ? directPool.reserve1 : directPool.reserve2; + const reserveOut = isForward ? directPool.reserve2 : directPool.reserve1; + + const amountOut = getAmountOut(amountIn, reserveIn, reserveOut); + const priceImpact = calculatePriceImpact(reserveIn, reserveOut, amountIn); + const minimumReceived = calculateMinAmount(amountOut, 1); // 1% slippage + + bestQuote = { + amountIn, + amountOut, + path: [assetIn, assetOut], + priceImpact, + minimumReceived, + route: `${getTokenSymbol(assetIn)} → ${getTokenSymbol(assetOut)}`, + }; + } + + // Try multi-hop routes (through intermediate tokens) + // Common intermediate tokens: wHEZ (0), PEZ (1), wUSDT (2) + const intermediateTokens = [0, 1, 2].filter( + (id) => id !== assetIn && id !== assetOut + ); + + for (const intermediate of intermediateTokens) { + try { + // Find first hop pool + const pool1 = pools.find( + (p) => + (p.asset1 === assetIn && p.asset2 === intermediate) || + (p.asset1 === intermediate && p.asset2 === assetIn) + ); + + // Find second hop pool + const pool2 = pools.find( + (p) => + (p.asset1 === intermediate && p.asset2 === assetOut) || + (p.asset1 === assetOut && p.asset2 === intermediate) + ); + + if (!pool1 || !pool2) continue; + + // Calculate first hop + const isForward1 = pool1.asset1 === assetIn; + const reserveIn1 = isForward1 ? pool1.reserve1 : pool1.reserve2; + const reserveOut1 = isForward1 ? pool1.reserve2 : pool1.reserve1; + const amountIntermediate = getAmountOut(amountIn, reserveIn1, reserveOut1); + + // Calculate second hop + const isForward2 = pool2.asset1 === intermediate; + const reserveIn2 = isForward2 ? pool2.reserve1 : pool2.reserve2; + const reserveOut2 = isForward2 ? pool2.reserve2 : pool2.reserve1; + const amountOut = getAmountOut(amountIntermediate, reserveIn2, reserveOut2); + + // Calculate combined price impact + const impact1 = calculatePriceImpact(reserveIn1, reserveOut1, amountIn); + const impact2 = calculatePriceImpact( + reserveIn2, + reserveOut2, + amountIntermediate + ); + const totalImpact = ( + parseFloat(impact1) + parseFloat(impact2) + ).toFixed(2); + + // If this route gives better output, use it + if (BigInt(amountOut) > BigInt(bestQuote.amountOut)) { + const minimumReceived = calculateMinAmount(amountOut, 1); + bestQuote = { + amountIn, + amountOut, + path: [assetIn, intermediate, assetOut], + priceImpact: totalImpact, + minimumReceived, + route: `${getTokenSymbol(assetIn)} → ${getTokenSymbol(intermediate)} → ${getTokenSymbol(assetOut)}`, + }; + } + } catch (error) { + console.warn(`Error calculating route through ${intermediate}:`, error); + continue; + } + } + + return bestQuote; + } catch (error) { + console.error('Error finding best swap route:', error); + return { + amountIn, + amountOut: '0', + path: [assetIn, assetOut], + priceImpact: '0', + minimumReceived: '0', + route: 'Error', + }; + } +}; + +/** + * Fetch user's LP token positions across all pools + * @param api - Polkadot API instance + * @param userAddress - User's wallet address + */ +export const fetchUserLPPositions = async ( + api: ApiPromise, + userAddress: string +): Promise => { + try { + const positions: UserLiquidityPosition[] = []; + + // First, get all available pools + const pools = await fetchPools(api); + + for (const pool of pools) { + try { + // Try to find LP token balance for this pool + let lpTokenBalance = '0'; + + // Method 1: Check poolAssets pallet + if (api.query.poolAssets && api.query.poolAssets.account) { + const lpTokenId = (pool.asset1 << 16) | pool.asset2; + const lpAccount = await api.query.poolAssets.account(lpTokenId, userAddress); + if (lpAccount.isSome) { + lpTokenBalance = lpAccount.unwrap().balance.toString(); + } + } + + // Skip if user has no LP tokens for this pool + if (lpTokenBalance === '0' || BigInt(lpTokenBalance) === BigInt(0)) { + continue; + } + + // Calculate user's share of the pool + const lpSupply = BigInt(pool.lpTokenSupply); + const userLPBig = BigInt(lpTokenBalance); + + if (lpSupply === BigInt(0)) { + continue; // Avoid division by zero + } + + // Share percentage: (userLP / totalLP) * 100 + const sharePercentage = (userLPBig * BigInt(10000)) / lpSupply; // Multiply by 10000 for precision + const shareOfPool = (Number(sharePercentage) / 100).toFixed(2); + + // Calculate underlying asset amounts + const reserve1Big = BigInt(pool.reserve1); + const reserve2Big = BigInt(pool.reserve2); + + const asset1Amount = ((reserve1Big * userLPBig) / lpSupply).toString(); + const asset2Amount = ((reserve2Big * userLPBig) / lpSupply).toString(); + + positions.push({ + poolId: pool.id, + asset1: pool.asset1, + asset2: pool.asset2, + lpTokenBalance, + shareOfPool, + asset1Amount, + asset2Amount, + // These will be calculated separately if needed + valueUSD: undefined, + feesEarned: undefined, + }); + } catch (error) { + console.warn(`Error fetching LP position for pool ${pool.id}:`, error); + // Continue with next pool + } + } + + return positions; + } catch (error) { + console.error('Failed to fetch user LP positions:', error); + return []; + } +}; diff --git a/web/public/shared/digital_citizen_card.png b/web/public/shared/digital_citizen_card.png new file mode 100644 index 00000000..b94c5049 Binary files /dev/null and b/web/public/shared/digital_citizen_card.png differ diff --git a/web/public/shared/qa_dashboard.png b/web/public/shared/qa_dashboard.png new file mode 100644 index 00000000..2e65f60a Binary files /dev/null and b/web/public/shared/qa_dashboard.png differ diff --git a/web/public/shared/qa_governance.jpg b/web/public/shared/qa_governance.jpg new file mode 100644 index 00000000..b6e8f9ab Binary files /dev/null and b/web/public/shared/qa_governance.jpg differ diff --git a/web/src/App.tsx b/web/src/App.tsx index 25d3d478..e1963249 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,15 +10,20 @@ import AdminPanel from '@/pages/AdminPanel'; import WalletDashboard from './pages/WalletDashboard'; import ReservesDashboardPage from './pages/ReservesDashboardPage'; import BeCitizen from './pages/BeCitizen'; +import Citizens from './pages/Citizens'; +import CitizensIssues from './pages/citizens/CitizensIssues'; +import GovernmentEntrance from './pages/citizens/GovernmentEntrance'; import Elections from './pages/Elections'; import EducationPlatform from './pages/EducationPlatform'; import P2PPlatform from './pages/P2PPlatform'; +import { DEXDashboard } from './components/dex/DEXDashboard'; import { AppProvider } from '@/contexts/AppContext'; import { PolkadotProvider } from '@/contexts/PolkadotContext'; import { WalletProvider } from '@/contexts/WalletContext'; import { WebSocketProvider } from '@/contexts/WebSocketContext'; import { IdentityProvider } from '@/contexts/IdentityContext'; import { AuthProvider } from '@/contexts/AuthContext'; +import { DashboardProvider } from '@/contexts/DashboardContext'; import { ProtectedRoute } from '@/components/ProtectedRoute'; import NotFound from '@/pages/NotFound'; import { Toaster } from '@/components/ui/toaster'; @@ -36,14 +41,18 @@ function App() { - - + + + } /> } /> } /> } /> } /> + } /> + } /> + } /> @@ -84,9 +93,15 @@ function App() { } /> + + + + } /> } /> - + + diff --git a/web/src/components/AppLayout.tsx b/web/src/components/AppLayout.tsx index c9823794..aba4637d 100644 --- a/web/src/components/AppLayout.tsx +++ b/web/src/components/AppLayout.tsx @@ -21,7 +21,7 @@ import { TreasuryOverview } from './treasury/TreasuryOverview'; import { FundingProposal } from './treasury/FundingProposal'; import { SpendingHistory } from './treasury/SpendingHistory'; import { MultiSigApproval } from './treasury/MultiSigApproval'; -import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat, Users, Droplet } from 'lucide-react'; +import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat, Users, Droplet, Mail } from 'lucide-react'; import GovernanceInterface from './GovernanceInterface'; import RewardDistribution from './RewardDistribution'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -293,17 +293,29 @@ const AppLayout: React.FC = () => {
{/* Conditional Rendering for Features */} {showDEX ? ( - +
+
+ +
+
) : showProposalWizard ? ( - { - console.log('Proposal created:', proposal); - setShowProposalWizard(false); - }} - onCancel={() => setShowProposalWizard(false)} - /> +
+
+ { + console.log('Proposal created:', proposal); + setShowProposalWizard(false); + }} + onCancel={() => setShowProposalWizard(false)} + /> +
+
) : showDelegation ? ( - +
+
+ +
+
) : showForum ? (
@@ -454,87 +466,97 @@ const AppLayout: React.FC = () => { {/* Footer */}