diff --git a/src/app/assessment/components/Question.tsx b/src/app/assessment/components/Question.tsx index 1fd0b88..45451aa 100644 --- a/src/app/assessment/components/Question.tsx +++ b/src/app/assessment/components/Question.tsx @@ -10,6 +10,7 @@ export interface QuestionProps { onChange: (id: string, value: number) => void; isLoading?: boolean; isSuccess?: boolean; + isAnswered?: boolean; explorerButton?: React.ReactNode; } @@ -28,8 +29,9 @@ export default function Question({ text, value, onChange, - isLoading, - isSuccess, + isLoading = false, + isSuccess = false, + isAnswered = false, explorerButton, }: QuestionProps) { // Treat 0 as "no selection" @@ -59,7 +61,7 @@ export default function Question({ htmlFor={`${id}-${t.value}`} className="flex flex-col items-center cursor-pointer" > - {isSuccess && value === t.value ? ( + {(isSuccess && value === t.value) || (isAnswered && value === t.value) ? (
) : isLoading && value === t.value ? (
diff --git a/src/app/assessment/components/web3.tsx b/src/app/assessment/components/web3.tsx index 4f5994b..ddd6ab5 100644 --- a/src/app/assessment/components/web3.tsx +++ b/src/app/assessment/components/web3.tsx @@ -12,10 +12,14 @@ import { useContainerSize } from '@/hooks/useContainerSize'; import { useAccount, useChainId } from 'wagmi'; import { useDepositTriple } from '@/hooks/useDepositTriple'; import { MULTIVAULT_CONTRACT_ADDRESS, BLOCK_EXPLORER_URL } from '@/config/blockchain'; -import { parseUnits } from 'viem'; +import { parseUnits, formatUnits } from 'viem'; import { Abi } from 'viem'; import { multivaultAbi } from '@/lib/abis/multivault'; import { baseSepolia } from 'viem/chains'; +import { flushSync } from "react-dom"; +import { useMultivaultContract } from '@/hooks/useMultivaultContract'; +import { useContractRead } from 'wagmi'; +import { useGetTriplesWithPositionsQuery } from '@0xintuition/graphql'; const ANIM = { duration: 0.3 }; const STORAGE_ANS = "plebs_answers_web3"; @@ -31,6 +35,7 @@ interface TransactionStatus { interface PendingTransaction { questionId: string; tripleId: number; + answer: number; } export default function Web3Assessment() { @@ -44,6 +49,24 @@ export default function Web3Assessment() { onReceipt } = useDepositTriple(MULTIVAULT_CONTRACT_ADDRESS); + // Get minimum deposit amount from contract + const { data: generalConfig } = useContractRead({ + address: MULTIVAULT_CONTRACT_ADDRESS as `0x${string}`, + abi: multivaultAbi as Abi, + functionName: 'generalConfig', + chainId: baseSepolia.id, + }) as { data: [string, string, bigint, bigint, bigint, bigint, bigint, bigint] | undefined }; + + const minDeposit = generalConfig ? formatUnits(generalConfig[3], 18) : '0.001'; + + // Get positions for all triples + const { data: positionsData, isLoading: isLoadingPositions } = useGetTriplesWithPositionsQuery({ + where: { + id: { _in: questions.map(q => q.triple.id) } + }, + address: address?.toLowerCase() as `0x${string}` + }); + const [answers, setAnswers] = useState>({}); const [currentIndex, setCurrentIndex] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); @@ -52,21 +75,110 @@ export default function Web3Assessment() { const [pendingTransactions, setPendingTransactions] = useState([]); const [isProcessingQueue, setIsProcessingQueue] = useState(false); + // Initialize answers based on positions + useEffect(() => { + if (!positionsData?.triples || !address) return; + + const newAnswers: Record = {}; + let firstUnansweredIndex = -1; + + questions.forEach((question, index) => { + const position = positionsData.triples.find( + (p: { id: number }) => p.id == question.triple.id + ); + + if (position) { + // If user has shares in vault, they agreed + if (position.vault?.positions?.[0]?.shares > 0) { + let shares = Number(position.vault?.positions?.[0]?.shares); + console.log("Shares", shares); + if (shares > 0) { // 612202501000000 + newAnswers[question.id] = 5; // Slightly Agree + } if (shares > 1000000000000000) { // 1224405001000000 + newAnswers[question.id] = 6; // Agree + } if (shares > 1600000000000000) { // 1836607501000000 + newAnswers[question.id] = 7; // Strongly Agree + } + } + // If user has shares in counter vault, they disagreed + else if (position.counter_vault?.positions?.[0]?.shares > 0) { + let shares = Number(position.counter_vault?.positions?.[0]?.shares); + console.log("Shares", shares); + if (shares > 0) { // 612202501000000 + newAnswers[question.id] = 3; // Slightly Disagree + } if (shares > 1000000000000000) { // 1224405001000000 + newAnswers[question.id] = 2; // Disagree + } if (shares > 1600000000000000) { // 1836607501000000 + newAnswers[question.id] = 1; // Strongly Disagree + } + } + } else if (firstUnansweredIndex === -1) { + // If this is the first unanswered question, set it as current + firstUnansweredIndex = index; + } + }); + + setAnswers(newAnswers); + if (firstUnansweredIndex === -1) { + // All questions answered + setCurrentIndex(questions.length - 1); + } else { + setCurrentIndex(firstUnansweredIndex); + } + }, [positionsData, address]); + // Process transaction queue useEffect(() => { const processQueue = async () => { - if (isProcessingQueue || pendingTransactions.length === 0 || !address) return; + if (isProcessingQueue || pendingTransactions.length === 0 || !address || !positionsData?.triples) return; setIsProcessingQueue(true); const currentTx = pendingTransactions[0]; + const answer = answers[currentTx.questionId]; + + // Skip if answer is neutral (4) + if (answer === 4) { + setTransactionStatuses(prev => ({ + ...prev, + [currentTx.questionId]: { + questionId: currentTx.questionId, + status: 'success' + } + })); + setPendingTransactions(prev => prev.slice(1)); + setIsProcessingQueue(false); + return; + } + + // Calculate deposit amount based on answer + let multiplier = 0; + if (answer <= 3) { // Disagree (1-3) + multiplier = 4 - answer; // 3x for 1, 2x for 2, 1x for 3 + } else if (answer >= 5) { // Agree (5-7) + multiplier = answer - 4; // 1x for 5, 2x for 6, 3x for 7 + } + + const depositAmount = parseUnits( + (Number(minDeposit) * multiplier).toString(), + 18 + ); try { + // Find the triple in positionsData to get the correct vault ID + const triple = positionsData.triples.find(t => t.id === currentTx.tripleId.toString()); + if (!triple) { + throw new Error('Triple not found in positions data'); + } + + // Get the correct vault ID based on the answer + const vaultId = answer <= 3 ? BigInt(triple.counter_vault_id) : BigInt(triple.vault_id); + const hash = await writeContractAsync({ address: MULTIVAULT_CONTRACT_ADDRESS as `0x${string}`, abi: multivaultAbi as Abi, functionName: 'depositTriple', - args: [address as `0x${string}`, BigInt(currentTx.tripleId)], - value: parseUnits('0.001', 18), + args: [address as `0x${string}`, vaultId], + value: depositAmount, chain: baseSepolia }); @@ -104,17 +216,24 @@ export default function Web3Assessment() { }; processQueue(); - }, [pendingTransactions, isProcessingQueue, address, writeContractAsync, onReceipt]); + }, [pendingTransactions, isProcessingQueue, address, writeContractAsync, onReceipt, answers, minDeposit, positionsData]); // Hydrate once on mount (fire-and-forget) useEffect(() => { if (typeof window === "undefined" || !address) return; try { const a = sessionStorage.getItem(STORAGE_ANS); - if (a) setAnswers(JSON.parse(a)); - const idx = parseInt(sessionStorage.getItem(STORAGE_IDX) || "", 10); - if (!isNaN(idx) && idx >= 0 && idx < total) { - setCurrentIndex(idx); + if (a) { + const parsedAnswers = JSON.parse(a); + setAnswers(parsedAnswers); + // Find the first unanswered question + const firstUnansweredIndex = questions.findIndex(q => parsedAnswers[q.id] === undefined); + if (firstUnansweredIndex !== -1) { + setCurrentIndex(firstUnansweredIndex); + } else { + // If all questions are answered, set to the last question + setCurrentIndex(questions.length - 1); + } } } catch { } }, [total, address]); @@ -154,7 +273,8 @@ export default function Web3Assessment() { if (question?.triple) { setPendingTransactions(prev => [...prev, { questionId: id, - tripleId: question.triple.id + tripleId: question.triple.id, + answer: value }]); // Set initial pending status @@ -204,8 +324,13 @@ export default function Web3Assessment() { [answers, router, total, address] ); - const visible = questions.slice(0, currentIndex + 1); + + const allAnswered = Object.keys(answers).length === total; + const answeredCount = Object.keys(answers).length; + const remainingCount = total - answeredCount; + const visible = questions.slice(0, answeredCount); + return (
@@ -220,10 +345,12 @@ export default function Web3Assessment() {
)} +
-

- Question {currentIndex + 1} of {total} -

+
+

{answeredCount} question{answeredCount !== 1 ? 's' : ''} already replied

+

{remainingCount} / {total} question{remainingCount !== 1 ? 's' : ''} left

+
- {formError && ( + {formError ? (

{formError}

- )} + ) : null}
); })} + + - ); + + ) } \ No newline at end of file diff --git a/src/app/triples/page.tsx b/src/app/triples/page.tsx index e3d989c..9f10fb0 100644 --- a/src/app/triples/page.tsx +++ b/src/app/triples/page.tsx @@ -4,6 +4,11 @@ import React from 'react'; import { useAccount } from 'wagmi'; import { useGetTriplesWithPositionsQuery, GetTriplesWithPositionsQuery } from '@0xintuition/graphql'; import { questions } from '../assessment/components/questions'; +import { useRedeemTriple } from '../../hooks/useRedeemTriple'; +import { CURRENT_ENV } from '../../config/blockchain'; +import { getChainEnvConfig } from '../../lib/environment'; +import { parseUnits } from 'viem'; +import { multivaultAbi } from '../../lib/abis/multivault'; // Import the ABI // Extract all triple IDs from questions const allTripleIds = questions.map(q => q.triple.id); @@ -17,20 +22,76 @@ const formatUSD = (ethAmount: number) => { // Helper function to format percentages const formatPercentage = (value: number) => { + if (isNaN(value) || !isFinite(value)) return '0.00%'; // Handle NaN/Infinity return `${value.toPrecision(3).split('e')[0]}%`; }; export default function TriplesPage() { const { address } = useAccount(); + const { contractAddress: multivaultContractAddress } = getChainEnvConfig(CURRENT_ENV); + const redeemTripleHook = useRedeemTriple(multivaultContractAddress); // Fetch positions for all triples - const { data: positionsData, isLoading, error } = useGetTriplesWithPositionsQuery({ + const { data: positionsData, isLoading: isLoadingPositions, error: positionsError } = useGetTriplesWithPositionsQuery({ where: { id: { _in: allTripleIds } }, address: address?.toLowerCase() as `0x${string}` }); + const handleWithdraw = async (tripleIdFromCard: string, vaultSharesFromCard?: number, counterVaultSharesFromCard?: number) => { + if (!address || !redeemTripleHook || !redeemTripleHook.writeContractAsync) { + console.error("Withdraw prerequisites not met: address, redeemTripleHook, or writeContractAsync missing."); + return; + } + + let sharesToRedeemBigInt: bigint; + let idForContractTx = BigInt(tripleIdFromCard); + let attemptingToRedeem: "For" | "Against" | null = null; + + if (vaultSharesFromCard && vaultSharesFromCard > 0) { + sharesToRedeemBigInt = parseUnits(vaultSharesFromCard.toString(), 0); + attemptingToRedeem = "For"; + console.log(`Preparing to withdraw ${sharesToRedeemBigInt} 'For' shares from vault ID ${idForContractTx}`); + } else if (counterVaultSharesFromCard && counterVaultSharesFromCard > 0) { + // === CRITICAL SECTION REGARDING COUNTER VAULT ID === + // If redeeming 'Against' shares requires a *different* vault ID + // (e.g., a specific counter-vault ID not equal to tripleIdFromCard), + // then this section needs modification. + // For now, we are proceeding with tripleIdFromCard for the 'id', which may be incorrect. + // Consider disabling this block or implementing logic to get the correct counter-vault ID. + console.warn("Attempting to redeem 'Against' shares. This might fail if 'tripleIdFromCard' is not the correct ID for the counter-vault."); + sharesToRedeemBigInt = parseUnits(counterVaultSharesFromCard.toString(), 0); + attemptingToRedeem = "Against"; + // idForContractTx is still BigInt(tripleIdFromCard) - this is the potential issue point. + console.log(`Preparing to withdraw ${sharesToRedeemBigInt} 'Against' shares using vault ID ${idForContractTx}.`); + // === END CRITICAL SECTION === + } else { + console.log("No shares available to withdraw for this position on triple:", tripleIdFromCard); + return; + } + + if (!attemptingToRedeem) { // Should not happen if logic above is correct + console.error("Logical error: No redeem type determined."); + return; + } + + console.log(`Calling redeemTriple with args: [shares=${sharesToRedeemBigInt}, receiver=${address}, id=${idForContractTx}] for ${attemptingToRedeem} position.`); + + try { + const tx = await redeemTripleHook.writeContractAsync({ + address: multivaultContractAddress, + abi: multivaultAbi, + functionName: 'redeemTriple', + args: [sharesToRedeemBigInt, address, idForContractTx] + }); + console.log("Withdraw transaction submitted:", tx, "for", attemptingToRedeem, "position."); + // Optionally, call redeemTripleHook.reset() here or on success + } catch (err) { + console.error(`Withdrawal failed for ${attemptingToRedeem} position:`, err); + } + }; + if (!address) { return (
@@ -40,7 +101,7 @@ export default function TriplesPage() { ); } - if (isLoading) { + if (isLoadingPositions) { return (

My Triple Positions

@@ -49,50 +110,36 @@ export default function TriplesPage() { ); } - if (error) { + if (positionsError) { return (

My Triple Positions

-

Error loading positions: {error.toString()}

+

Error loading positions: {positionsError.toString()}

); } const triples = positionsData?.triples || []; const total = positionsData?.total.aggregate?.count || 0; - - // Calculate total ETH amount across all positions - const totalEthAmount = triples.reduce((sum, triple) => { - const vaultPosition = triple.vault?.positions[0]; - const counterVaultPosition = triple.counter_vault?.positions[0]; - - const vaultEthAmount = vaultPosition ? (vaultPosition.shares * triple.vault?.current_share_price) : 0; - const counterVaultEthAmount = counterVaultPosition ? (counterVaultPosition.shares * triple.counter_vault?.current_share_price) : 0; - - return sum + vaultEthAmount + counterVaultEthAmount; - }, 0); + const isRedeeming = redeemTripleHook.awaitingWalletConfirmation || redeemTripleHook.awaitingOnChainConfirmation; return (

My Triple Positions

-
-

Total positions: {total}

-

- Total Value: {formatUSD(totalEthAmount)} -

-
+

Total positions: {total}

+ {isRedeeming &&

Processing withdrawal...

} + {redeemTripleHook.isError &&

Withdrawal error occurred. Please check console.

} + {/* Access receipt when available: redeemTripleHook.receipt */} {triples.length === 0 ? ( -

You don't have any positions yet.

+

You don't have any positions yet.

) : (
{triples.map((triple: NonNullable[number]) => { - // Find the corresponding question for this triple const question = questions.find(q => q.triple.id === parseInt(triple.id)); const vaultPosition = triple.vault?.positions[0]; const counterVaultPosition = triple.counter_vault?.positions[0]; - // Calculate ETH amounts const vaultEthAmount = vaultPosition ? (vaultPosition.shares * triple.vault?.current_share_price) : 0; const counterVaultEthAmount = counterVaultPosition ? (counterVaultPosition.shares * triple.counter_vault?.current_share_price) : 0; const totalVaultEth = triple.vault?.total_shares * triple.vault?.current_share_price; @@ -136,6 +183,15 @@ export default function TriplesPage() { )}
+ {(vaultPosition || counterVaultPosition) && ( + + )}
); diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index d99ca11..851ac71 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -17,8 +17,8 @@ export function Logomark({ className = "", ...props }: SVGProps) {...props} > - - + + ) diff --git a/src/hooks/useRedeemTriple.ts b/src/hooks/useRedeemTriple.ts new file mode 100644 index 0000000..35064e6 --- /dev/null +++ b/src/hooks/useRedeemTriple.ts @@ -0,0 +1,19 @@ +import { CURRENT_ENV } from "../config/blockchain"; +import { type GetContractReturnType } from "viem"; + +import { getChainEnvConfig } from "../lib/environment"; +import { useContractWriteAndWait } from "./useContractWriteAndWait"; +import { useMultivaultContract } from "./useMultivaultContract"; + +export const useRedeemTriple = (contract: string) => { + const multivault = useMultivaultContract( + contract, + getChainEnvConfig(CURRENT_ENV).chainId + ) as GetContractReturnType; + + return useContractWriteAndWait({ + ...multivault, + // @ts-ignore TODO: Fix type for useContractWriteAndWait + functionName: "redeemTriple", + }); +};