From dfc8d27c39be1740524171b0ff5407dcee5f1fff Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sun, 18 Jan 2026 01:08:42 +0300 Subject: [PATCH] Add presale lib and backup file, ignore credentials --- .gitignore | 1 + mobile/App.tsx.backup | 25 ++ shared/lib/presale.ts | 649 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 675 insertions(+) create mode 100644 mobile/App.tsx.backup create mode 100644 shared/lib/presale.ts diff --git a/.gitignore b/.gitignore index b8d3a6b3..8ce90fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,4 @@ COMMISSION_SYSTEM_SUMMARY - Copy.md:Zone.Identifier # APK files (too large for GitHub) *.apk +mobile/credentials.json diff --git a/mobile/App.tsx.backup b/mobile/App.tsx.backup new file mode 100644 index 00000000..e61be44e --- /dev/null +++ b/mobile/App.tsx.backup @@ -0,0 +1,25 @@ +import React from 'react'; +import { StatusBar } from 'expo-status-bar'; +import { ErrorBoundary } from './src/components/ErrorBoundary'; +import { AuthProvider } from './src/contexts/AuthContext'; +import { PezkuwiProvider } from './src/contexts/PezkuwiContext'; +import { BiometricAuthProvider } from './src/contexts/BiometricAuthContext'; +import { ThemeProvider } from './src/contexts/ThemeContext'; +import AppNavigator from './src/navigation/AppNavigator'; + +export default function App() { + return ( + + + + + + + + + + + + + ); +} diff --git a/shared/lib/presale.ts b/shared/lib/presale.ts new file mode 100644 index 00000000..4be47603 --- /dev/null +++ b/shared/lib/presale.ts @@ -0,0 +1,649 @@ +// ======================================== +// Presale Helper Functions +// ======================================== +// Helper functions for pezpallet_presale integration +// Multi-presale launchpad platform for PezkuwiChain + +import type { ApiPromise } from '@pezkuwi/api'; +import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types'; + +// ======================================== +// Types & Interfaces +// ======================================== + +export type PresaleStatus = 'Pending' | 'Active' | 'Paused' | 'Successful' | 'Failed' | 'Cancelled' | 'Finalized'; +export type AccessControl = 'Public' | 'Whitelist'; + +export interface ContributionLimits { + minContribution: string; + maxContribution: string; + softCap: string; + hardCap: string; +} + +export interface VestingSchedule { + immediateReleasePercent: number; + vestingDurationBlocks: number; + cliffBlocks: number; +} + +export interface RefundConfig { + gracePeriodBlocks: number; + refundFeePercent: number; + graceRefundFeePercent: number; +} + +export interface BonusTier { + minContribution: string; + bonusPercentage: number; +} + +export interface PresaleConfig { + id: number; + owner: string; + paymentAsset: number; + rewardAsset: number; + tokensForSale: string; + startBlock: number; + duration: number; + status: PresaleStatus; + accessControl: AccessControl; + limits: ContributionLimits; + bonusTiers: BonusTier[]; + vesting: VestingSchedule | null; + gracePeriodBlocks: number; + refundFeePercent: number; + graceRefundFeePercent: number; +} + +export interface ContributionInfo { + amount: string; + contributedAt: number; + refunded: boolean; + refundedAt: number | null; + refundFeePaid: string; +} + +export interface PresaleInfo extends PresaleConfig { + totalRaised: string; + contributorCount: number; + endBlock: number; + progress: number; + timeRemaining: { + days: number; + hours: number; + minutes: number; + seconds: number; + }; + isEnded: boolean; + softCapReached: boolean; + hardCapReached: boolean; +} + +export interface UserPresaleInfo { + contribution: ContributionInfo | null; + isWhitelisted: boolean; + vestedClaimed: string; + claimableVested: string; +} + +export interface PlatformStats { + totalPresales: number; + activePresales: number; + successfulPresales: number; + totalPlatformVolume: string; + totalPlatformFees: string; +} + +export interface CreatePresaleParams { + paymentAsset: number; + rewardAsset: number; + tokensForSale: string; + duration: number; + isWhitelist: boolean; + limits: ContributionLimits; + vesting?: VestingSchedule; + refundConfig: RefundConfig; +} + +// Constants +export const PLATFORM_FEE_PERCENT = 2; +export const BLOCK_TIME_SECONDS = 6; +export const WUSDT_DECIMALS = 6; +export const PEZ_DECIMALS = 12; + +// ======================================== +// Query Functions +// ======================================== + +/** + * Get total number of presales + */ +export async function getPresaleCount(api: ApiPromise): Promise { + try { + const nextId = await api.query.presale.nextPresaleId(); + return nextId.toNumber(); + } catch (error) { + console.error('Error getting presale count:', error); + return 0; + } +} + +/** + * Get presale configuration by ID + */ +export async function getPresaleConfig( + api: ApiPromise, + presaleId: number +): Promise { + try { + const presaleData = await api.query.presale.presales(presaleId); + + if (presaleData.isNone) { + return null; + } + + const presale = presaleData.unwrap(); + const config = presale.toJSON() as any; + + return { + id: presaleId, + owner: config.owner || '', + paymentAsset: config.paymentAsset || 0, + rewardAsset: config.rewardAsset || 0, + tokensForSale: config.tokensForSale?.toString() || '0', + startBlock: config.startBlock || 0, + duration: config.duration || 0, + status: config.status || 'Pending', + accessControl: config.accessControl || 'Public', + limits: { + minContribution: config.limits?.minContribution?.toString() || '0', + maxContribution: config.limits?.maxContribution?.toString() || '0', + softCap: config.limits?.softCap?.toString() || '0', + hardCap: config.limits?.hardCap?.toString() || '0', + }, + bonusTiers: (config.bonusTiers || []).map((tier: any) => ({ + minContribution: tier.minContribution?.toString() || '0', + bonusPercentage: tier.bonusPercentage || 0, + })), + vesting: config.vesting ? { + immediateReleasePercent: config.vesting.immediateReleasePercent || 0, + vestingDurationBlocks: config.vesting.vestingDurationBlocks || 0, + cliffBlocks: config.vesting.cliffBlocks || 0, + } : null, + gracePeriodBlocks: config.gracePeriodBlocks || 0, + refundFeePercent: config.refundFeePercent || 0, + graceRefundFeePercent: config.graceRefundFeePercent || 0, + }; + } catch (error) { + console.error(`Error getting presale ${presaleId}:`, error); + return null; + } +} + +/** + * Get presale with computed info (total raised, time remaining, etc.) + */ +export async function getPresaleInfo( + api: ApiPromise, + presaleId: number +): Promise { + try { + const config = await getPresaleConfig(api, presaleId); + if (!config) return null; + + const [totalRaised, contributors, header] = await Promise.all([ + api.query.presale.totalRaised(presaleId), + api.query.presale.contributors(presaleId), + api.rpc.chain.getHeader(), + ]); + + const currentBlock = header.number.toNumber(); + const endBlock = config.startBlock + config.duration; + const totalRaisedStr = totalRaised.toString(); + const contributorList = contributors.toHuman() as string[]; + + // Calculate progress + const hardCapNum = parseFloat(config.limits.hardCap); + const raisedNum = parseFloat(totalRaisedStr); + const progress = hardCapNum > 0 ? Math.min(100, (raisedNum / hardCapNum) * 100) : 0; + + // Calculate time remaining + const blocksRemaining = Math.max(0, endBlock - currentBlock); + const secondsRemaining = blocksRemaining * BLOCK_TIME_SECONDS; + const timeRemaining = { + days: Math.floor(secondsRemaining / 86400), + hours: Math.floor((secondsRemaining % 86400) / 3600), + minutes: Math.floor((secondsRemaining % 3600) / 60), + seconds: Math.floor(secondsRemaining % 60), + }; + + // Check caps + const softCapNum = parseFloat(config.limits.softCap); + const softCapReached = raisedNum >= softCapNum; + const hardCapReached = raisedNum >= hardCapNum; + + return { + ...config, + totalRaised: totalRaisedStr, + contributorCount: contributorList?.length || 0, + endBlock, + progress, + timeRemaining, + isEnded: currentBlock >= endBlock, + softCapReached, + hardCapReached, + }; + } catch (error) { + console.error(`Error getting presale info ${presaleId}:`, error); + return null; + } +} + +/** + * Get all presales + */ +export async function getAllPresales(api: ApiPromise): Promise { + try { + const count = await getPresaleCount(api); + const presales: PresaleInfo[] = []; + + for (let i = 0; i < count; i++) { + const info = await getPresaleInfo(api, i); + if (info) { + presales.push(info); + } + } + + // Sort: Active first, then by ID desc + return presales.sort((a, b) => { + const statusOrder: Record = { + Active: 0, Pending: 1, Paused: 2, Successful: 3, Failed: 4, Finalized: 5, Cancelled: 6 + }; + if (statusOrder[a.status] !== statusOrder[b.status]) { + return statusOrder[a.status] - statusOrder[b.status]; + } + return b.id - a.id; + }); + } catch (error) { + console.error('Error getting all presales:', error); + return []; + } +} + +/** + * Get user's contribution info for a presale + */ +export async function getUserContribution( + api: ApiPromise, + presaleId: number, + address: string +): Promise { + try { + const contribution = await api.query.presale.contributions(presaleId, address); + + if (contribution.isNone) { + return null; + } + + const data = contribution.unwrap().toJSON() as any; + + return { + amount: data.amount?.toString() || '0', + contributedAt: data.contributedAt || 0, + refunded: data.refunded || false, + refundedAt: data.refundedAt || null, + refundFeePaid: data.refundFeePaid?.toString() || '0', + }; + } catch (error) { + console.error(`Error getting contribution for ${address}:`, error); + return null; + } +} + +/** + * Check if user is whitelisted for a presale + */ +export async function isUserWhitelisted( + api: ApiPromise, + presaleId: number, + address: string +): Promise { + try { + const isWhitelisted = await api.query.presale.whitelistedAccounts(presaleId, address); + return isWhitelisted.isTrue; + } catch (error) { + console.error(`Error checking whitelist for ${address}:`, error); + return false; + } +} + +/** + * Get user's vesting claimed amount + */ +export async function getVestingClaimed( + api: ApiPromise, + presaleId: number, + address: string +): Promise { + try { + const claimed = await api.query.presale.vestingClaimed(presaleId, address); + return claimed.toString(); + } catch (error) { + console.error(`Error getting vesting claimed:`, error); + return '0'; + } +} + +/** + * Get complete user info for a presale + */ +export async function getUserPresaleInfo( + api: ApiPromise, + presaleId: number, + address: string +): Promise { + try { + const [contribution, isWhitelisted, vestedClaimed] = await Promise.all([ + getUserContribution(api, presaleId, address), + isUserWhitelisted(api, presaleId, address), + getVestingClaimed(api, presaleId, address), + ]); + + // TODO: Calculate claimableVested based on vesting schedule + return { + contribution, + isWhitelisted, + vestedClaimed, + claimableVested: '0', // Calculate based on vesting schedule + }; + } catch (error) { + console.error(`Error getting user presale info:`, error); + return { + contribution: null, + isWhitelisted: false, + vestedClaimed: '0', + claimableVested: '0', + }; + } +} + +/** + * Get platform-wide statistics + */ +export async function getPlatformStats(api: ApiPromise): Promise { + try { + const [totalPresales, totalVolume, totalFees, successfulPresales] = await Promise.all([ + getPresaleCount(api), + api.query.presale.totalPlatformVolume(), + api.query.presale.totalPlatformFees(), + api.query.presale.successfulPresales(), + ]); + + // Count active presales + let activeCount = 0; + for (let i = 0; i < totalPresales; i++) { + const config = await getPresaleConfig(api, i); + if (config?.status === 'Active') { + activeCount++; + } + } + + return { + totalPresales, + activePresales: activeCount, + successfulPresales: successfulPresales.toNumber(), + totalPlatformVolume: totalVolume.toString(), + totalPlatformFees: totalFees.toString(), + }; + } catch (error) { + console.error('Error getting platform stats:', error); + return { + totalPresales: 0, + activePresales: 0, + successfulPresales: 0, + totalPlatformVolume: '0', + totalPlatformFees: '0', + }; + } +} + +// ======================================== +// Transaction Functions +// ======================================== + +/** + * Contribute to a presale + */ +export async function contribute( + api: ApiPromise, + account: InjectedAccountWithMeta, + presaleId: number, + amount: string, // in raw units (with decimals) + onStatus?: (status: string) => void +): Promise<{ success: boolean; error?: string; txHash?: string }> { + try { + onStatus?.('Preparing transaction...'); + + const tx = api.tx.presale.contribute(presaleId, amount); + + return new Promise((resolve) => { + tx.signAndSend( + account.address, + { signer: account.signer }, + ({ status, dispatchError, txHash }) => { + if (status.isInBlock) { + onStatus?.('Transaction in block...'); + } + if (status.isFinalized) { + if (dispatchError) { + let errorMessage = 'Transaction failed'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; + } + resolve({ success: false, error: errorMessage }); + } else { + resolve({ success: true, txHash: txHash.toHex() }); + } + } + } + ).catch((error) => { + resolve({ success: false, error: error.message }); + }); + }); + } catch (error: any) { + return { success: false, error: error.message }; + } +} + +/** + * Request refund from a presale + */ +export async function refund( + api: ApiPromise, + account: InjectedAccountWithMeta, + presaleId: number, + onStatus?: (status: string) => void +): Promise<{ success: boolean; error?: string; txHash?: string }> { + try { + onStatus?.('Preparing refund...'); + + const tx = api.tx.presale.refund(presaleId); + + return new Promise((resolve) => { + tx.signAndSend( + account.address, + { signer: account.signer }, + ({ status, dispatchError, txHash }) => { + if (status.isInBlock) { + onStatus?.('Refund in block...'); + } + if (status.isFinalized) { + if (dispatchError) { + let errorMessage = 'Refund failed'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; + } + resolve({ success: false, error: errorMessage }); + } else { + resolve({ success: true, txHash: txHash.toHex() }); + } + } + } + ).catch((error) => { + resolve({ success: false, error: error.message }); + }); + }); + } catch (error: any) { + return { success: false, error: error.message }; + } +} + +/** + * Claim vested tokens + */ +export async function claimVested( + api: ApiPromise, + account: InjectedAccountWithMeta, + presaleId: number, + onStatus?: (status: string) => void +): Promise<{ success: boolean; error?: string; txHash?: string }> { + try { + onStatus?.('Preparing vesting claim...'); + + const tx = api.tx.presale.claimVested(presaleId); + + return new Promise((resolve) => { + tx.signAndSend( + account.address, + { signer: account.signer }, + ({ status, dispatchError, txHash }) => { + if (status.isInBlock) { + onStatus?.('Claim in block...'); + } + if (status.isFinalized) { + if (dispatchError) { + let errorMessage = 'Claim failed'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; + } + resolve({ success: false, error: errorMessage }); + } else { + resolve({ success: true, txHash: txHash.toHex() }); + } + } + } + ).catch((error) => { + resolve({ success: false, error: error.message }); + }); + }); + } catch (error: any) { + return { success: false, error: error.message }; + } +} + +// ======================================== +// Utility Functions +// ======================================== + +/** + * Format wUSDT amount (6 decimals) to human readable + */ +export function formatWUSDT(amount: string): string { + const num = parseFloat(amount) / Math.pow(10, WUSDT_DECIMALS); + if (num >= 1_000_000) return (num / 1_000_000).toFixed(2) + 'M'; + if (num >= 1_000) return (num / 1_000).toFixed(2) + 'K'; + return num.toLocaleString('en-US', { maximumFractionDigits: 2 }); +} + +/** + * Format PEZ amount (12 decimals) to human readable + */ +export function formatPEZ(amount: string): string { + const num = parseFloat(amount) / Math.pow(10, PEZ_DECIMALS); + if (num >= 1_000_000) return (num / 1_000_000).toFixed(2) + 'M'; + if (num >= 1_000) return (num / 1_000).toFixed(2) + 'K'; + return num.toLocaleString('en-US', { maximumFractionDigits: 0 }); +} + +/** + * Parse human readable amount to raw units + */ +export function parseWUSDT(amount: string | number): string { + const num = typeof amount === 'string' ? parseFloat(amount) : amount; + return Math.floor(num * Math.pow(10, WUSDT_DECIMALS)).toString(); +} + +/** + * Calculate expected reward tokens for a contribution + * Formula: (contribution / totalRaised) * tokensForSale + */ +export function calculateExpectedReward( + contribution: string, + totalRaised: string, + tokensForSale: string +): string { + const contrib = parseFloat(contribution); + const raised = parseFloat(totalRaised); + const tokens = parseFloat(tokensForSale); + + if (raised === 0) return '0'; + + const reward = (contrib / raised) * tokens; + return Math.floor(reward).toString(); +} + +/** + * Calculate platform fee for an amount + */ +export function calculatePlatformFee(amount: string): { + fee: string; + net: string; + toTreasury: string; + toBurn: string; + toStakers: string; +} { + const amountNum = parseFloat(amount); + const fee = amountNum * (PLATFORM_FEE_PERCENT / 100); + const net = amountNum - fee; + const toTreasury = fee * 0.5; // 50% + const toBurn = fee * 0.25; // 25% + const toStakers = fee * 0.25; // 25% + + return { + fee: Math.floor(fee).toString(), + net: Math.floor(net).toString(), + toTreasury: Math.floor(toTreasury).toString(), + toBurn: Math.floor(toBurn).toString(), + toStakers: Math.floor(toStakers).toString(), + }; +} + +/** + * Check if refund is in grace period (lower fee) + */ +export function isInGracePeriod( + contributedAtBlock: number, + gracePeriodBlocks: number, + currentBlock: number +): boolean { + return currentBlock <= contributedAtBlock + gracePeriodBlocks; +} + +/** + * Get refund fee percentage based on grace period + */ +export function getRefundFeePercent( + contributedAtBlock: number, + gracePeriodBlocks: number, + graceRefundFeePercent: number, + normalRefundFeePercent: number, + currentBlock: number +): number { + return isInGracePeriod(contributedAtBlock, gracePeriodBlocks, currentBlock) + ? graceRefundFeePercent + : normalRefundFeePercent; +}