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
+86
View File
@@ -0,0 +1,86 @@
/**
* Authentication and Authorization Utilities
* Security-critical: Founder wallet detection and permissions
*/
// SECURITY: Founder wallet address for beta testnet
// This address has sudo rights and can perform privileged operations
export const FOUNDER_ADDRESS = '5GgTgG9sRmPQAYU1RsTejZYnZRjwzKZKWD3awtuqjHioki45';
/**
* Check if given address is the founder wallet
* @param address - Substrate address to check
* @returns true if address matches founder, false otherwise
*/
export const isFounderWallet = (address: string | null | undefined): boolean => {
if (!address) return false;
return address === FOUNDER_ADDRESS;
};
/**
* Validate substrate address format
* @param address - Address to validate
* @returns true if address is valid format
*/
export const isValidSubstrateAddress = (address: string): boolean => {
// Substrate addresses start with 5 and are 47-48 characters
return /^5[a-zA-Z0-9]{46,47}$/.test(address);
};
/**
* Permission levels for DEX operations
*/
export enum DexPermission {
// Anyone can perform these
VIEW_POOLS = 'view_pools',
ADD_LIQUIDITY = 'add_liquidity',
REMOVE_LIQUIDITY = 'remove_liquidity',
SWAP = 'swap',
// Only founder can perform these
CREATE_POOL = 'create_pool',
SET_FEE_RATE = 'set_fee_rate',
PAUSE_POOL = 'pause_pool',
WHITELIST_TOKEN = 'whitelist_token',
}
/**
* Check if user has permission for a specific DEX operation
* @param address - User's wallet address
* @param permission - Required permission level
* @returns true if user has permission
*/
export const hasPermission = (
address: string | null | undefined,
permission: DexPermission
): boolean => {
if (!address || !isValidSubstrateAddress(address)) {
return false;
}
const founderOnly = [
DexPermission.CREATE_POOL,
DexPermission.SET_FEE_RATE,
DexPermission.PAUSE_POOL,
DexPermission.WHITELIST_TOKEN,
];
// Founder-only operations
if (founderOnly.includes(permission)) {
return isFounderWallet(address);
}
// Everyone can view and trade
return true;
};
/**
* Get user role string for display
* @param address - User's wallet address
* @returns Human-readable role
*/
export const getUserRole = (address: string | null | undefined): string => {
if (!address) return 'Guest';
if (isFounderWallet(address)) return 'Founder (Admin)';
return 'User';
};
+242
View File
@@ -0,0 +1,242 @@
import { ApiPromise } from '@polkadot/api';
import { KNOWN_TOKENS, PoolInfo, SwapQuote } from '../types/dex';
/**
* Format balance with proper decimals
* @param balance - Raw balance string
* @param decimals - Token decimals
* @param precision - Display precision (default 4)
*/
export const formatTokenBalance = (
balance: string | number | bigint,
decimals: number,
precision: number = 4
): string => {
const balanceBigInt = BigInt(balance);
const divisor = BigInt(10 ** decimals);
const integerPart = balanceBigInt / divisor;
const fractionalPart = balanceBigInt % divisor;
const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
const displayFractional = fractionalStr.slice(0, precision);
return `${integerPart}.${displayFractional}`;
};
/**
* Parse user input to raw balance
* @param input - User input string (e.g., "10.5")
* @param decimals - Token decimals
*/
export const parseTokenInput = (input: string, decimals: number): string => {
if (!input || input === '' || input === '.') return '0';
// Remove non-numeric chars except decimal point
const cleaned = input.replace(/[^\d.]/g, '');
const [integer, fractional] = cleaned.split('.');
const integerPart = BigInt(integer || '0') * BigInt(10 ** decimals);
if (!fractional) {
return integerPart.toString();
}
// Pad or truncate fractional part
const fractionalPadded = fractional.padEnd(decimals, '0').slice(0, decimals);
const fractionalPart = BigInt(fractionalPadded);
return (integerPart + fractionalPart).toString();
};
/**
* Calculate price impact for a swap
* @param reserveIn - Reserve of input token
* @param reserveOut - Reserve of output token
* @param amountIn - Amount being swapped in
*/
export const calculatePriceImpact = (
reserveIn: string,
reserveOut: string,
amountIn: string
): string => {
const reserveInBig = BigInt(reserveIn);
const reserveOutBig = BigInt(reserveOut);
const amountInBig = BigInt(amountIn);
if (reserveInBig === BigInt(0) || reserveOutBig === BigInt(0)) {
return '0';
}
// Price before = reserveOut / reserveIn
// Amount out with constant product: (amountIn * reserveOut) / (reserveIn + amountIn)
const amountOut =
(amountInBig * reserveOutBig) / (reserveInBig + amountInBig);
// Price after = (reserveOut - amountOut) / (reserveIn + amountIn)
const priceBefore = (reserveOutBig * BigInt(10000)) / reserveInBig;
const priceAfter =
((reserveOutBig - amountOut) * BigInt(10000)) /
(reserveInBig + amountInBig);
// Impact = |priceAfter - priceBefore| / priceBefore * 100
const impact = ((priceBefore - priceAfter) * BigInt(100)) / priceBefore;
return (Number(impact) / 100).toFixed(2);
};
/**
* Calculate output amount for a swap (constant product formula)
* @param amountIn - Input amount
* @param reserveIn - Reserve of input token
* @param reserveOut - Reserve of output token
* @param feeRate - Fee rate (e.g., 30 for 0.3%)
*/
export const getAmountOut = (
amountIn: string,
reserveIn: string,
reserveOut: string,
feeRate: number = 30
): string => {
const amountInBig = BigInt(amountIn);
const reserveInBig = BigInt(reserveIn);
const reserveOutBig = BigInt(reserveOut);
if (
amountInBig === BigInt(0) ||
reserveInBig === BigInt(0) ||
reserveOutBig === BigInt(0)
) {
return '0';
}
// amountInWithFee = amountIn * (10000 - feeRate) / 10000
const amountInWithFee = (amountInBig * BigInt(10000 - feeRate)) / BigInt(10000);
// amountOut = (amountInWithFee * reserveOut) / (reserveIn + amountInWithFee)
const numerator = amountInWithFee * reserveOutBig;
const denominator = reserveInBig + amountInWithFee;
return (numerator / denominator).toString();
};
/**
* Calculate required amount1 for given amount2 (maintaining ratio)
* @param amount2 - Amount of token 2
* @param reserve1 - Reserve of token 1
* @param reserve2 - Reserve of token 2
*/
export const quote = (
amount2: string,
reserve1: string,
reserve2: string
): string => {
const amount2Big = BigInt(amount2);
const reserve1Big = BigInt(reserve1);
const reserve2Big = BigInt(reserve2);
if (reserve2Big === BigInt(0)) return '0';
return ((amount2Big * reserve1Big) / reserve2Big).toString();
};
/**
* Fetch all existing pools from chain
* @param api - Polkadot API instance
*/
export const fetchPools = async (api: ApiPromise): Promise<PoolInfo[]> => {
try {
const pools: PoolInfo[] = [];
// Query all pool accounts
const poolKeys = await api.query.assetConversion.pools.keys();
for (const key of poolKeys) {
// Extract asset IDs from storage key
const [asset1Raw, asset2Raw] = key.args;
const asset1 = Number(asset1Raw.toString());
const asset2 = Number(asset2Raw.toString());
// Get pool account
const poolAccount = await api.query.assetConversion.pools([asset1, asset2]);
if (poolAccount.isNone) continue;
// Get reserves
const reserve1Data = await api.query.assets.account(asset1, poolAccount.unwrap());
const reserve2Data = await api.query.assets.account(asset2, poolAccount.unwrap());
const reserve1 = reserve1Data.isSome ? reserve1Data.unwrap().balance.toString() : '0';
const reserve2 = reserve2Data.isSome ? reserve2Data.unwrap().balance.toString() : '0';
// Get token info
const token1 = KNOWN_TOKENS[asset1] || {
id: asset1,
symbol: `Asset ${asset1}`,
name: `Unknown Asset ${asset1}`,
decimals: 12,
};
const token2 = KNOWN_TOKENS[asset2] || {
id: asset2,
symbol: `Asset ${asset2}`,
name: `Unknown Asset ${asset2}`,
decimals: 12,
};
pools.push({
id: `${asset1}-${asset2}`,
asset1,
asset2,
asset1Symbol: token1.symbol,
asset2Symbol: token2.symbol,
asset1Decimals: token1.decimals,
asset2Decimals: token2.decimals,
reserve1,
reserve2,
lpTokenSupply: '0', // TODO: Query LP token supply
feeRate: '0.3', // Default 0.3%
});
}
return pools;
} catch (error) {
console.error('Failed to fetch pools:', error);
return [];
}
};
/**
* Validate amounts are greater than zero
*/
export const validateAmount = (amount: string): boolean => {
try {
const amountBig = BigInt(amount);
return amountBig > BigInt(0);
} catch {
return false;
}
};
/**
* Calculate minimum amount with slippage tolerance
* @param amount - Expected amount
* @param slippage - Slippage percentage (e.g., 1 for 1%)
*/
export const calculateMinAmount = (amount: string, slippage: number): string => {
const amountBig = BigInt(amount);
const slippageFactor = BigInt(10000 - slippage * 100);
return ((amountBig * slippageFactor) / BigInt(10000)).toString();
};
/**
* Get token symbol safely
*/
export const getTokenSymbol = (assetId: number): string => {
return KNOWN_TOKENS[assetId]?.symbol || `Asset ${assetId}`;
};
/**
* Get token decimals safely
*/
export const getTokenDecimals = (assetId: number): number => {
return KNOWN_TOKENS[assetId]?.decimals || 12;
};
+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;
}