Skip to content

Deposit & Redeem Flows

This guide covers the write hooks that drive the ERC-7540 lifecycle. If you have not read the request → epoch → claim lifecycle, start there.

The transaction-hook state machine

Every write hook returns the same shape:

{
  txState: TxState;           // 'idle' | 'approving' | 'pending' | 'confirming' | 'success' | 'error'
  txHash: Hash | undefined;   // set once the wallet returns a hash
  error: string | null;       // human-readable; from err.shortMessage ?? err.message
  reset: () => void;          // back to 'idle', clears txHash and error
  // ...plus the action function(s), documented per hook
}

State transitions:

idle → (approving →) pending → confirming → success | error
  • 'approving' occurs only in useDeposit when an ERC-20 approval is needed.
  • 'confirming' is set after the wallet returns a hash, while waitForTransactionReceipt runs.

Action functions never throw

Failures land in error + txState: 'error' — you do not need try/catch around hook actions. Precondition failures (wallet not connected, zero amount, wrong vault type) also set error without sending anything. This is the opposite of the standalone VaultActions methods, which throw — see the Core Reference.

Deposit

useDeposit(vaultAddress, depositAssetAddress, depositAssetDecimals, vaultType) orchestrates the full flow in one call: it checks the ERC-20 allowance → if insufficient, sends approve (txState: 'approving') and waits for the receipt → then sends the deposit transaction. For 'ASYNC' vaults the deposit tx is requestDeposit(assets, controller, owner); for 'SYNC_DEPOSIT_ASYNC_REDEEM' it is the ERC-4626 deposit(assets, receiver).

The amount is human-readable (e.g. "100"), parsed with assetDecimals — take the decimals from metadata, never hard-code them.

import { useDeposit, useVaultMetadata } from "@arkonix.xyz/arkonix-vault-sdk";

function DepositForm({ vaultAddress }: { vaultAddress: `0x${string}` }) {
  const { data: meta } = useVaultMetadata(vaultAddress);
  const [amount, setAmount] = useState("");

  const { deposit, txState, error, reset } = useDeposit(
    vaultAddress, meta?.asset, meta?.assetDecimals, meta?.vaultType,
  );

  return (
    <div>
      <input value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="100" />
      <button onClick={() => deposit(amount)} disabled={txState !== "idle"}>
        {txState === "approving" ? "Approving…" :
         txState === "pending" ? "Depositing…" :
         txState === "confirming" ? "Confirming…" :
         txState === "success" ? "Done!" : "Deposit"}
      </button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

On ASYNC vaults, a deposit request must be claimed after the epoch: pass state.claimableDepositAssets (a raw bigint from useVaultUserState) to useClaimDeposit(vaultAddress)'s claimDeposit(assets) action. On-chain the claim is the ERC-4626 deposit(assets, receiver) call that mints the shares.

Redeem: request → wait → claim

Redeem is asynchronous on both vault types:

  1. useRequestRedeem(vaultAddress) — requestRedeem(shares, shareDecimals) with a human-readable share amount (pass meta.shareDecimals).
  2. Wait for the epoch — useVaultUserState polls every 10 s and flips hasPendingRedeem → hasClaimableRedeem.
  3. useClaimRedeem(vaultAddress) — claimRedeem(shares) with the raw bigint state.claimableRedeemShares.
import { useRequestRedeem, useClaimRedeem, useVaultUserState } from "@arkonix.xyz/arkonix-vault-sdk";

function RedeemFlow({ vaultAddress, meta }) {
  const state = useVaultUserState(vaultAddress, meta.share, meta.assetDecimals, meta.vaultType);
  const { requestRedeem } = useRequestRedeem(vaultAddress);
  const { claimRedeem } = useClaimRedeem(vaultAddress);

  return (
    <div>
      {state.shareBalance > 0n && (
        <button onClick={() => requestRedeem("10", meta.shareDecimals)}>
          Request Redeem 10 Shares
        </button>
      )}
      {state.hasPendingRedeem && (
        <p>Pending redeem: ~{state.pendingRedeemAssetsFormatted} {meta.assetSymbol}</p>
      )}
      {state.hasClaimableRedeem && (
        <button onClick={() => claimRedeem(state.claimableRedeemShares)}>
          Claim {state.claimableRedeemAssetsFormatted} {meta.assetSymbol}
        </button>
      )}
    </div>
  );
}

Pending vs claimable valuations differ

pendingRedeemAssetsFormatted is estimated at the current price; claimableRedeemAssetsFormatted is locked at the epoch price (via maxWithdraw).

Cancel flows

A pending (not yet epoch-processed) request can be cancelled. Cancellation is itself asynchronous: cancel → wait for the epoch → claim back the funds.

  • Cancel redeem (both vault types): useCancelRedeem(vaultAddress) exposes cancelRedeem() and claimCancelRedeem() (returns the user's shares), sharing one txState.
  • Cancel deposit (ASYNC vaults only): useCancelDeposit(vaultAddress, vaultType) exposes cancelDeposit() and claimCancelDeposit() (returns the assets).

useCancelDeposit requires the vaultType argument

The second argument is required — pass meta?.vaultType. If vaultType is not exactly 'ASYNC' (including undefined while metadata loads), both actions fail early with:

"Cancel deposit is not supported on this vault: SYNC_DEPOSIT_ASYNC_REDEEM deposits settle immediately."

Sync deposits settle immediately, so there is nothing to cancel; the guard exists to prevent a confusing on-chain revert.

import { useCancelRedeem, useCancelDeposit, useVaultMetadata, useVaultUserState } from "@arkonix.xyz/arkonix-vault-sdk";

function CancelControls({ vaultAddress }) {
  const { data: meta } = useVaultMetadata(vaultAddress);
  const state = useVaultUserState(vaultAddress, meta?.share, meta?.assetDecimals, meta?.vaultType);
  const { cancelRedeem, claimCancelRedeem } = useCancelRedeem(vaultAddress);
  const { cancelDeposit, claimCancelDeposit } = useCancelDeposit(vaultAddress, meta?.vaultType);

  return (
    <div>
      {state.hasPendingRedeem && <button onClick={cancelRedeem}>Cancel Redeem</button>}
      {state.hasClaimableCancelRedeem && (
        <button onClick={claimCancelRedeem}>Claim Cancelled Shares</button>
      )}
      {/* ASYNC vaults only — the hook fails early with a clear error otherwise */}
      {state.hasPendingDeposit && <button onClick={cancelDeposit}>Cancel Deposit</button>}
      {state.hasClaimableCancelDeposit && (
        <button onClick={claimCancelDeposit}>Claim Cancelled Assets</button>
      )}
    </div>
  );
}
  • Hooks Reference — full signatures, query keys, refetch cadences
  • Core Reference — the same flows without React (VaultActions, VaultTxBuilder)
  • Concepts — lifecycle rules, decimals, vault types