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:
Claude
2025-11-14 22:44:53 +00:00
parent 06d4da81df
commit 7b95b8a409
43 changed files with 172 additions and 484 deletions
+6 -25
View File
@@ -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';
+9 -1
View File
@@ -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"]
}
}
}
+30
View File
@@ -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;
+74
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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();
+2 -2
View File
@@ -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>;
+2 -2
View File
@@ -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) => {
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 => {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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();
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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 = () => {
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -9,7 +9,7 @@ import {
generateZKProof,
DEFAULT_BADGES,
ROLES
} from '@/lib/identity';
} from '@pezkuwi/lib/identity';
interface IdentityContextType {
profile: IdentityProfile | null;
+1 -1
View File
@@ -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';
-404
View File
@@ -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
View File
@@ -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';
+2 -2
View File
@@ -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
View File
@@ -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,
+5
View File
@@ -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: {