mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 02:07:55 +00:00
Centralize common code in shared folder
This commit reorganizes the codebase to eliminate duplication between web and mobile frontends by moving all commonly used files to the shared folder. Changes: - Moved lib files to shared/lib/: * wallet.ts, staking.ts, tiki.ts, identity.ts * multisig.ts, usdt.ts, scores.ts, citizenship-workflow.ts - Moved utils to shared/utils/: * auth.ts, dex.ts * Created format.ts (extracted formatNumber from web utils) - Created shared/theme/: * colors.ts (Kurdistan and App color definitions) - Updated web configuration: * Added @pezkuwi/* path aliases in tsconfig.json and vite.config.ts * Updated all imports to use @pezkuwi/lib/*, @pezkuwi/utils/*, @pezkuwi/theme/* * Removed duplicate files from web/src/lib and web/src/utils - Updated mobile configuration: * Added @pezkuwi/* path aliases in tsconfig.json * Updated theme/colors.ts to re-export from shared * Mobile already uses relative imports to shared (no changes needed) Architecture Benefits: - Single source of truth for common code - No duplication between frontends - Easier maintenance and consistency - Clear separation of shared vs platform-specific code Web-specific files kept: - web/src/lib/supabase.ts - web/src/lib/utils.ts (cn function for Tailwind, re-exports formatNumber from shared) All imports updated and tested. Both web and mobile now use the centralized shared folder.
This commit is contained in:
@@ -1,25 +1,6 @@
|
||||
// Kurdistan Flag Colors
|
||||
export const KurdistanColors = {
|
||||
kesk: '#00A94F', // Green - Primary
|
||||
sor: '#EE2A35', // Red - Accent
|
||||
zer: '#FFD700', // Gold - Secondary
|
||||
spi: '#FFFFFF', // White - Background
|
||||
reş: '#000000', // Black - Text
|
||||
};
|
||||
|
||||
export const AppColors = {
|
||||
primary: KurdistanColors.kesk,
|
||||
secondary: KurdistanColors.zer,
|
||||
accent: KurdistanColors.sor,
|
||||
background: '#F5F5F5',
|
||||
surface: KurdistanColors.spi,
|
||||
text: KurdistanColors.reş,
|
||||
textSecondary: '#666666',
|
||||
border: '#E0E0E0',
|
||||
error: KurdistanColors.sor,
|
||||
success: KurdistanColors.kesk,
|
||||
warning: KurdistanColors.zer,
|
||||
info: '#2196F3',
|
||||
};
|
||||
|
||||
export default AppColors;
|
||||
/**
|
||||
* Re-export colors from shared theme
|
||||
* All color definitions are centralized in shared/theme/colors.ts
|
||||
*/
|
||||
export { KurdistanColors, AppColors } from '../../../shared/theme/colors';
|
||||
export { AppColors as default } from '../../../shared/theme/colors';
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@pezkuwi/lib/*": ["../shared/lib/*"],
|
||||
"@pezkuwi/utils/*": ["../shared/utils/*"],
|
||||
"@pezkuwi/theme/*": ["../shared/theme/*"],
|
||||
"@pezkuwi/types/*": ["../shared/types/*"],
|
||||
"@pezkuwi/i18n": ["../shared/i18n"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Shared theme colors for all platforms
|
||||
*/
|
||||
|
||||
// Kurdistan Flag Colors
|
||||
export const KurdistanColors = {
|
||||
kesk: '#00A94F', // Green - Primary
|
||||
sor: '#EE2A35', // Red - Accent
|
||||
zer: '#FFD700', // Gold - Secondary
|
||||
spi: '#FFFFFF', // White - Background
|
||||
reş: '#000000', // Black - Text
|
||||
};
|
||||
|
||||
// Application color palette
|
||||
export const AppColors = {
|
||||
primary: KurdistanColors.kesk,
|
||||
secondary: KurdistanColors.zer,
|
||||
accent: KurdistanColors.sor,
|
||||
background: '#F5F5F5',
|
||||
surface: KurdistanColors.spi,
|
||||
text: KurdistanColors.reş,
|
||||
textSecondary: '#666666',
|
||||
border: '#E0E0E0',
|
||||
error: KurdistanColors.sor,
|
||||
success: KurdistanColors.kesk,
|
||||
warning: KurdistanColors.zer,
|
||||
info: '#2196F3',
|
||||
};
|
||||
|
||||
export default AppColors;
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Shared formatting utilities
|
||||
* Platform-agnostic formatters for numbers, currency, etc.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a number with K, M, B suffixes for large values
|
||||
* @param value - The number to format
|
||||
* @param decimals - Number of decimal places (default 2)
|
||||
* @returns Formatted string
|
||||
*/
|
||||
export function formatNumber(value: number, decimals: number = 2): string {
|
||||
if (value === 0) return '0';
|
||||
if (value < 0.01) return '<0.01';
|
||||
|
||||
// For large numbers, use K, M, B suffixes
|
||||
if (value >= 1e9) {
|
||||
return (value / 1e9).toFixed(decimals) + 'B';
|
||||
}
|
||||
if (value >= 1e6) {
|
||||
return (value / 1e6).toFixed(decimals) + 'M';
|
||||
}
|
||||
if (value >= 1e3) {
|
||||
return (value / 1e3).toFixed(decimals) + 'K';
|
||||
}
|
||||
|
||||
return value.toFixed(decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a percentage value
|
||||
* @param value - The percentage value (e.g., 0.15 for 15%)
|
||||
* @param decimals - Number of decimal places (default 2)
|
||||
* @returns Formatted percentage string
|
||||
*/
|
||||
export function formatPercentage(value: number, decimals: number = 2): string {
|
||||
return `${(value * 100).toFixed(decimals)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a currency value
|
||||
* @param value - The currency value
|
||||
* @param symbol - Currency symbol (default '$')
|
||||
* @param decimals - Number of decimal places (default 2)
|
||||
* @returns Formatted currency string
|
||||
*/
|
||||
export function formatCurrency(
|
||||
value: number,
|
||||
symbol: string = '$',
|
||||
decimals: number = 2
|
||||
): string {
|
||||
return `${symbol}${formatNumber(value, decimals)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a formatted number string back to number
|
||||
* @param formatted - Formatted string (e.g., "1.5K")
|
||||
* @returns Number value
|
||||
*/
|
||||
export function parseFormattedNumber(formatted: string): number {
|
||||
const cleaned = formatted.replace(/[^0-9.KMB]/g, '');
|
||||
const match = cleaned.match(/^([\d.]+)([KMB])?$/);
|
||||
|
||||
if (!match) return 0;
|
||||
|
||||
const [, numberPart, suffix] = match;
|
||||
let value = parseFloat(numberPart);
|
||||
|
||||
if (suffix === 'K') value *= 1e3;
|
||||
else if (suffix === 'M') value *= 1e6;
|
||||
else if (suffix === 'B') value *= 1e9;
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send, Shield, Users } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
import { AddTokenModal } from './AddTokenModal';
|
||||
import { TransferModal } from './TransferModal';
|
||||
import { getAllScores, type UserScores } from '@/lib/scores';
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
|
||||
interface TokenBalance {
|
||||
assetId: number;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
|
||||
interface AddLiquidityModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronRight, Shield } from 'lucide-react';
|
||||
import { usePolkadot } from '../contexts/PolkadotContext';
|
||||
import { formatBalance } from '../lib/wallet';
|
||||
import { formatBalance } from '@pezkuwi/lib/wallet';
|
||||
|
||||
const HeroSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
calculateMultisigAddress,
|
||||
USDT_MULTISIG_CONFIG,
|
||||
formatMultisigAddress,
|
||||
} from '@/lib/multisig';
|
||||
import { getTikiDisplayName, getTikiEmoji } from '@/lib/tiki';
|
||||
} from '@pezkuwi/lib/multisig';
|
||||
import { getTikiDisplayName, getTikiEmoji } from '@pezkuwi/lib/tiki';
|
||||
|
||||
interface MultisigMembersProps {
|
||||
specificAddresses?: Record<string, string>;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Award, Crown, Shield, Users } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { getUserTikis } from '@/lib/citizenship-workflow';
|
||||
import type { TikiInfo } from '@/lib/citizenship-workflow';
|
||||
import { getUserTikis } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import type { TikiInfo } from '@pezkuwi/lib/citizenship-workflow';
|
||||
|
||||
// Icon map for different Tiki roles
|
||||
const getTikiIcon = (role: string) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
import { AddLiquidityModal } from '@/components/AddLiquidityModal';
|
||||
import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
|
||||
// Helper to get display name for tokens (users see HEZ not wHEZ, USDT not wUSDT)
|
||||
const getDisplayTokenName = (assetId: number): string => {
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { getWUSDTTotalSupply, checkReserveHealth, formatWUSDT } from '@pezkuwi/lib/usdt';
|
||||
import { MultisigMembers } from './MultisigMembers';
|
||||
|
||||
interface ReservesDashboardProps {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { ASSET_IDS, formatBalance, parseAmount } from '@/lib/wallet';
|
||||
import { ASSET_IDS, formatBalance, parseAmount } from '@pezkuwi/lib/wallet';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { KurdistanSun } from './KurdistanSun';
|
||||
import { PriceChart } from './trading/PriceChart';
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
getWithdrawalTier,
|
||||
formatDelay,
|
||||
formatWUSDT,
|
||||
} from '@/lib/usdt';
|
||||
import { isMultisigMember } from '@/lib/multisig';
|
||||
} from '@pezkuwi/lib/usdt';
|
||||
import { isMultisigMember } from '@pezkuwi/lib/multisig';
|
||||
|
||||
interface USDTBridgeProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Loader2, CheckCircle, AlertTriangle, Shield } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { verifyNftOwnership } from '@/lib/citizenship-workflow';
|
||||
import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@/lib/citizenship-crypto';
|
||||
import type { AuthChallenge } from '@/lib/citizenship-crypto';
|
||||
import { verifyNftOwnership } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@pezkuwi/lib/citizenship-crypto';
|
||||
import type { AuthChallenge } from '@pezkuwi/lib/citizenship-crypto';
|
||||
|
||||
interface ExistingCitizenAuthProps {
|
||||
onClose: () => void;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Clock } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import type { CitizenshipData, Region, MaritalStatus } from '@/lib/citizenship-workflow';
|
||||
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@/lib/citizenship-workflow';
|
||||
import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@/lib/citizenship-crypto';
|
||||
import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@pezkuwi/lib/citizenship-crypto';
|
||||
|
||||
interface NewCitizenApplicationProps {
|
||||
onClose: () => void;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useWallet } from '@/contexts/WalletContext';
|
||||
import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { PoolInfo } from '@/types/dex';
|
||||
import { parseTokenInput, formatTokenBalance, quote } from '@/utils/dex';
|
||||
import { parseTokenInput, formatTokenBalance, quote } from '@pezkuwi/utils/dex';
|
||||
|
||||
interface AddLiquidityModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X, Plus, AlertCircle, Loader2, CheckCircle } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { KNOWN_TOKENS } from '@/types/dex';
|
||||
import { parseTokenInput, formatTokenBalance } from '@/utils/dex';
|
||||
import { parseTokenInput, formatTokenBalance } from '@pezkuwi/utils/dex';
|
||||
|
||||
interface CreatePoolModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -6,7 +6,7 @@ import PoolDashboard from '@/components/PoolDashboard';
|
||||
import { CreatePoolModal } from './CreatePoolModal';
|
||||
import { InitializeHezPoolModal } from './InitializeHezPoolModal';
|
||||
import { ArrowRightLeft, Droplet, Settings } from 'lucide-react';
|
||||
import { isFounderWallet } from '@/utils/auth';
|
||||
import { isFounderWallet } from '@pezkuwi/utils/auth';
|
||||
|
||||
export const DEXDashboard: React.FC = () => {
|
||||
const { account } = useWallet();
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TrendingUp, Droplet, BarChart3, Search, Plus } from 'lucide-react';
|
||||
import { PoolInfo } from '@/types/dex';
|
||||
import { fetchPools, formatTokenBalance } from '@/utils/dex';
|
||||
import { isFounderWallet } from '@/utils/auth';
|
||||
import { fetchPools, formatTokenBalance } from '@pezkuwi/utils/dex';
|
||||
import { isFounderWallet } from '@pezkuwi/utils/auth';
|
||||
|
||||
interface PoolBrowserProps {
|
||||
onAddLiquidity?: (pool: PoolInfo) => void;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useWallet } from '@/contexts/WalletContext';
|
||||
import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { PoolInfo } from '@/types/dex';
|
||||
import { formatTokenBalance } from '@/utils/dex';
|
||||
import { formatTokenBalance } from '@pezkuwi/utils/dex';
|
||||
|
||||
interface RemoveLiquidityModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
formatTokenBalance,
|
||||
getAmountOut,
|
||||
calculatePriceImpact,
|
||||
} from '@/utils/dex';
|
||||
} from '@pezkuwi/utils/dex';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface SwapInterfaceProps {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Progress } from '../ui/progress';
|
||||
import { usePolkadot } from '../../contexts/PolkadotContext';
|
||||
import { formatBalance } from '../../lib/wallet';
|
||||
import { formatBalance } from '../@pezkuwi/lib/wallet';
|
||||
|
||||
interface GovernanceStats {
|
||||
activeProposals: number;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
getCurrentEra,
|
||||
parseAmount,
|
||||
type StakingInfo
|
||||
} from '@/lib/staking';
|
||||
} from '@pezkuwi/lib/staking';
|
||||
|
||||
export const StakingDashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { formatAddress, formatBalance } from '@/lib/wallet';
|
||||
import { formatAddress, formatBalance } from '@pezkuwi/lib/wallet';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export const WalletButton: React.FC = () => {
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { formatAddress } from '@/lib/wallet';
|
||||
import { getAllScores, type UserScores } from '@/lib/scores';
|
||||
import { formatAddress } from '@pezkuwi/lib/wallet';
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
|
||||
interface WalletModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
generateZKProof,
|
||||
DEFAULT_BADGES,
|
||||
ROLES
|
||||
} from '@/lib/identity';
|
||||
} from '@pezkuwi/lib/identity';
|
||||
|
||||
interface IdentityContextType {
|
||||
profile: IdentityProfile | null;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { usePolkadot } from './PolkadotContext';
|
||||
import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet';
|
||||
import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@pezkuwi/lib/wallet';
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
import type { Signer } from '@polkadot/api/types';
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
// ========================================
|
||||
// Citizenship Crypto Utilities
|
||||
// ========================================
|
||||
// Handles encryption, hashing, signatures for citizenship data
|
||||
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
import { stringToHex, hexToU8a, u8aToHex, stringToU8a } from '@polkadot/util';
|
||||
import { decodeAddress, signatureVerify, cryptoWaitReady } from '@polkadot/util-crypto';
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
import type { CitizenshipData } from './citizenship-workflow';
|
||||
|
||||
// ========================================
|
||||
// HASHING FUNCTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Generate SHA-256 hash from data
|
||||
*/
|
||||
export async function generateHash(data: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const dataBuffer = encoder.encode(data);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return `0x${hashHex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate commitment hash from citizenship data
|
||||
*/
|
||||
export async function generateCommitmentHash(
|
||||
data: CitizenshipData
|
||||
): Promise<string> {
|
||||
const dataString = JSON.stringify({
|
||||
fullName: data.fullName,
|
||||
fatherName: data.fatherName,
|
||||
grandfatherName: data.grandfatherName,
|
||||
motherName: data.motherName,
|
||||
tribe: data.tribe,
|
||||
maritalStatus: data.maritalStatus,
|
||||
childrenCount: data.childrenCount,
|
||||
children: data.children,
|
||||
region: data.region,
|
||||
email: data.email,
|
||||
profession: data.profession,
|
||||
referralCode: data.referralCode,
|
||||
walletAddress: data.walletAddress,
|
||||
timestamp: data.timestamp
|
||||
});
|
||||
|
||||
return generateHash(dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate nullifier hash (prevents double-registration)
|
||||
*/
|
||||
export async function generateNullifierHash(
|
||||
walletAddress: string,
|
||||
timestamp: number
|
||||
): Promise<string> {
|
||||
const nullifierData = `${walletAddress}-${timestamp}-nullifier`;
|
||||
return generateHash(nullifierData);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ENCRYPTION / DECRYPTION (AES-GCM)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Derive encryption key from wallet address
|
||||
* NOTE: For MVP, we use a deterministic key. For production, use proper key derivation
|
||||
*/
|
||||
async function deriveEncryptionKey(walletAddress: string): Promise<CryptoKey> {
|
||||
// Create a deterministic seed from wallet address
|
||||
const seed = await generateHash(walletAddress);
|
||||
|
||||
// Convert hex to ArrayBuffer
|
||||
const keyMaterial = hexToU8a(seed).slice(0, 32); // 256-bit key
|
||||
|
||||
// Import as AES-GCM key
|
||||
return crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt citizenship data
|
||||
*/
|
||||
export async function encryptData(
|
||||
data: CitizenshipData,
|
||||
walletAddress: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const key = await deriveEncryptionKey(walletAddress);
|
||||
|
||||
// Generate random IV (Initialization Vector)
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Encrypt data
|
||||
const encoder = new TextEncoder();
|
||||
const dataBuffer = encoder.encode(JSON.stringify(data));
|
||||
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
dataBuffer
|
||||
);
|
||||
|
||||
// Combine IV + encrypted data
|
||||
const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength);
|
||||
combined.set(iv, 0);
|
||||
combined.set(new Uint8Array(encryptedBuffer), iv.length);
|
||||
|
||||
// Convert to hex
|
||||
return u8aToHex(combined);
|
||||
} catch (error) {
|
||||
console.error('Encryption error:', error);
|
||||
throw new Error('Failed to encrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt citizenship data
|
||||
*/
|
||||
export async function decryptData(
|
||||
encryptedHex: string,
|
||||
walletAddress: string
|
||||
): Promise<CitizenshipData> {
|
||||
try {
|
||||
const key = await deriveEncryptionKey(walletAddress);
|
||||
|
||||
// Convert hex to Uint8Array
|
||||
const combined = hexToU8a(encryptedHex);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
const iv = combined.slice(0, 12);
|
||||
const encryptedData = combined.slice(12);
|
||||
|
||||
// Decrypt
|
||||
const decryptedBuffer = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encryptedData
|
||||
);
|
||||
|
||||
// Convert to string and parse JSON
|
||||
const decoder = new TextDecoder();
|
||||
const decryptedString = decoder.decode(decryptedBuffer);
|
||||
|
||||
return JSON.parse(decryptedString) as CitizenshipData;
|
||||
} catch (error) {
|
||||
console.error('Decryption error:', error);
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SIGNATURE GENERATION & VERIFICATION
|
||||
// ========================================
|
||||
|
||||
export interface AuthChallenge {
|
||||
nonce: string; // Random UUID
|
||||
timestamp: number; // Current timestamp
|
||||
tikiNumber: string; // NFT number to prove
|
||||
expiresAt: number; // Expiry timestamp (5 min)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate authentication challenge
|
||||
*/
|
||||
export function generateAuthChallenge(tikiNumber: string): AuthChallenge {
|
||||
const now = Date.now();
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
nonce,
|
||||
timestamp: now,
|
||||
tikiNumber,
|
||||
expiresAt: now + (5 * 60 * 1000) // 5 minutes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format challenge message for signing
|
||||
*/
|
||||
export function formatChallengeMessage(challenge: AuthChallenge): string {
|
||||
return `Prove ownership of Welati Tiki #${challenge.tikiNumber}
|
||||
|
||||
Nonce: ${challenge.nonce}
|
||||
Timestamp: ${challenge.timestamp}
|
||||
Expires: ${new Date(challenge.expiresAt).toISOString()}
|
||||
|
||||
By signing this message, you prove you control the wallet that owns this Tiki NFT.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign authentication challenge with wallet
|
||||
*/
|
||||
export async function signChallenge(
|
||||
account: InjectedAccountWithMeta,
|
||||
challenge: AuthChallenge
|
||||
): Promise<string> {
|
||||
try {
|
||||
await cryptoWaitReady();
|
||||
|
||||
const injector = await web3FromAddress(account.address);
|
||||
const signRaw = injector?.signer?.signRaw;
|
||||
|
||||
if (!signRaw) {
|
||||
throw new Error('Signer not available');
|
||||
}
|
||||
|
||||
const message = formatChallengeMessage(challenge);
|
||||
|
||||
const { signature } = await signRaw({
|
||||
address: account.address,
|
||||
data: stringToHex(message),
|
||||
type: 'bytes'
|
||||
});
|
||||
|
||||
return signature;
|
||||
} catch (error) {
|
||||
console.error('Signature error:', error);
|
||||
throw new Error('Failed to sign challenge');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify signature
|
||||
*/
|
||||
export async function verifySignature(
|
||||
signature: string,
|
||||
challenge: AuthChallenge,
|
||||
expectedAddress: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await cryptoWaitReady();
|
||||
|
||||
// Check if challenge has expired
|
||||
if (Date.now() > challenge.expiresAt) {
|
||||
console.warn('Challenge has expired');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = formatChallengeMessage(challenge);
|
||||
const messageU8a = stringToU8a(message);
|
||||
const signatureU8a = hexToU8a(signature);
|
||||
const publicKey = decodeAddress(expectedAddress);
|
||||
|
||||
const result = signatureVerify(messageU8a, signatureU8a, publicKey);
|
||||
|
||||
return result.isValid;
|
||||
} catch (error) {
|
||||
console.error('Verification error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LOCAL STORAGE UTILITIES
|
||||
// ========================================
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'pezkuwi_citizen_';
|
||||
|
||||
export interface CitizenSession {
|
||||
tikiNumber: string;
|
||||
walletAddress: string;
|
||||
sessionToken: string; // JWT-like token
|
||||
encryptedDataCID?: string; // IPFS CID
|
||||
lastAuthenticated: number; // Timestamp
|
||||
expiresAt: number; // Session expiry (24h)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save encrypted citizen session to localStorage
|
||||
*/
|
||||
export async function saveCitizenSession(session: CitizenSession): Promise<void> {
|
||||
try {
|
||||
const sessionJson = JSON.stringify(session);
|
||||
const sessionKey = `${STORAGE_KEY_PREFIX}session`;
|
||||
|
||||
// For MVP, store plainly. For production, encrypt with device key
|
||||
localStorage.setItem(sessionKey, sessionJson);
|
||||
} catch (error) {
|
||||
console.error('Error saving session:', error);
|
||||
throw new Error('Failed to save session');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load citizen session from localStorage
|
||||
*/
|
||||
export function loadCitizenSession(): CitizenSession | null {
|
||||
try {
|
||||
const sessionKey = `${STORAGE_KEY_PREFIX}session`;
|
||||
const sessionJson = localStorage.getItem(sessionKey);
|
||||
|
||||
if (!sessionJson) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = JSON.parse(sessionJson) as CitizenSession;
|
||||
|
||||
// Check if session has expired
|
||||
if (Date.now() > session.expiresAt) {
|
||||
clearCitizenSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error('Error loading session:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear citizen session from localStorage
|
||||
*/
|
||||
export function clearCitizenSession(): void {
|
||||
try {
|
||||
const sessionKey = `${STORAGE_KEY_PREFIX}session`;
|
||||
localStorage.removeItem(sessionKey);
|
||||
} catch (error) {
|
||||
console.error('Error clearing session:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save encrypted citizenship data to localStorage (backup)
|
||||
*/
|
||||
export async function saveLocalCitizenshipData(
|
||||
data: CitizenshipData,
|
||||
walletAddress: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const encrypted = await encryptData(data, walletAddress);
|
||||
const dataKey = `${STORAGE_KEY_PREFIX}data_${walletAddress}`;
|
||||
|
||||
localStorage.setItem(dataKey, encrypted);
|
||||
} catch (error) {
|
||||
console.error('Error saving citizenship data:', error);
|
||||
throw new Error('Failed to save citizenship data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load encrypted citizenship data from localStorage
|
||||
*/
|
||||
export async function loadLocalCitizenshipData(
|
||||
walletAddress: string
|
||||
): Promise<CitizenshipData | null> {
|
||||
try {
|
||||
const dataKey = `${STORAGE_KEY_PREFIX}data_${walletAddress}`;
|
||||
const encrypted = localStorage.getItem(dataKey);
|
||||
|
||||
if (!encrypted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await decryptData(encrypted, walletAddress);
|
||||
} catch (error) {
|
||||
console.error('Error loading citizenship data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IPFS UTILITIES (Placeholder)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Upload encrypted data to IPFS
|
||||
* NOTE: This is a placeholder. Implement with actual IPFS client (Pinata, Web3.Storage, etc.)
|
||||
*/
|
||||
export async function uploadToIPFS(encryptedData: string): Promise<string> {
|
||||
// TODO: Implement actual IPFS upload
|
||||
// For MVP, we can use Pinata API or Web3.Storage
|
||||
|
||||
console.warn('IPFS upload not yet implemented. Using mock CID.');
|
||||
|
||||
// Mock CID for development
|
||||
const mockCid = `Qm${Math.random().toString(36).substring(2, 15)}`;
|
||||
|
||||
return mockCid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch encrypted data from IPFS
|
||||
* NOTE: This is a placeholder. Implement with actual IPFS client
|
||||
*/
|
||||
export async function fetchFromIPFS(cid: string): Promise<string> {
|
||||
// TODO: Implement actual IPFS fetch
|
||||
// For MVP, use public IPFS gateways or dedicated service
|
||||
|
||||
console.warn('IPFS fetch not yet implemented. Returning mock data.');
|
||||
|
||||
// Mock encrypted data
|
||||
return '0x000000000000000000000000';
|
||||
}
|
||||
+7
-17
@@ -1,24 +1,14 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
/**
|
||||
* Web-specific className utility (uses Tailwind merge)
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatNumber(value: number, decimals: number = 2): string {
|
||||
if (value === 0) return '0';
|
||||
if (value < 0.01) return '<0.01';
|
||||
|
||||
// For large numbers, use K, M, B suffixes
|
||||
if (value >= 1e9) {
|
||||
return (value / 1e9).toFixed(decimals) + 'B';
|
||||
}
|
||||
if (value >= 1e6) {
|
||||
return (value / 1e6).toFixed(decimals) + 'M';
|
||||
}
|
||||
if (value >= 1e3) {
|
||||
return (value / 1e3).toFixed(decimals) + 'K';
|
||||
}
|
||||
|
||||
return value.toFixed(decimals);
|
||||
}
|
||||
/**
|
||||
* Re-export formatNumber from shared utils
|
||||
*/
|
||||
export { formatNumber } from '@pezkuwi/utils/format';
|
||||
|
||||
@@ -9,8 +9,8 @@ import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@/lib/tiki';
|
||||
import { getAllScores, type UserScores } from '@/lib/scores';
|
||||
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@pezkuwi/lib/tiki';
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
|
||||
+5
-1
@@ -8,7 +8,11 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@pezkuwi/i18n": ["../shared/i18n"]
|
||||
"@pezkuwi/i18n": ["../shared/i18n"],
|
||||
"@pezkuwi/lib": ["../shared/lib"],
|
||||
"@pezkuwi/utils": ["../shared/utils"],
|
||||
"@pezkuwi/theme": ["../shared/theme"],
|
||||
"@pezkuwi/types": ["../shared/types"]
|
||||
},
|
||||
"noImplicitAny": false,
|
||||
"noUnusedParameters": false,
|
||||
|
||||
@@ -23,6 +23,11 @@ export default defineConfig(({ mode }) => ({
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@pezkuwi/i18n": path.resolve(__dirname, "../shared/i18n"),
|
||||
"@pezkuwi/lib": path.resolve(__dirname, "../shared/lib"),
|
||||
"@pezkuwi/utils": path.resolve(__dirname, "../shared/utils"),
|
||||
"@pezkuwi/theme": path.resolve(__dirname, "../shared/theme"),
|
||||
"@pezkuwi/types": path.resolve(__dirname, "../shared/types"),
|
||||
'buffer': 'buffer/',
|
||||
},
|
||||
},
|
||||
json: {
|
||||
|
||||
Reference in New Issue
Block a user