mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +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:
@@ -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';
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user