From 008551955fc6a24142e991332b046c3bfa6adb54 Mon Sep 17 00:00:00 2001 From: Jeremie Date: Fri, 16 May 2025 00:49:41 +0200 Subject: [PATCH 1/7] create triples page --- src/app/triples/page.tsx | 20 ++------------------ src/components/Icons.tsx | 4 ++-- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/app/triples/page.tsx b/src/app/triples/page.tsx index e3d989c..7882767 100644 --- a/src/app/triples/page.tsx +++ b/src/app/triples/page.tsx @@ -61,29 +61,13 @@ export default function TriplesPage() { 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); - return (

My Triple Positions

-
-

Total positions: {total}

-

- Total Value: {formatUSD(totalEthAmount)} -

-
+

Total positions: {total}

{triples.length === 0 ? ( -

You don't have any positions yet.

+

You don't have any positions yet.

) : (
{triples.map((triple: NonNullable[number]) => { diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index deffea9..7bb439d 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -17,8 +17,8 @@ export function Logomark({ className = "", ...props }: SVGProps) {...props} > - - + + ) From 53d30b697ade30534f860484e90393a826013fa5 Mon Sep 17 00:00:00 2001 From: Jeremie Date: Fri, 16 May 2025 02:58:31 +0200 Subject: [PATCH 2/7] WIP - add redeem to triple page --- src/app/triples/page.tsx | 84 +++++++++++++++++++++++++++++++++--- src/hooks/useRedeemTriple.ts | 19 ++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useRedeemTriple.ts diff --git a/src/app/triples/page.tsx b/src/app/triples/page.tsx index 7882767..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,34 +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; + const isRedeeming = redeemTripleHook.awaitingWalletConfirmation || redeemTripleHook.awaitingOnChainConfirmation; return (

My Triple Positions

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.

) : (
{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; @@ -120,6 +183,15 @@ export default function TriplesPage() { )}
+ {(vaultPosition || counterVaultPosition) && ( + + )}
); 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", + }); +}; From f35d44351d2ed64e515f2de3f17ec032a3a9dd31 Mon Sep 17 00:00:00 2001 From: Jeremie Date: Fri, 16 May 2025 23:48:04 +0200 Subject: [PATCH 3/7] fix - deposit amount depending on reply --- src/app/assessment/components/web3.tsx | 51 +++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/app/assessment/components/web3.tsx b/src/app/assessment/components/web3.tsx index d58e307..2f0c079 100644 --- a/src/app/assessment/components/web3.tsx +++ b/src/app/assessment/components/web3.tsx @@ -1,4 +1,3 @@ - "use client"; import React, { useState, useCallback, useEffect } from "react"; @@ -13,11 +12,13 @@ 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'; const ANIM = { duration: 0.3 }; const STORAGE_ANS = "plebs_answers_web3"; @@ -33,6 +34,7 @@ interface TransactionStatus { interface PendingTransaction { questionId: string; tripleId: number; + answer: number; } export default function Web3Assessment() { @@ -46,6 +48,16 @@ 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'; // minDeposit is at index 3 + const [answers, setAnswers] = useState>({}); const [currentIndex, setCurrentIndex] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); @@ -61,6 +73,34 @@ export default function Web3Assessment() { 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 { const hash = await writeContractAsync({ @@ -68,7 +108,7 @@ export default function Web3Assessment() { abi: multivaultAbi as Abi, functionName: 'depositTriple', args: [address as `0x${string}`, BigInt(currentTx.tripleId)], - value: parseUnits('0.001', 18), + value: depositAmount, chain: baseSepolia }); @@ -106,7 +146,7 @@ export default function Web3Assessment() { }; processQueue(); - }, [pendingTransactions, isProcessingQueue, address, writeContractAsync, onReceipt]); + }, [pendingTransactions, isProcessingQueue, address, writeContractAsync, onReceipt, answers, minDeposit]); // Hydrate once on mount (fire-and-forget) useEffect(() => { @@ -158,7 +198,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 From f6fd6a94f95d56267931d2e9a34c05d09c247055 Mon Sep 17 00:00:00 2001 From: Jeremie Date: Sat, 17 May 2025 00:19:23 +0200 Subject: [PATCH 4/7] fix initial state --- src/app/assessment/components/Question.tsx | 6 +- src/app/assessment/components/web3.tsx | 217 +++++++++++++-------- 2 files changed, 140 insertions(+), 83 deletions(-) diff --git a/src/app/assessment/components/Question.tsx b/src/app/assessment/components/Question.tsx index 1fd0b88..3714dc7 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" diff --git a/src/app/assessment/components/web3.tsx b/src/app/assessment/components/web3.tsx index 2f0c079..5daffa0 100644 --- a/src/app/assessment/components/web3.tsx +++ b/src/app/assessment/components/web3.tsx @@ -19,6 +19,7 @@ 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"; @@ -56,7 +57,15 @@ export default function Web3Assessment() { chainId: baseSepolia.id, }) as { data: [string, string, bigint, bigint, bigint, bigint, bigint, bigint] | undefined }; - const minDeposit = generalConfig ? formatUnits(generalConfig[3], 18) : '0.001'; // minDeposit is at index 3 + 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); @@ -66,6 +75,39 @@ 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 = 0; + + 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) { + newAnswers[question.id] = 7; // Strongly Agree + } + // If user has shares in counter vault, they disagreed + else if (position.counter_vault?.positions?.[0]?.shares > 0) { + newAnswers[question.id] = 1; // Strongly Disagree + } + } else { + // If this is the first unanswered question, set it as current + if (firstUnansweredIndex === 0) { + firstUnansweredIndex = index; + } + } + }); + + setAnswers(newAnswers); + setCurrentIndex(firstUnansweredIndex); + }, [positionsData, address]); + // Process transaction queue useEffect(() => { const processQueue = async () => { @@ -251,6 +293,8 @@ export default function Web3Assessment() { const visible = questions.slice(0, currentIndex + 1); const allAnswered = Object.keys(answers).length === total; + const answeredCount = Object.keys(answers).length; + const remainingCount = total - answeredCount; return (
@@ -268,87 +312,98 @@ export default function Web3Assessment() {
)} -
-

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

- -
- - {formError && ( -

- {formError} -

- )} - -
-
- + {isLoadingPositions ? ( +
+

Loading your previous answers...

-
- - - {visible - .slice() - .reverse() - .map((q, revIdx) => { - const idx = visible.length - 1 - revIdx; - const isActive = idx === currentIndex; - const txStatus = transactionStatuses[q.id]; - - return ( - - window.open(`${BLOCK_EXPLORER_URL}/tx/${txStatus.txHash}`, '_blank')} - > - - - - - - Explorer - - ) - } - /> - - ); - })} - + ) : ( + <> +
+
+

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

+

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

+
+ +
+ + {formError && ( +

+ {formError} +

+ )} + +
+
+ +
+
+ + + {visible + .slice() + .reverse() + .map((q, revIdx) => { + const idx = visible.length - 1 - revIdx; + const isActive = idx === currentIndex; + const txStatus = transactionStatuses[q.id]; + const isAnswered = answers[q.id] !== undefined; + + return ( + + window.open(`${BLOCK_EXPLORER_URL}/tx/${txStatus.txHash}`, '_blank')} + > + + + + + + Explorer + + ) + } + /> + + ); + })} + + + )} ); } \ No newline at end of file From 7ec9870c53de17f7bdc23c4017713a48801020a3 Mon Sep 17 00:00:00 2001 From: Jeremie Date: Mon, 19 May 2025 15:37:00 +0200 Subject: [PATCH 5/7] fix negative deposit --- src/app/assessment/components/web3.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/assessment/components/web3.tsx b/src/app/assessment/components/web3.tsx index 65a6938..1f99846 100644 --- a/src/app/assessment/components/web3.tsx +++ b/src/app/assessment/components/web3.tsx @@ -114,7 +114,7 @@ export default function Web3Assessment() { // 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]; @@ -148,11 +148,20 @@ export default function Web3Assessment() { ); 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)], + args: [address as `0x${string}`, vaultId], value: depositAmount, chain: baseSepolia }); @@ -191,7 +200,7 @@ export default function Web3Assessment() { }; processQueue(); - }, [pendingTransactions, isProcessingQueue, address, writeContractAsync, onReceipt, answers, minDeposit]); + }, [pendingTransactions, isProcessingQueue, address, writeContractAsync, onReceipt, answers, minDeposit, positionsData]); // Hydrate once on mount (fire-and-forget) useEffect(() => { From 1e00f947e042c8e4db8a70a285185fcb55c9b99f Mon Sep 17 00:00:00 2001 From: Jeremie Date: Mon, 19 May 2025 16:27:08 +0200 Subject: [PATCH 6/7] fix web3 past response ratio --- src/app/assessment/components/web3.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/app/assessment/components/web3.tsx b/src/app/assessment/components/web3.tsx index 1f99846..ddd6ab5 100644 --- a/src/app/assessment/components/web3.tsx +++ b/src/app/assessment/components/web3.tsx @@ -90,11 +90,27 @@ export default function Web3Assessment() { if (position) { // If user has shares in vault, they agreed if (position.vault?.positions?.[0]?.shares > 0) { - newAnswers[question.id] = 7; // Strongly Agree + 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) { - newAnswers[question.id] = 1; // Strongly Disagree + 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 From 3b02e38ad318f8dc76a19ea4e1588944e0a671aa Mon Sep 17 00:00:00 2001 From: Jeremie Date: Mon, 19 May 2025 16:33:56 +0200 Subject: [PATCH 7/7] change default gray color to green for past question --- src/app/assessment/components/Question.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/assessment/components/Question.tsx b/src/app/assessment/components/Question.tsx index 3714dc7..45451aa 100644 --- a/src/app/assessment/components/Question.tsx +++ b/src/app/assessment/components/Question.tsx @@ -61,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 ? (