Last active
September 9, 2025 14:49
-
-
Save hydrogenbond007/f24c087acebeb579d167fc17e52a6fea to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @file Hook for campUSD operations using Nucleus SDK | |
| * @description Manages campUSD deposits and withdrawals using the Nucleus protocol | |
| */ | |
| import { useCallback, useState } from "react"; | |
| import { | |
| useAccount, | |
| useWriteContract, | |
| useWaitForTransactionReceipt, | |
| useSwitchChain, | |
| } from "wagmi"; | |
| import { | |
| prepareCampUSDDeposit, | |
| prepareCampUSDWithdraw, | |
| prepareCampUSDDepositApproval, | |
| prepareCampUSDWithdrawalApproval, | |
| isDepositSpendApproved, | |
| isWithdrawalSpendApproved, | |
| type CampUSDDepositParams, | |
| type CampUSDWithdrawParams, | |
| ETHEREUM_CHAIN_ID, | |
| } from "@/lib/nucleus/campusd"; | |
| import { | |
| type DepositTransactionData, | |
| type WithdrawTransactionData, | |
| type ApproveDepositTokenTransactionData, | |
| type ApproveWithdrawTokenTransactionData, | |
| } from "@molecularlabs/nucleus-frontend"; | |
| interface CampUSDOperationState { | |
| isLoading: boolean; | |
| isSuccess: boolean; | |
| isError: boolean; | |
| error: Error | null; | |
| txHash?: string; | |
| step?: | |
| | "checking-approval" | |
| | "approving" | |
| | "depositing" | |
| | "withdrawing" | |
| | "bridging" | |
| | "switching-chain" | |
| | "completed"; | |
| } | |
| interface ApprovalState { | |
| isApproved: boolean; | |
| allowance: string; | |
| allowanceAsBigInt: string; | |
| decimals: number; | |
| error: Error | null; | |
| isChecking: boolean; | |
| } | |
| export function useCampUSDNucleus() { | |
| const { address, chain } = useAccount(); | |
| const { writeContractAsync } = useWriteContract(); | |
| const { switchChainAsync } = useSwitchChain(); | |
| const [operationState, setOperationState] = useState<CampUSDOperationState>({ | |
| isLoading: false, | |
| isSuccess: false, | |
| isError: false, | |
| error: null, | |
| }); | |
| const [approvalState, setApprovalState] = useState<ApprovalState>({ | |
| isApproved: false, | |
| allowance: "0", | |
| allowanceAsBigInt: "0", | |
| decimals: 0, | |
| error: null, | |
| isChecking: false, | |
| }); | |
| /** | |
| * Check USDC approval status for deposit | |
| */ | |
| const checkDepositApproval = useCallback( | |
| async (depositAmount: string) => { | |
| if (!address) return; | |
| setApprovalState((prev) => ({ ...prev, isChecking: true, error: null })); | |
| try { | |
| const result = await isDepositSpendApproved({ | |
| userAddress: address, | |
| depositAmount, | |
| }); | |
| setApprovalState({ | |
| isApproved: result.isApproved, | |
| allowance: result.allowance, | |
| allowanceAsBigInt: result.allowanceAsBigInt, | |
| decimals: result.decimals, | |
| error: result.error, | |
| isChecking: false, | |
| }); | |
| return result; | |
| } catch (error) { | |
| const errorObj = | |
| error instanceof Error | |
| ? error | |
| : new Error("Failed to check approval"); | |
| setApprovalState((prev) => ({ | |
| ...prev, | |
| isChecking: false, | |
| error: errorObj, | |
| })); | |
| return null; | |
| } | |
| }, | |
| [address], | |
| ); | |
| /** | |
| * Deposits USDC and bridges to campUSD on Camp Network | |
| * Implements depositAndBridge flow: USDC (Ethereum) -> campUSD (Camp Network) | |
| */ | |
| const depositCampUSD = useCallback( | |
| async (params: Omit<CampUSDDepositParams, "userAddress">) => { | |
| if (!address) { | |
| throw new Error("Wallet not connected"); | |
| } | |
| setOperationState({ | |
| isLoading: true, | |
| isSuccess: false, | |
| isError: false, | |
| error: null, | |
| step: "checking-approval", | |
| }); | |
| try { | |
| // Step 1: Check if approval is needed | |
| const approvalStatus = await checkDepositApproval(params.depositAmount); | |
| let approvalHash: string | undefined; | |
| if (!approvalStatus?.isApproved) { | |
| // Step 2: Prepare and execute USDC approval if needed | |
| setOperationState((prev) => ({ ...prev, step: "approving" })); | |
| const approvalData: ApproveDepositTokenTransactionData = | |
| await prepareCampUSDDepositApproval({ | |
| userAddress: address, | |
| depositAmount: params.depositAmount, // Optional: specific amount to approve | |
| }); | |
| // Execute approval transaction | |
| approvalHash = await writeContractAsync({ | |
| ...approvalData, | |
| }); | |
| console.log("USDC approval transaction sent:", approvalHash); | |
| } | |
| // Step 3: Prepare and execute deposit and bridge transaction | |
| setOperationState((prev) => ({ ...prev, step: "depositing" })); | |
| const depositData: DepositTransactionData = | |
| await prepareCampUSDDeposit({ | |
| ...params, | |
| userAddress: address, | |
| }); | |
| // Execute deposit and bridge transaction | |
| const depositHash = await writeContractAsync({ | |
| ...depositData, | |
| }); | |
| console.log( | |
| "campUSD deposit and bridge transaction sent:", | |
| depositHash, | |
| ); | |
| setOperationState({ | |
| isLoading: false, | |
| isSuccess: true, | |
| isError: false, | |
| error: null, | |
| txHash: depositHash, | |
| step: "completed", | |
| }); | |
| return depositHash; | |
| } catch (error) { | |
| const errorMessage = | |
| error instanceof Error ? error : new Error("campUSD deposit failed"); | |
| setOperationState({ | |
| isLoading: false, | |
| isSuccess: false, | |
| isError: true, | |
| error: errorMessage, | |
| }); | |
| throw errorMessage; | |
| } | |
| }, | |
| [address, writeContractAsync, checkDepositApproval], | |
| ); | |
| /** | |
| * Withdraws campUSD and bridges back to USDC on Ethereum | |
| * Implements bridgeAndWithdraw flow: campUSD (Camp Network) -> USDC (Ethereum) | |
| * This is a two-step process: | |
| * 1. Bridge vault shares from Camp Network to Ethereum | |
| * 2. Withdraw vault shares on Ethereum to receive USDC | |
| */ | |
| const withdrawCampUSD = useCallback( | |
| async (params: Omit<CampUSDWithdrawParams, "userAddress">) => { | |
| if (!address) { | |
| throw new Error("Wallet not connected"); | |
| } | |
| setOperationState({ | |
| isLoading: true, | |
| isSuccess: false, | |
| isError: false, | |
| error: null, | |
| step: "withdrawing", | |
| }); | |
| try { | |
| // Prepare withdraw transaction data | |
| const withdrawData: WithdrawTransactionData = | |
| await prepareCampUSDWithdraw({ | |
| ...params, | |
| userAddress: address, | |
| }); | |
| console.log("campUSD withdraw data prepared:", { | |
| chainId: withdrawData.chainId, | |
| }); | |
| // Step 1: Ensure we're on Ethereum for the withdraw transaction | |
| if (chain?.id !== ETHEREUM_CHAIN_ID) { | |
| setOperationState((prev) => ({ ...prev, step: "switching-chain" })); | |
| await switchChainAsync({ chainId: ETHEREUM_CHAIN_ID }); | |
| } | |
| // Step 2: Execute withdraw transaction on Ethereum (AtomicQueue) | |
| setOperationState((prev) => ({ ...prev, step: "withdrawing" })); | |
| const withdrawHash = await writeContractAsync({ | |
| ...withdrawData, | |
| }); | |
| console.log("campUSD withdraw transaction sent on Ethereum:", withdrawHash); | |
| setOperationState({ | |
| isLoading: false, | |
| isSuccess: true, | |
| isError: false, | |
| error: null, | |
| txHash: withdrawHash, | |
| step: "completed", | |
| }); | |
| return withdrawHash; | |
| } catch (error) { | |
| const errorMessage = | |
| error instanceof Error ? error : new Error("campUSD withdraw failed"); | |
| setOperationState({ | |
| isLoading: false, | |
| isSuccess: false, | |
| isError: true, | |
| error: errorMessage, | |
| }); | |
| throw errorMessage; | |
| } | |
| }, | |
| [address, chain?.id, writeContractAsync, switchChainAsync], | |
| ); | |
| /** | |
| * Checks if a specific amount can be deposited | |
| */ | |
| const canDeposit = useCallback( | |
| (amount: string): boolean => { | |
| if (!address || !amount) return false; | |
| const depositAmount = parseFloat(amount); | |
| return depositAmount > 0; | |
| }, | |
| [address], | |
| ); | |
| /** | |
| * Checks if a specific amount can be withdrawn | |
| */ | |
| const canWithdraw = useCallback( | |
| (amount: string): boolean => { | |
| if (!address || !amount) return false; | |
| const withdrawAmount = parseFloat(amount); | |
| return withdrawAmount > 0; | |
| }, | |
| [address], | |
| ); | |
| // Function to check withdrawal approval on Ethereum mainnet | |
| const checkWithdrawalApproval = useCallback( | |
| async (withdrawalAmount: string): Promise<ApprovalState | null> => { | |
| if (!address) return null; | |
| setApprovalState((prev) => ({ ...prev, isChecking: true })); | |
| try { | |
| const result = await isWithdrawalSpendApproved({ | |
| userAddress: address, | |
| withdrawalAmount, | |
| }); | |
| const approvalState: ApprovalState = { | |
| isApproved: result.isApproved, | |
| allowance: result.allowance, | |
| allowanceAsBigInt: result.allowanceAsBigInt, | |
| decimals: result.decimals, | |
| error: result.error, | |
| isChecking: false, | |
| }; | |
| setApprovalState(approvalState); | |
| return approvalState; | |
| } catch (error) { | |
| const errorObj = | |
| error instanceof Error | |
| ? error | |
| : new Error("Failed to check approval"); | |
| const errorState: ApprovalState = { | |
| isApproved: false, | |
| allowance: "0", | |
| allowanceAsBigInt: "0", | |
| decimals: 6, | |
| error: errorObj, | |
| isChecking: false, | |
| }; | |
| setApprovalState(errorState); | |
| return errorState; | |
| } | |
| }, | |
| [address], | |
| ); | |
| // New withdrawal function implementing the proper flow | |
| const withdrawCampUSDWithApproval = useCallback( | |
| async (params: Omit<CampUSDWithdrawParams, "userAddress">) => { | |
| if (!address) { | |
| throw new Error("Wallet not connected"); | |
| } | |
| setOperationState({ | |
| isLoading: true, | |
| isSuccess: false, | |
| isError: false, | |
| error: null, | |
| step: "switching-chain", | |
| }); | |
| try { | |
| // Step 1: Switch to Ethereum where AtomicQueue contract exists | |
| if (Number(chain?.id) !== ETHEREUM_CHAIN_ID) { | |
| console.log(`π Switching from chain ${chain?.id} to Ethereum (${ETHEREUM_CHAIN_ID})`); | |
| await switchChainAsync({ chainId: ETHEREUM_CHAIN_ID }); | |
| console.log("β Successfully switched to Ethereum"); | |
| } | |
| // Wait a moment for chain switch to be fully processed | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| // Step 2: Check if AtomicQueue has permission to spend campUSD vault shares on Ethereum | |
| setOperationState((prev) => ({ ...prev, step: "checking-approval" })); | |
| const approvalStatus = await checkWithdrawalApproval( | |
| params.withdrawAmount, | |
| ); | |
| let approvalHash: string | undefined; | |
| if (!approvalStatus?.isApproved) { | |
| // Step 3: Approve AtomicQueue to spend campUSD vault shares on Ethereum | |
| setOperationState((prev) => ({ ...prev, step: "approving" })); | |
| const approvalData: ApproveWithdrawTokenTransactionData = await prepareCampUSDWithdrawalApproval({ | |
| userAddress: address, | |
| withdrawAmount: params.withdrawAmount, // Optional: specific amount to approve | |
| }); | |
| console.log("π§ Prepared campUSD vault shares approval for AtomicQueue:", { | |
| data: approvalData, | |
| chainId: ETHEREUM_CHAIN_ID, | |
| currentChain: chain?.id | |
| }); | |
| // CRITICAL VERIFICATION: Ensure we're on Ethereum before approval | |
| if (Number(chain?.id) !== ETHEREUM_CHAIN_ID) { | |
| throw new Error(`CHAIN MISMATCH: Expected Ethereum (${ETHEREUM_CHAIN_ID}), but currently on chain ${chain?.id}. AtomicQueue approval MUST be on Ethereum!`); | |
| } | |
| // Execute approval transaction on Ethereum | |
| approvalHash = await writeContractAsync({ | |
| ...approvalData, | |
| chainId: ETHEREUM_CHAIN_ID, // Explicitly force Ethereum | |
| }); | |
| console.log("π’ campUSD vault shares approved for AtomicQueue on Ethereum:", approvalHash); | |
| } | |
| // Step 4: Execute withdraw transaction on Ethereum via AtomicQueue | |
| setOperationState((prev) => ({ ...prev, step: "withdrawing" })); | |
| const withdrawData: WithdrawTransactionData = | |
| await prepareCampUSDWithdraw({ | |
| ...params, | |
| userAddress: address, | |
| }); | |
| console.log("π§ Prepared withdraw data for AtomicQueue:", { | |
| data: withdrawData, | |
| chainId: withdrawData.chainId, | |
| currentChain: chain?.id | |
| }); | |
| // Execute the withdraw transaction on Ethereum via AtomicQueue | |
| const withdrawHash = await writeContractAsync({ | |
| ...withdrawData, | |
| }); | |
| console.log("π’ campUSD withdraw transaction sent to AtomicQueue on Ethereum:", withdrawHash); | |
| setOperationState({ | |
| isLoading: false, | |
| isSuccess: true, | |
| isError: false, | |
| error: null, | |
| txHash: withdrawHash, // Return the withdrawal hash, not bridge hash | |
| step: "completed", | |
| }); | |
| return withdrawHash; | |
| } catch (error) { | |
| const errorMessage = | |
| error instanceof Error | |
| ? error | |
| : new Error("campUSD withdrawal failed"); | |
| setOperationState({ | |
| isLoading: false, | |
| isSuccess: false, | |
| isError: true, | |
| error: errorMessage, | |
| step: "completed", | |
| }); | |
| throw errorMessage; | |
| } | |
| }, | |
| [ | |
| address, | |
| chain?.id, | |
| switchChainAsync, | |
| writeContractAsync, | |
| checkWithdrawalApproval, | |
| ], | |
| ); | |
| return { | |
| // State | |
| operationState, | |
| approvalState, | |
| // Actions | |
| depositCampUSD, | |
| withdrawCampUSD, | |
| withdrawCampUSDWithApproval, // New improved withdrawal function | |
| checkDepositApproval, | |
| checkWithdrawalApproval, // New withdrawal approval checking | |
| // Helpers | |
| canDeposit, | |
| canWithdraw, | |
| // Current state | |
| address, | |
| }; | |
| } | |
| /** | |
| * Hook to wait for campUSD transaction confirmation | |
| */ | |
| export function useCampUSDTransaction(txHash?: string) { | |
| const { | |
| data: txReceipt, | |
| isLoading, | |
| isSuccess, | |
| isError, | |
| } = useWaitForTransactionReceipt({ | |
| hash: txHash as `0x${string}`, | |
| query: { | |
| enabled: !!txHash, | |
| }, | |
| }); | |
| return { | |
| txReceipt, | |
| isConfirming: isLoading, | |
| isConfirmed: isSuccess, | |
| isError, | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment