Skip to main content
Redeem shares in one transaction by pairing prepareWithdrawTxData with Privy’s wallet connection hooks. This pattern keeps signer UX inside Privy while letting the Amplify SDK handle calldata.

Overview

  • Fetch the target vault (for example with fetchVaults and React Query).
  • Prepare withdrawal calldata with slippage protection.
  • Submit the transaction via Privy’s sendTransaction.
  • Track optimistic state and surface any APIError details.

Prerequisites

  • React 18+
  • @privy-io/react-auth >= 3
  • @paxoslabs/amplify-sdk
  • Vault metadata available in your component (e.g., from fetchVaults)

Withdraw Hook

import { useCallback, useMemo, useState } from "react";
import { encodeFunctionData } from "viem";
import {
  isWithdrawalSpendApproved,
  prepareApproveWithdrawTxData,
  prepareWithdrawTxData,
  type YieldType,
} from "@paxoslabs/amplify-sdk";
import { usePrivy, useWallets } from "@privy-io/react-auth";

interface WithdrawParams {
  yieldType: YieldType;
  shareAmount: string;
  wantAssetAddress: `0x${string}`;
  chainId: number;
  userAddress: `0x${string}`;
  slippage?: number;
}

export function usePrivyWithdraw() {
  const { sendTransaction } = usePrivy();
  const { wallets } = useWallets();
  const wallet = useMemo(() => wallets.find(Boolean), [wallets]);

  const [txHash, setTxHash] = useState<`0x${string}` | null>(null);
  const [status, setStatus] = useState<
    "idle" | "checking" | "approving" | "withdrawing" | "success"
  >("idle");
  const [error, setError] = useState<Error | null>(null);

  const run = useCallback(
    async ({
      yieldType,
      shareAmount,
      wantAssetAddress,
      chainId,
      userAddress,
      slippage = 75,
    }: WithdrawParams) => {
      if (!wallet) {
        throw new Error("Connect a Privy wallet before withdrawing.");
      }

      setStatus("checking");
      setError(null);
      setTxHash(null);

      try {
        const approval = await isWithdrawalSpendApproved({
          yieldType,
          wantAssetAddress,
          userAddress,
          chainId,
        });

        if (!approval.isApproved) {
          setStatus("approving");
          const approvalTx = await prepareApproveWithdrawTxData({
            yieldType,
            wantAssetAddress,
            shareAmount,
            chainId,
          });

          await sendTransaction({
            chainId: approvalTx.chainId,
            to: approvalTx.address,
            data: encodeFunctionData({
              abi: approvalTx.abi,
              functionName: approvalTx.functionName,
              args: approvalTx.args,
            }),
          });
        }

        const tx = await prepareWithdrawTxData({
          yieldType,
          shareAmount,
          wantAssetAddress,
          chainId,
          userAddress,
          slippage,
        });

        setStatus("withdrawing");
        const hash = await sendTransaction({
          chainId: tx.chainId,
          to: tx.address,
          data: encodeFunctionData({
            abi: tx.abi,
            functionName: tx.functionName,
            args: tx.args,
          }),
        });

        setTxHash(hash as `0x${string}`);
        setStatus("success");
      } catch (err) {
        const message = err instanceof Error ? err : new Error(String(err));
        setError(message);
        setStatus("idle");
        throw err;
      }
    },
    [wallet, sendTransaction]
  );

  return { run, status, error, txHash };
}

Component Usage

import { useMemo } from "react";
import { useWallets } from "@privy-io/react-auth";
import { useQuery } from "@tanstack/react-query";
import { fetchVaults, YieldType } from "@paxoslabs/amplify-sdk";
import { mainnet } from "viem/chains";
import { usePrivyWithdraw } from "./usePrivyWithdraw";

export function PrivyWithdrawButton() {
  const { wallets } = useWallets();
  const wallet = useMemo(() => wallets.find(Boolean), [wallets]);
  const { run, status, error, txHash } = usePrivyWithdraw();

  const { data: vaults } = useQuery({
    queryKey: ["core-vaults", mainnet.id],
    queryFn: () =>
      fetchVaults({
        chainId: mainnet.id,
        yieldType: YieldType.CORE,
      }),
    enabled: Boolean(wallet),
  });

  const vault = vaults?.[0];

  if (!wallet) {
    return <p>Connect with Privy to withdraw.</p>;
  }

  if (!vault) {
    return <p>Loading vault metadata…</p>;
  }

  async function handleClick() {
    await run({
      yieldType: vault.yieldType,
      shareAmount: "5.0",
      wantAssetAddress: vault.depositTokenAddressId as `0x${string}`,
      chainId: vault.chainId,
      userAddress: wallet.address as `0x${string}`,
      slippage: 75,
    });
  }

  return (
    <section>
      <button onClick={handleClick} disabled={status !== "idle" && status !== "success"}>
        {status === "checking" && "Checking allowance…"}
        {status === "approving" && "Approving…"}
        {status === "withdrawing" && "Withdrawing…"}
        {status === "success" && "Withdraw Again"}
        {(status === "idle" || status === "success") && "Withdraw"}
      </button>

      {txHash && (
        <p>
          Withdrawal sent:{" "}
          <a
            href={`https://etherscan.io/tx/${txHash}`}
            target="_blank"
            rel="noreferrer"
          >
            {txHash}
          </a>
        </p>
      )}

      {error && <p role="alert">Withdraw failed: {error.message}</p>}
    </section>
  );
}
prepareWithdrawTxData throws APIError when the vault or asset is unsupported. Log the endpoint and statusCode to help users fall back to alternate flows.