Bridge and Withdraw

Interface

/**
 * Parameters for preparing a cross-chain bridge and withdrawal transaction
 * @interface PrepareBridgeAndWithdrawTransactionDataParams
 * @extends {Omit<PrepareWithdrawTransactionDataParams, 'chainId'>}
 * @property {ChainId} sourceChainId - Chain ID where shares currently exist
 * @property {ChainId} destinationChainId - Chain ID where assets will be withdrawn
 * @property {number} offerAmount - Amount of vault shares to withdraw
 * @property {number} [deadline] - Unix timestamp when the request expires (optional)
 * @property {number} [slippage] - Maximum acceptable slippage percentage (optional)
 * @property {Address} userAddress - Ethereum address of the user making the withdrawal
 * @property {VaultKey} vaultKey - Unique identifier for the vault
 * @property {TokenKey} wantTokenSymbol - Symbol of the token the user wants to receive
 */
interface PrepareBridgeAndWithdrawTransactionDataParams
  extends Omit<PrepareWithdrawTransactionDataParams, 'chainId'> {
  sourceChainId: ChainId;
  destinationChainId: ChainId;
}

/**
 * Combined result containing both bridge and withdraw transaction data
 * @interface BridgeAndWithdrawResult
 */
interface BridgeAndWithdrawTransactionData {
  bridgeTransactionData: BridgeTransactionData; // Data for bridging shares to destination chain
  withdrawTransactionData: WithdrawTransactionData; // Data for withdraw request on destination chain
}

Function Overview

import { prepareWithdrawAndBridge, TokenKeys } from '@molecularlabs/nucleus-frontend';

const COOLVAULT_VAULT_KEY = VaultKeys.COOLVAULT;

// Prepare withdrawal and bridge transaction data
const withdrawAndBridgeData = await prepareWithdrawAndBridge({
  vaultKey: COOLVAULT_VAULT_KEY,
  sourceChainId: 288, // Boba network
  destinationChainId: 1, // Ethereum mainnet
  userAddress: '0x1234...',
  wantAsset: TokenKey.WETH,
  redeemAmount: BigInt('1000000000000000000'), // 1 token
  previewFee: BigInt('100000000000000'), // Bridge fee (get this from getPreviewFee)
  deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now (optional)
  slippage: 0.01, // 1% slippage (optional)
});

const {
  bridgeData, // Bridge configuration data
  withdrawRequestData, // Withdraw request details
} = withdrawAndBridgeData;

// Further destructure the withdrawal request data
const {
  abi, // AtomicQueueAbi
  address, // Contract address for the atomic queue
  functionName, // 'updateAtomicRequest'
  args, // [redeemTokenAddress, wantAssetAddress, userRequest]
  chainId, // Chain ID for the transaction
} = withdrawRequestData;

Viem

import { createPublicClient, http, createWalletClient, custom } from 'viem';
import { mainnet, arbitrum } from 'viem/chains';
import { 
  prepareBridgeAndWithdrawTransactionData, 
  VaultKeys, 
  TokenKey 
} from '@molecularlabs/nucleus-sdk';

// Example using Viem with MetaMask
const publicClient = createPublicClient({
  chain: arbitrum,
  transport: http()
});

const walletClient = createWalletClient({
  chain: arbitrum,
  transport: custom(window.ethereum)
});

const COOLVAULT_VAULT_KEY = VaultKeys.COOLVAULT;

async function bridgeAndWithdrawFromVault() {
  try {
    // Get user's address
    const [address] = await walletClient.requestAddresses();

    // Prepare bridge and withdraw data
    const bridgeAndWithdrawData = await prepareBridgeAndWithdrawTransactionData({
      vaultKey: COOLVAULT_VAULT_KEY,
      sourceChainId: arbitrum.id, // Arbitrum (where shares are)
      destinationChainId: mainnet.id, // Ethereum Mainnet (where to withdraw)
      userAddress: address,
      wantTokenSymbol: TokenKey.WETH, // Token to receive
      offerAmount: 1.5, // Amount of vault shares to withdraw
      deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
      slippage: 0.005 // 0.5% slippage
    });

    // First, send the bridge transaction
    const bridgeHash = await walletClient.writeContract({
      ...bridgeAndWithdrawData.bridgeTransactionData,
      value: bridgeAndWithdrawData.bridgeTransactionData.value // Include bridge fee
    });
    
    // Wait for bridge transaction
    const bridgeReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: bridgeHash 
    });

    // Switch to destination chain for withdraw
    const withdrawClient = createWalletClient({
      chain: mainnet,
      transport: custom(window.ethereum)
    });

    // Send the withdraw transaction
    const withdrawHash = await withdrawClient.writeContract({
      ...bridgeAndWithdrawData.withdrawTransactionData
    });

    // Wait for withdraw transaction
    const withdrawReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: withdrawHash 
    });

  } catch (error) {
    console.error('Bridge and withdraw failed:', error);
  }
}

Wagmi Example

import { useState } from 'react';
import { useAccount, useContractWrite, usePrepareContractWrite, useWaitForTransaction, useNetwork, useSwitchNetwork } from 'wagmi';
import { useQuery } from '@tanstack/react-query';
import { mainnet, arbitrum } from 'viem/chains';
import { 
  prepareBridgeAndWithdrawTransactionData, 
  VaultKey, 
  TokenKey 
} from '@molecularlabs/nucleus-sdk';

const VAULT_KEY = VaultKey.BOBAETH;

function BridgeAndWithdrawForm() {
  const { address } = useAccount();
  const { chain } = useNetwork();
  const { switchNetwork } = useSwitchNetwork();
  const [amount, setAmount] = useState('');
  
  // Fetch bridge and withdraw data
  const { data: transactionData, isLoading: isPreparingTransactions, error: prepareError } = useQuery({
    queryKey: ['prepareBridgeAndWithdraw', VAULT_KEY, amount, address],
    queryFn: async () => {
      if (!amount || !address) return null;
      
      return prepareBridgeAndWithdrawTransactionData({
        vaultKey: VAULT_KEY,
        sourceChainId: arbitrum.id, // Arbitrum (where shares are)
        destinationChainId: mainnet.id, // Ethereum Mainnet (where to withdraw)
        userAddress: address,
        wantTokenSymbol: TokenKey.WETH,
        offerAmount: Number(amount),
        deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour
        slippage: 0.005 // 0.5%
      });
    },
    enabled: Boolean(amount && address),
  });

  // Prepare the bridge contract write
  const { config: bridgeConfig, error: prepareBridgeError } = usePrepareContractWrite({
    address: transactionData?.bridgeTransactionData.address,
    abi: transactionData?.bridgeTransactionData.abi,
    functionName: transactionData?.bridgeTransactionData.functionName,
    args: transactionData?.bridgeTransactionData.args,
    value: transactionData?.bridgeTransactionData.value,
    chainId: transactionData?.bridgeTransactionData.chainId,
    enabled: Boolean(transactionData) && chain?.id === arbitrum.id,
  });

  // Handle the bridge contract write
  const { write: writeBridge, data: bridgeData, error: bridgeError } = useContractWrite(bridgeConfig);

  // Handle bridge transaction status
  const { isLoading: isBridgePending, isSuccess: isBridgeSuccess } = useWaitForTransaction({
    hash: bridgeData?.hash,
  });

  // Prepare the withdraw contract write
  const { config: withdrawConfig, error: prepareWithdrawError } = usePrepareContractWrite({
    address: transactionData?.withdrawTransactionData.address,
    abi: transactionData?.withdrawTransactionData.abi,
    functionName: transactionData?.withdrawTransactionData.functionName,
    args: transactionData?.withdrawTransactionData.args,
    chainId: transactionData?.withdrawTransactionData.chainId,
    enabled: Boolean(transactionData) && isBridgeSuccess && chain?.id === mainnet.id,
  });

  // Handle the withdraw contract write
  const { write: writeWithdraw, data: withdrawData, error: withdrawError } = useContractWrite(withdrawConfig);

  // Handle withdraw transaction status
  const { isLoading: isWithdrawPending, isSuccess: isWithdrawSuccess } = useWaitForTransaction({
    hash: withdrawData?.hash,
  });

  // Combine all errors
  const error = prepareError || prepareBridgeError || prepareWithdrawError || bridgeError || withdrawError;

  // Handle the full bridge and withdraw process
  const handleBridgeAndWithdraw = async () => {
    if (!transactionData) return;

    // Ensure we're on the source chain for bridging
    if (chain?.id !== arbitrum.id) {
      await switchNetwork?.(arbitrum.id);
      return;
    }

    // Execute bridge transaction
    writeBridge?.();
  };

  // Handle withdraw after bridge success
  const handleWithdraw = async () => {
    if (!transactionData || !isBridgeSuccess) return;

    // Switch to destination chain for withdrawal
    if (chain?.id !== mainnet.id) {
      await switchNetwork?.(mainnet.id);
      return;
    }

    // Execute withdraw transaction
    writeWithdraw?.();
  };

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg">
      <h2 className="text-2xl font-bold mb-6">Bridge and Withdraw WETH</h2>
      
      {/* Amount Input */}
      <div className="mb-4">
        <label className="block text-sm font-medium text-gray-700 mb-2">
          Amount (Vault Shares)
        </label>
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          placeholder="0.0"
          className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
          disabled={isBridgePending || isWithdrawPending}
        />
      </div>

      {/* Transaction Details */}
      <div className="mb-6 text-sm text-gray-600">
        <p>From: Arbitrum</p>
        <p>To: Ethereum Mainnet</p>
        <p>Want Token: WETH</p>
        <p>Slippage: 0.5%</p>
      </div>

      {/* Bridge Button */}
      <button
        onClick={handleBridgeAndWithdraw}
        disabled={!writeBridge || isPreparingTransactions || isBridgePending}
        className={`w-full py-3 px-4 rounded-lg font-medium text-white mb-4
          ${(!writeBridge || isPreparingTransactions || isBridgePending)
            ? 'bg-gray-400'
            : 'bg-blue-600 hover:bg-blue-700'
          } transition-colors`}
      >
        {isPreparingTransactions ? 'Preparing...' : isBridgePending ? 'Bridging...' : 'Bridge Shares'}
      </button>

      {/* Withdraw Button */}
      <button
        onClick={handleWithdraw}
        disabled={!writeWithdraw || !isBridgeSuccess || isWithdrawPending}
        className={`w-full py-3 px-4 rounded-lg font-medium text-white
          ${(!writeWithdraw || !isBridgeSuccess || isWithdrawPending)
            ? 'bg-gray-400'
            : 'bg-green-600 hover:bg-green-700'
          } transition-colors`}
      >
        {isWithdrawPending ? 'Withdrawing...' : 'Withdraw'}
      </button>

      {/* Status Messages */}
      {isBridgeSuccess && !isWithdrawSuccess && (
        <div className="mt-4 p-4 bg-blue-100 text-blue-700 rounded-lg">
          Bridge successful! Ready to withdraw.
        </div>
      )}

      {isWithdrawSuccess && (
        <div className="mt-4 p-4 bg-green-100 text-green-700 rounded-lg">
          Withdrawal successful! Your tokens have been received.
        </div>
      )}

      {error && (
        <div className="mt-4 p-4 bg-red-100 text-red-700 rounded-lg">
          {error.message}
        </div>
      )}

      {/* Transaction Progress */}
      {(isBridgePending || isWithdrawPending) && (
        <div className="mt-4">
          <div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
            <div className="h-full bg-blue-600 animate-pulse rounded-full" />
          </div>
          <p className="text-sm text-gray-600 mt-2">
            {isBridgePending ? 'Bridge in progress...' : 'Withdrawal in progress...'}
          </p>
        </div>
      )}
    </div>
  );
}

export default BridgeAndWithdrawForm;

Last updated