diff --git a/src/app/dashboard/pool-market/[id]/components/StakePool.tsx b/src/app/dashboard/pool-market/[id]/components/StakePool.tsx new file mode 100644 index 0000000..41a6d6e --- /dev/null +++ b/src/app/dashboard/pool-market/[id]/components/StakePool.tsx @@ -0,0 +1,228 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { truncate } from '@/lib/utils'; +import React, { useState } from 'react'; +import { FaSpinner } from 'react-icons/fa'; +import useStakePool from '@/app/hooks/useStakePool'; +import useWalletBalance from '@/app/hooks/useWalletBalance'; +import ConnectWalletModal from '@/components/ConnectWalletModal'; + +interface StakePoolProps { + poolId: string; + isConnected?: boolean; + address?: string; + onStakeSuccess?: () => void; +} + +export default function StakePool({ + poolId, + isConnected, + address, + onStakeSuccess +}: StakePoolProps) { + const [stakeAmount, setStakeAmount] = useState(''); + const [showConnectModal, setShowConnectModal] = useState(false); + const { stakeStatus, stakeOnPool } = useStakePool(poolId); + + // Get wallet balance using the dedicated hook + const { balance: walletBalance, isLoading: balanceLoading } = useWalletBalance({ + address, + enabled: isConnected, + }); + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + + // Allow empty string + if (value === '') { + setStakeAmount(''); + return; + } + + // Only allow valid number patterns (digits and single decimal point) + const numberRegex = /^\d*\.?\d*$/; + + if (numberRegex.test(value)) { + // Prevent multiple decimal points + const decimalCount = (value.match(/\./g) || []).length; + if (decimalCount <= 1) { + // Convert to number to check if it's positive + const numValue = parseFloat(value); + if (isNaN(numValue) || numValue >= 0) { + setStakeAmount(value); + } + } + } + }; + + const handleStake = async () => { + await stakeOnPool(stakeAmount); + if (onStakeSuccess) { + onStakeSuccess(); + } + // Clear the input after successful stake + setStakeAmount(''); + }; + + const isLoading = stakeStatus === 'pending'; + const stakeAmountNum = parseFloat(stakeAmount) || 0; + const isValidAmount = stakeAmount && stakeAmountNum > 0; + const isMinimumStake = stakeAmountNum >= 10; + const hasSufficientBalance = stakeAmountNum <= walletBalance; + const canStake = isValidAmount && isMinimumStake && hasSufficientBalance && isConnected; + + return ( +
+

+ Stake tokens on this pool without voting on a specific outcome. + Your stake will be distributed proportionally based on the final results. +

+ +
+
+ +
+ + {balanceLoading ? 'Loading...' : `${walletBalance.toFixed(2)} STRK`} + +
+
+ +
+
+
+
+
+ +
+ Enter amount to stake +
+
+
+
+ STRK +
+
+ Token +
+
+
+
+
+ +
+ {['10', '50', '100', '500'].map((amount) => ( + + ))} +
+
+ + {isConnected && stakeAmount && ( +
+ {!isMinimumStake && ( +
+ ⚠️ + Minimum stake is 10 STRK +
+ )} + {!hasSufficientBalance && ( +
+ ⚠️ + Insufficient balance. You have {walletBalance.toFixed(2)} STRK +
+ )} + {isValidAmount && isMinimumStake && hasSufficientBalance && ( +
+ + Ready to stake +
+ )} +
+ )} + + {/* Wallet Balance Display */} + {isConnected && ( +
+
+ Wallet Balance: + + {balanceLoading ? 'Loading...' : `${walletBalance.toFixed(2)} STRK`} + +
+
+ )} + + {/* Stake Button */} + {isLoading ? ( + + ) : !isConnected ? ( + + ) : !isValidAmount ? ( + + ) : !isMinimumStake ? ( + + ) : !hasSufficientBalance ? ( + + ) : ( + + )} + +
+ Stakes are locked until pool settlement +
+ + setShowConnectModal(false)} + /> +
+ ); +} diff --git a/src/app/dashboard/pool-market/[id]/page.tsx b/src/app/dashboard/pool-market/[id]/page.tsx index 190beef..d47c066 100644 --- a/src/app/dashboard/pool-market/[id]/page.tsx +++ b/src/app/dashboard/pool-market/[id]/page.tsx @@ -6,6 +6,7 @@ import { PREDIFI_ABI } from "@/app/abi/predifi_abi"; import { GET_POOL } from "@/constants/functionNames"; import { PoolCardDetails, PoolDescription } from "./components/poolDetails"; import PoolPrediction from "./components/poolPrediction"; +import StakePool from "./components/StakePool"; import SocialsShare from "./components/socialsShare"; import Comments from "@/components/ui/comments"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; @@ -36,11 +37,14 @@ export default function Market() { readData: pool, } = useContractFetch(PREDIFI_ABI, GET_POOL, [poolId]); - const { data: hasUserAlreadyParticipated, isLoading: isUserParticipatedLoading } = useGetUserParticiaptionInPool({ + const { + data: hasUserAlreadyParticipated, + isLoading: isUserParticipatedLoading, + } = useGetUserParticiaptionInPool({ enabled: isConnected, userAddress: address, - poolId: poolId - }) + poolId: poolId, + }); const [poolDetails, setPoolDetails] = useState(null); @@ -114,8 +118,8 @@ export default function Market() { -
-
+
+
{readIsLoading ? ( ) : readError ? ( @@ -152,11 +156,37 @@ export default function Market() { {poolDetails?.option2} {poolDetails?.totalStakeOption2}
-
- Total stake distribution:{" "} - - {poolDetails?.totalBetAmountStrk} - +
+
+ Total stake distribution:{" "} + + {poolDetails?.totalBetAmountStrk} STRK + +
+ {poolDetails && ( + + + + + + Stake + + + + Stake on Pool + + { + // Optionally refresh pool data after successful stake + window.location.reload(); + }} + /> + + + )}
)} @@ -216,27 +246,31 @@ export default function Market() {
- {poolDetails && ( - - )} +
+ {poolDetails && ( + + )} + + +
); diff --git a/src/app/hooks/useStakePool.ts b/src/app/hooks/useStakePool.ts new file mode 100644 index 0000000..ef6a2d8 --- /dev/null +++ b/src/app/hooks/useStakePool.ts @@ -0,0 +1,58 @@ +import { ActionType } from "@/lib/types"; +import { myProvider } from "@/lib/utils"; +import { PREDIFI_CONTRACT_ADDRESS } from "@/static"; +import { useAccount } from "@starknet-react/core"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { cairo, CallData } from "starknet"; + +export default function useStakePool(poolId: string) { + const { account } = useAccount(); + + const [stakeStatus, setStakeStatus] = useState("idle"); + + const stakeOnPool = async (amount: string) => { + if (!account) { + toast.error("Account not connected!"); + return; + } + + if (!amount || parseFloat(amount) <= 0) { + toast.error("Please enter a valid stake amount!"); + return; + } + + try { + setStakeStatus("pending"); + + const result = await account.execute({ + contractAddress: PREDIFI_CONTRACT_ADDRESS, + entrypoint: "stake", + calldata: CallData.compile({ + pool_id: cairo.uint256(poolId), + amount: cairo.uint256(amount), + }), + }); + + const status = await myProvider.waitForTransaction( + result.transaction_hash + ); + + if (status.isSuccess()) { + toast.success(`Success! Staked ${amount} STRK on the pool.`); + setStakeStatus("success"); + } + } catch (err) { + console.log(err); + setStakeStatus("error"); + toast.error("Error staking on pool. Please try again."); + } finally { + setStakeStatus("idle"); + } + }; + + return { + stakeStatus, + stakeOnPool, + }; +} diff --git a/src/app/hooks/useWalletBalance.ts b/src/app/hooks/useWalletBalance.ts new file mode 100644 index 0000000..1256454 --- /dev/null +++ b/src/app/hooks/useWalletBalance.ts @@ -0,0 +1,68 @@ +import { useState, useEffect } from 'react'; +import { useBalance } from '@starknet-react/core'; +import { constants } from 'starknet'; + +interface UseWalletBalanceProps { + address?: string; + enabled?: boolean; +} + +interface UseWalletBalanceReturn { + balance: number; + balanceFormatted: string; + isLoading: boolean; + error: Error | null; + refetch: () => void; +} + +export default function useWalletBalance({ + address, + enabled = true +}: UseWalletBalanceProps): UseWalletBalanceReturn { + const [balance, setBalance] = useState(0); + const [error, setError] = useState(null); + + // STRK token address (mainnet) + const STRK_TOKEN_ADDRESS = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'; + + const { + data: balanceData, + isLoading, + error: balanceError, + refetch + } = useBalance({ + address: address as `0x${string}`, + token: STRK_TOKEN_ADDRESS, + watch: true, + }); + + useEffect(() => { + if (balanceData?.value) { + try { + // Convert from wei to STRK (18 decimals) + const balanceInStrk = Number(balanceData.value) / 1e18; + setBalance(balanceInStrk); + setError(null); + } catch (err) { + setError(err as Error); + setBalance(0); + } + } else if (balanceError) { + setError(balanceError); + setBalance(0); + } + }, [balanceData, balanceError]); + + const balanceFormatted = balance.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 6 + }); + + return { + balance, + balanceFormatted, + isLoading, + error, + refetch: refetch || (() => {}), + }; +}