Skip to content

Instantly share code, notes, and snippets.

@hydrogenbond007
Last active September 9, 2025 14:49
Show Gist options
  • Select an option

  • Save hydrogenbond007/f24c087acebeb579d167fc17e52a6fea to your computer and use it in GitHub Desktop.

Select an option

Save hydrogenbond007/f24c087acebeb579d167fc17e52a6fea to your computer and use it in GitHub Desktop.
/**
* @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