diff --git a/apps/shell/next-env.d.ts b/apps/shell/next-env.d.ts index 1511519d3..20e7bcfb0 100644 --- a/apps/shell/next-env.d.ts +++ b/apps/shell/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import './.next/types/routes.d.ts'; +import './.next/dev/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/shell/src/app/[locale]/burn/page.tsx b/apps/shell/src/app/[locale]/burn/page.tsx new file mode 100644 index 000000000..7fec6a4ec --- /dev/null +++ b/apps/shell/src/app/[locale]/burn/page.tsx @@ -0,0 +1,21 @@ +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query'; +import { MintPage } from '@haqq/shell-burn-waitlist'; + +export const dynamic = 'force-dynamic'; +export const fetchCache = 'force-no-store'; + +export default async function BurnMintPage() { + const queryClient = new QueryClient(); + + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); +} diff --git a/apps/shell/src/config/wagmi-config.ts b/apps/shell/src/config/wagmi-config.ts index d21d24b98..dbaaacc24 100644 --- a/apps/shell/src/config/wagmi-config.ts +++ b/apps/shell/src/config/wagmi-config.ts @@ -21,6 +21,11 @@ const supportedChainsTransports = supportedChains.reduce( {} as Record, ); +// TODO: temporary RPC override for burn testing on mainnet +supportedChainsTransports[11235] = http('http://128.199.216.2:38545', { + batch: true, +}); + /** Skip WalletConnect on server (SSR) — it uses indexedDB which is not defined in Node. */ const isClient = typeof window !== 'undefined'; diff --git a/libs/burn-waitlist/src/index.ts b/libs/burn-waitlist/src/index.ts index 12f040977..da9ec3a56 100644 --- a/libs/burn-waitlist/src/index.ts +++ b/libs/burn-waitlist/src/index.ts @@ -1,4 +1,6 @@ export * from './lib/waitlist-page'; +export * from './lib/mint-page'; export * from './lib/hooks'; export * from './lib/components'; export * from './lib/constants/waitlist-config'; +export * from './lib/constants/ethiq-config'; diff --git a/libs/burn-waitlist/src/lib/abi/ethiq.ts b/libs/burn-waitlist/src/lib/abi/ethiq.ts new file mode 100644 index 000000000..0bb735d9f --- /dev/null +++ b/libs/burn-waitlist/src/lib/abi/ethiq.ts @@ -0,0 +1,442 @@ +// ABI for Ethiq precompile contract at 0x0000000000000000000000000000000000000900 +export const EthiqAbi = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'granter', + type: 'address', + }, + { + indexed: false, + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + { + indexed: false, + internalType: 'uint256[]', + name: 'values', + type: 'uint256[]', + }, + ], + name: 'AllowanceChange', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'granter', + type: 'address', + }, + { + indexed: false, + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + { + indexed: false, + internalType: 'uint256', + name: 'value', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'islmAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'haqqAmount', + type: 'uint256', + }, + ], + name: 'MintHaqq', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'applicationId', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'haqqAmount', + type: 'uint256', + }, + ], + name: 'MintHaqqByApplication', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'granter', + type: 'address', + }, + { + indexed: false, + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'Revocation', + type: 'event', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'address', + name: 'granter', + type: 'address', + }, + { + internalType: 'string', + name: 'method', + type: 'string', + }, + ], + name: 'allowance', + outputs: [ + { + internalType: 'uint256', + name: 'remaining', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'approve', + outputs: [ + { + internalType: 'bool', + name: 'approved', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'uint256', + name: 'applicationId', + type: 'uint256', + }, + { + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'approveApplicationID', + outputs: [ + { + internalType: 'bool', + name: 'approved', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'islmAmount', + type: 'uint256', + }, + ], + name: 'calculate', + outputs: [ + { + internalType: 'uint256', + name: 'estimatedHaqqAmount', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'supplyBefore', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'supplyAfter', + type: 'uint256', + }, + { + internalType: 'string', + name: 'pricePerUnit', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'decreaseAllowance', + outputs: [ + { + internalType: 'bool', + name: 'approved', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'increaseAllowance', + outputs: [ + { + internalType: 'bool', + name: 'approved', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + internalType: 'uint256', + name: 'islmAmount', + type: 'uint256', + }, + ], + name: 'mintHaqq', + outputs: [ + { + internalType: 'uint256', + name: 'haqqAmount', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { + internalType: 'uint256', + name: 'applicationId', + type: 'uint256', + }, + ], + name: 'mintHaqqByApplication', + outputs: [ + { + internalType: 'uint256', + name: 'haqqAmount', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'revoke', + outputs: [ + { + internalType: 'bool', + name: 'revoked', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'grantee', + type: 'address', + }, + { + internalType: 'uint256', + name: 'applicationId', + type: 'uint256', + }, + { + internalType: 'string[]', + name: 'methods', + type: 'string[]', + }, + ], + name: 'revokeApplicationID', + outputs: [ + { + internalType: 'bool', + name: 'revoked', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx index bf38257f1..02b123300 100644 --- a/libs/burn-waitlist/src/lib/components/requests-list.tsx +++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx @@ -17,8 +17,11 @@ export interface RequestsListProps { >; canCancel: boolean; onCancel: (requestId: bigint) => void; + onMintHaqq?: (applicationId: bigint) => void; isCancelling?: boolean; cancellingRequestId?: bigint; + isMinting?: boolean; + mintingApplicationId?: bigint; balances?: WaitlistBalancesResponse; locale?: string; } @@ -27,8 +30,11 @@ export function RequestsList({ applications, canCancel, onCancel, + onMintHaqq, isCancelling = false, cancellingRequestId, + isMinting = false, + mintingApplicationId, balances, locale = 'en', }: RequestsListProps) { @@ -140,17 +146,36 @@ export function RequestsList({ )} - {canCancel && !isCancelled && !isPending && ( - - )} +
+ {onMintHaqq && + !isPending && + !isCancelled && + app.valid && + app.ready && ( + + )} + {canCancel && !isCancelled && !isPending && ( + + )} +
); diff --git a/libs/burn-waitlist/src/lib/constants/ethiq-config.ts b/libs/burn-waitlist/src/lib/constants/ethiq-config.ts new file mode 100644 index 000000000..ab3fb942a --- /dev/null +++ b/libs/burn-waitlist/src/lib/constants/ethiq-config.ts @@ -0,0 +1,5 @@ +/** + * Ethiq precompile contract address (same on all HAQQ chains) + */ +export const ETHIQ_PRECOMPILE_ADDRESS = + '0x0000000000000000000000000000000000000900' as `0x${string}`; diff --git a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts index eed8f7c57..77bd3939f 100644 --- a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts +++ b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts @@ -50,7 +50,7 @@ export function isWaitlistChainSupported(chainId?: number): boolean { */ // TODO: Change to mainnet when ready -export const WAITLIST_DEFAULT_CHAIN_ID = haqqTestedge2.id; +export const WAITLIST_DEFAULT_CHAIN_ID = haqqMainnet.id; /** * Backend API base URL diff --git a/libs/burn-waitlist/src/lib/hooks/index.ts b/libs/burn-waitlist/src/lib/hooks/index.ts index 45c48fada..ab94d7f02 100644 --- a/libs/burn-waitlist/src/lib/hooks/index.ts +++ b/libs/burn-waitlist/src/lib/hooks/index.ts @@ -7,3 +7,4 @@ export * from './use-waitlist-applications'; export * from './use-waitlist-price'; export * from './use-waitlist-price-chart'; export * from './use-waitlist-global-stats'; +export * from './use-ethiq-contract'; diff --git a/libs/burn-waitlist/src/lib/hooks/use-ethiq-contract.ts b/libs/burn-waitlist/src/lib/hooks/use-ethiq-contract.ts new file mode 100644 index 000000000..0301d7f3e --- /dev/null +++ b/libs/burn-waitlist/src/lib/hooks/use-ethiq-contract.ts @@ -0,0 +1,149 @@ +'use client'; + +import { + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, + useAccount, +} from 'wagmi'; +import { EthiqAbi } from '../abi/ethiq'; +import { ETHIQ_PRECOMPILE_ADDRESS } from '../constants/ethiq-config'; +import { WAITLIST_DEFAULT_CHAIN_ID } from '../constants/waitlist-config'; + +/** + * Hook to call calculate(islmAmount) on the Ethiq precompile. + * Returns estimated HAQQ amount, supply before/after, and price per unit. + */ +export function useEthiqCalculate(islmAmount?: bigint) { + const { chain } = useAccount(); + const chainId = chain?.id || WAITLIST_DEFAULT_CHAIN_ID; + + const { data, isLoading, error, refetch } = useReadContract({ + address: ETHIQ_PRECOMPILE_ADDRESS, + abi: EthiqAbi, + functionName: 'calculate', + args: islmAmount !== undefined ? [islmAmount] : undefined, + chainId, + query: { + enabled: islmAmount !== undefined && islmAmount > 0n, + }, + }); + + const result = data as [bigint, bigint, bigint, string] | undefined; + + console.log('result', result, error); + + return { + estimatedHaqqAmount: result?.[0], + supplyBefore: result?.[1], + supplyAfter: result?.[2], + pricePerUnit: result?.[3], + isLoading, + error, + refetch, + }; +} + +/** + * Hook to call mintHaqq(sender, receiver, islmAmount) on the Ethiq precompile. + * Used by regular users (not in waitlist) to burn ISLM and mint HAQQ. + */ +export function useMintHaqq() { + const { chain } = useAccount(); + const chainId = chain?.id; + + const { + writeContractAsync, + data: hash, + isPending, + error, + } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + }); + + const mintHaqq = async ( + sender: `0x${string}`, + receiver: `0x${string}`, + islmAmount: bigint, + ) => { + if (!writeContractAsync) { + throw new Error('Wallet not connected'); + } + + if (!chainId) { + throw new Error('Chain ID not available'); + } + + return writeContractAsync({ + address: ETHIQ_PRECOMPILE_ADDRESS, + abi: EthiqAbi, + functionName: 'mintHaqq', + args: [sender, receiver, islmAmount], + chainId, + }); + }; + + return { + mintHaqq, + hash, + isPending, + isConfirming, + isSuccess, + error, + }; +} + +/** + * Hook to call mintHaqqByApplication(sender, receiver, applicationId) on the Ethiq precompile. + * Used by waitlist participants to mint HAQQ for their approved applications. + */ +export function useMintHaqqByApplication() { + const { chain } = useAccount(); + const chainId = chain?.id; + + const { + writeContractAsync, + data: hash, + isPending, + error, + } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + hash, + chainId: chainId || WAITLIST_DEFAULT_CHAIN_ID, + }); + + const mintHaqqByApplication = async ( + sender: `0x${string}`, + receiver: `0x${string}`, + applicationId: bigint, + ) => { + if (!writeContractAsync) { + throw new Error('Wallet not connected'); + } + + if (!chainId) { + throw new Error('Chain ID not available'); + } + + return writeContractAsync({ + address: ETHIQ_PRECOMPILE_ADDRESS, + abi: EthiqAbi, + functionName: 'mintHaqqByApplication', + args: [sender, receiver, applicationId], + chainId, + }); + }; + + return { + mintHaqqByApplication, + hash, + isPending, + isConfirming, + isSuccess, + error, + }; +} diff --git a/libs/burn-waitlist/src/lib/mint-page.tsx b/libs/burn-waitlist/src/lib/mint-page.tsx new file mode 100644 index 000000000..a3de5003f --- /dev/null +++ b/libs/burn-waitlist/src/lib/mint-page.tsx @@ -0,0 +1,310 @@ +'use client'; + +import { useState, useCallback, useMemo, useRef, useEffect } from 'react'; +import { useAccount, useBalance, useSwitchChain } from 'wagmi'; +import { parseEther } from 'viem'; +import { Container } from '@haqq/shell-ui-kit/server'; +import { Button, ModalInput } from '@haqq/shell-ui-kit'; +import { formatEthDecimal } from '@haqq/shell-shared'; +import { useEthiqCalculate, useMintHaqq } from './hooks'; +import { + WAITLIST_DEFAULT_CHAIN_ID, + isWaitlistChainSupported, +} from './constants/waitlist-config'; +import { WalletConnectionWarning } from './components/wallet-connection-warning'; +import { NetworkWarning } from './components/network-warning'; + +function sanitizeErrorMessage( + error: Error | null | undefined, +): string | undefined { + if (!error) { + return undefined; + } + + const message = error instanceof Error ? error.message : String(error || ''); + + if (!message || message.trim() === '') { + return undefined; + } + + const messageLower = message.toLowerCase(); + + if ( + messageLower.includes('user rejected') || + messageLower.includes('user denied') || + messageLower.includes('denied transaction signature') || + messageLower.includes('rejected the request') + ) { + return 'Transaction rejected by user'; + } + + if (messageLower.includes('network') || messageLower.includes('fetch')) { + return 'Network error. Please check your connection and try again'; + } + + if ( + messageLower.includes('insufficient funds') || + messageLower.includes('insufficient balance') + ) { + return 'Insufficient balance'; + } + + if ( + messageLower.includes('execution reverted') || + messageLower.includes('revert') + ) { + const revertMatch = message.match(/execution reverted:?\s*(.+?)(?:\n|$)/i); + if (revertMatch && revertMatch[1] && revertMatch[1].trim().length < 100) { + return `Transaction failed: ${revertMatch[1].trim()}`; + } + return 'Transaction failed. Please try again'; + } + + if (messageLower.includes('timeout') || messageLower.includes('expired')) { + return 'Transaction timed out. Please try again'; + } + + return message.length > 200 ? `${message.substring(0, 200)}...` : message; +} + +export function MintPage() { + const { address, isConnected, chain } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + const isCorrectChain = isWaitlistChainSupported(chain?.id); + const hasAttemptedSwitch = useRef(undefined); + + const [amount, setAmount] = useState(''); + + // Parse user input to bigint (wei) + const parsedAmount = useMemo(() => { + if (!amount || amount.trim() === '') { + return undefined; + } + try { + return parseEther(amount); + } catch { + return undefined; + } + }, [amount]); + + // Wallet balance + const { data: walletBalance, refetch: refetchBalance } = useBalance({ + address: address as `0x${string}` | undefined, + chainId: chain?.id || WAITLIST_DEFAULT_CHAIN_ID, + }); + + // Calculate estimated HAQQ amount + const { + estimatedHaqqAmount, + supplyBefore, + supplyAfter, + pricePerUnit, + isLoading: isCalculating, + } = useEthiqCalculate(parsedAmount); + + // Mint hook + const { + mintHaqq: mintHaqqTx, + isPending: isMinting, + isConfirming, + isSuccess, + hash: mintHash, + error: mintError, + } = useMintHaqq(); + + const hasProcessedSuccess = useRef(false); + + // Handle successful mint + useEffect(() => { + if (isSuccess && mintHash && !hasProcessedSuccess.current) { + hasProcessedSuccess.current = true; + setAmount(''); + refetchBalance(); + } + + if (!isSuccess) { + hasProcessedSuccess.current = false; + } + }, [isSuccess, mintHash, refetchBalance]); + + const handleSwitchChain = useCallback(async () => { + try { + await switchChainAsync({ chainId: WAITLIST_DEFAULT_CHAIN_ID }); + if (chain?.id) { + hasAttemptedSwitch.current = chain.id; + } + } catch (error) { + console.error('Failed to switch chain:', error); + } + }, [switchChainAsync, chain?.id]); + + // Auto switch chain + useEffect(() => { + if ( + isConnected && + chain?.id && + !isCorrectChain && + hasAttemptedSwitch.current !== chain.id + ) { + hasAttemptedSwitch.current = chain.id; + handleSwitchChain(); + } + }, [isConnected, chain?.id, isCorrectChain, handleSwitchChain]); + + const handleMaxClick = useCallback(() => { + if (walletBalance?.value && walletBalance.value > 0n) { + const formatted = formatEthDecimal(walletBalance.value, 18); + setAmount(formatted); + } + }, [walletBalance?.value]); + + const handleSubmit = useCallback(async () => { + if (!address || !parsedAmount || parsedAmount <= 0n) { + return; + } + + try { + await mintHaqqTx(address, address, parsedAmount); + } catch (error) { + console.error('Failed to mint HAQQ:', error); + } + }, [address, parsedAmount, mintHaqqTx]); + + const isSubmitting = isMinting || isConfirming; + const errorMessage = useMemo( + () => sanitizeErrorMessage(mintError || undefined), + [mintError], + ); + + const isValid = + parsedAmount !== undefined && + parsedAmount > 0n && + walletBalance?.value !== undefined && + parsedAmount <= walletBalance.value; + + const formattedBalance = useMemo(() => { + if (!walletBalance?.value) { + return '0'; + } + return formatEthDecimal(walletBalance.value, 4); + }, [walletBalance?.value]); + + return ( + +
+
+

+ Burn ISLM & Mint HAQQ +

+

+ Burn your ISLM tokens and receive HAQQ tokens in return. The + exchange rate is determined by the bonding curve. +

+ + {!isConnected && } + + {isConnected && !isCorrectChain && ( + + )} + + {isConnected && isCorrectChain && ( +
+ {/* Amount input */} +
+ + { + if (value === undefined || value === '') { + setAmount(''); + } else { + setAmount(value); + } + }} + onMaxButtonClick={handleMaxClick} + hint={ + + Available Balance: {formattedBalance} ISLM + + } + isMaxButtonDisabled={ + !walletBalance?.value || walletBalance.value <= 0n + } + /> +
+ + {/* Calculation results */} + {parsedAmount && parsedAmount > 0n && ( +
+
+ + Estimated HAQQ to receive + + + {isCalculating + ? 'Calculating...' + : estimatedHaqqAmount !== undefined + ? `${formatEthDecimal(estimatedHaqqAmount, 4, 18)} HAQQ` + : '—'} + +
+ {pricePerUnit && ( +
+ Price per HAQQ + + {pricePerUnit} ISLM + +
+ )} + {supplyBefore !== undefined && supplyAfter !== undefined && ( +
+ Supply change + + {formatEthDecimal(supplyBefore, 2, 18)} →{' '} + {formatEthDecimal(supplyAfter, 2, 18)} + +
+ )} +
+ )} + + {/* Success message */} + {isSuccess && mintHash && ( +
+
+ HAQQ tokens minted successfully! +
+
+ )} + + {/* Error */} + {errorMessage && ( +
+
+ {errorMessage} +
+
+ )} + + {/* Submit */} +
+ +
+
+ )} +
+
+
+ ); +} diff --git a/libs/burn-waitlist/src/lib/waitlist-page.tsx b/libs/burn-waitlist/src/lib/waitlist-page.tsx index 5cb072a95..0607d1b25 100644 --- a/libs/burn-waitlist/src/lib/waitlist-page.tsx +++ b/libs/burn-waitlist/src/lib/waitlist-page.tsx @@ -13,6 +13,7 @@ import { useWaitlistPrice, useWaitlistPriceChart, useWaitlistGlobalStats, + useMintHaqqByApplication, } from './hooks'; import type { Application } from './hooks/use-waitlist-applications'; import { useBackendSignature } from './hooks/use-backend-signature'; @@ -186,9 +187,22 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { error: cancelError, } = useCancelWaitlistRequest(); + // Mint HAQQ by application + const { + mintHaqqByApplication: mintHaqqByApplicationTx, + isPending: isMintingByApp, + isConfirming: isConfirmingMintByApp, + isSuccess: isMintByAppSuccess, + hash: mintByAppHash, + error: mintByAppError, + } = useMintHaqqByApplication(); + const [cancellingRequestId, setCancellingRequestId] = useState< bigint | undefined >(); + const [mintingApplicationId, setMintingApplicationId] = useState< + bigint | undefined + >(); // Track pending transactions for optimistic updates const [pendingApplications, setPendingApplications] = useState< @@ -413,6 +427,45 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { ], ); + // Handle mint HAQQ by application + const handleMintHaqqByApplication = useCallback( + async (applicationId: bigint) => { + if (!address) { + return; + } + + try { + setMintingApplicationId(applicationId); + await mintHaqqByApplicationTx(address, address, applicationId); + + // Refetch after successful mint + refetchApplications(); + refetchBalances(); + refetchContractState(); + + const intervalId = setInterval(() => { + refetchApplications(); + refetchBalances(); + }, 2000); + + setTimeout(() => { + clearInterval(intervalId); + }, 10000); + } catch (error) { + console.error('Failed to mint HAQQ by application:', error); + } finally { + setMintingApplicationId(undefined); + } + }, + [ + address, + mintHaqqByApplicationTx, + refetchApplications, + refetchBalances, + refetchContractState, + ], + ); + // Automatically remove pending applications when they appear in backend data // This ensures smooth transition from pending to confirmed state useEffect(() => { @@ -608,8 +661,11 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { const isSubmitting = isCreating || isConfirmingCreate || isLoadingSignature; const errorMessage = useMemo( - () => sanitizeErrorMessage(createError || cancelError || undefined), - [createError, cancelError], + () => + sanitizeErrorMessage( + createError || cancelError || mintByAppError || undefined, + ), + [createError, cancelError, mintByAppError], ); // Merge applications with pending ones, sort by requestId descending (newest first) @@ -824,8 +880,11 @@ export function WaitlistPage({ locale = 'en' }: WaitlistPageProps = {}) { applications={mergedApplications} canCancel={canWithdraw || false} onCancel={handleCancel} + onMintHaqq={handleMintHaqqByApplication} isCancelling={isCancelling || isConfirmingCancel} cancellingRequestId={cancellingRequestId} + isMinting={isMintingByApp || isConfirmingMintByApp} + mintingApplicationId={mintingApplicationId} balances={waitlistBalances} locale={locale} />