> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Building an Earn UI

> List Earn vaults, show a wallet's position, deposit, withdraw, and claim rewards.

## Prerequisites

Before this page: a connected or embedded **EVM** wallet (see [Connecting external wallets](/docs/javascript/building-ui/connecting-external-wallets)). [Earn](/docs/overview/yield) vault actions are EVM-only today.

## What you'll build

An Earn flow has four parts:

1. **List** vaults and show their APY and curator.
2. **Show** a wallet's position (shares and asset value) in a vault.
3. **Deposit / withdraw** — sign and submit from the user's wallet.
4. **Rewards** — show accrued rewards and claim them.

Amounts you pass to `depositToYieldVault` and `withdrawFromYieldVault` are **human-readable** (`'10.5'` for 10.5 USDC). Amounts the SDK returns (`shares`, `assets`, `accrued`) are **raw on-chain units** — divide by `assetDecimals` from `getYieldDetails` before displaying them.

## Listing vaults

`listYieldVaults` returns every vault the current environment can see, each with a best-effort `netApy` and `tvlUsd`.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import { listYieldVaults } from '@dynamic-labs-sdk/client';

    async function loadVaults() {
      const vaults = await listYieldVaults();

      // renderVaultList is your implementation.
      renderVaultList(vaults);
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import { useListYieldVaults } from '@dynamic-labs-sdk/react-hooks';

    function VaultList({ onSelect }: { onSelect: (vaultId: string) => void }) {
      const { data: vaults = [], isLoading } = useListYieldVaults();

      if (isLoading) {
        return <p>Loading vaults...</p>;
      }

      return (
        <ul>
          {vaults.map((vault) => (
            <li key={vault.vaultId}>
              <button onClick={() => onSelect(vault.vaultId)}>
                {vault.assetSymbol} — {vault.curator} — APY: {vault.netApy ?? 'n/a'}
              </button>
            </li>
          ))}
        </ul>
      );
    }
    ```
  </Tab>
</Tabs>

## Showing a wallet's position

`getYieldPosition` reads on-chain, so it always reflects current state. Pair it with `getYieldDetails` to format `shares`/`assets` — both are raw units, scaled by `assetDecimals`.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import { getYieldDetails, getYieldPosition, type WalletAccount } from '@dynamic-labs-sdk/client';
    import { formatUnits } from 'viem';

    async function loadPosition(vaultId: string, walletAccount: WalletAccount) {
      const [details, position] = await Promise.all([
        getYieldDetails({ vaultId }),
        getYieldPosition({ vaultId, walletAccount }),
      ]);

      return formatUnits(BigInt(position.assets), details.assetDecimals);
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import { type WalletAccount } from '@dynamic-labs-sdk/client';
    import { useGetYieldDetails, useGetYieldPosition } from '@dynamic-labs-sdk/react-hooks';
    import { formatUnits } from 'viem';

    function Position({
      vaultId,
      walletAccount,
    }: {
      vaultId: string;
      walletAccount: WalletAccount | undefined;
    }) {
      const { data: details } = useGetYieldDetails({ vaultId });
      const { data: position, isLoading } = useGetYieldPosition({
        vaultId,
        walletAccount,
      });

      if (isLoading || !details || !position) {
        return <p>Loading position...</p>;
      }

      return (
        <p>
          Balance: {formatUnits(BigInt(position.assets), details.assetDecimals)}{' '}
          {details.assetSymbol}
        </p>
      );
    }
    ```

    `useGetYieldPosition` stays disabled until `walletAccount` is defined — safe to render before a wallet is connected.
  </Tab>
</Tabs>

## Depositing

`depositToYieldVault` takes a human-readable `amount` and does everything else itself: it requests the signing payload, signs it, and broadcasts it. If the wallet's allowance is short of `amount`, it signs an ERC-20 approval first — `onStepChange` tells you which step is running.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import { depositToYieldVault, type WalletAccount } from '@dynamic-labs-sdk/client';

    async function deposit(
      vaultId: string,
      walletAccount: WalletAccount,
      amount: string,
    ) {
      const { transactionHash } = await depositToYieldVault({
        vaultId,
        walletAccount,
        amount,
        onStepChange: (step) => {
          // updateStep is your implementation — 'approval' or 'transaction'.
          updateStep(step);
        },
      });

      return transactionHash;
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import type { WalletAccount } from '@dynamic-labs-sdk/client';
    import { useDepositToYieldVault } from '@dynamic-labs-sdk/react-hooks';
    import { useState } from 'react';

    function DepositButton({
      vaultId,
      walletAccount,
      amount,
    }: {
      vaultId: string;
      walletAccount: WalletAccount;
      amount: string;
    }) {
      const { mutate: deposit, isPending } = useDepositToYieldVault();
      const [step, setStep] = useState<'approval' | 'transaction' | null>(null);

      return (
        <button
          type="button"
          onClick={() =>
            deposit({ vaultId, walletAccount, amount, onStepChange: setStep })
          }
          disabled={isPending}
        >
          {step === 'approval' && 'Approving...'}
          {step === 'transaction' && 'Depositing...'}
          {!step && 'Deposit'}
        </button>
      );
    }
    ```
  </Tab>
</Tabs>

## Withdrawing

`withdrawFromYieldVault` is the mirror of deposit, minus the approval step — the wallet is redeeming shares it already holds, so it's always a single transaction.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import { withdrawFromYieldVault, type WalletAccount } from '@dynamic-labs-sdk/client';

    async function withdraw(
      vaultId: string,
      walletAccount: WalletAccount,
      amount: string,
    ) {
      const { transactionHash } = await withdrawFromYieldVault({
        vaultId,
        walletAccount,
        amount,
      });

      return transactionHash;
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import type { WalletAccount } from '@dynamic-labs-sdk/client';
    import { useWithdrawFromYieldVault } from '@dynamic-labs-sdk/react-hooks';

    function WithdrawButton({
      vaultId,
      walletAccount,
      amount,
    }: {
      vaultId: string;
      walletAccount: WalletAccount;
      amount: string;
    }) {
      const { mutate: withdraw, isPending } = useWithdrawFromYieldVault();

      return (
        <button
          type="button"
          onClick={() => withdraw({ vaultId, walletAccount, amount })}
          disabled={isPending}
        >
          Withdraw
        </button>
      );
    }
    ```
  </Tab>
</Tabs>

## Rewards

Read accrued rewards with `getYieldRewards`, and only show a claim action once at least one entry has a non-zero `accrued` amount — `claimYieldRewards` rejects when nothing is claimable.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import { claimYieldRewards, getYieldRewards, type WalletAccount } from '@dynamic-labs-sdk/client';

    async function claimIfAvailable(vaultId: string, walletAccount: WalletAccount) {
      const rewards = await getYieldRewards({ vaultId, walletAccount });
      const claimable = rewards.some((r) => r.accrued && r.accrued !== '0');

      if (!claimable) return null;

      const { transactionHash } = await claimYieldRewards({ vaultId, walletAccount });
      return transactionHash;
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import type { WalletAccount } from '@dynamic-labs-sdk/client';
    import { useClaimYieldRewards, useGetYieldRewards } from '@dynamic-labs-sdk/react-hooks';

    function Rewards({
      vaultId,
      walletAccount,
    }: {
      vaultId: string;
      walletAccount: WalletAccount | undefined;
    }) {
      const { data: rewards = [] } = useGetYieldRewards({ vaultId, walletAccount });
      const { mutate: claim, isPending } = useClaimYieldRewards();

      const claimable = rewards.some((r) => r.accrued && r.accrued !== '0');

      return (
        <>
          <ul>
            {rewards.map((r) => (
              <li key={r.tokenAddress}>
                {r.tokenSymbol}: {r.accrued}
              </li>
            ))}
          </ul>
          <button
            type="button"
            disabled={!claimable || !walletAccount || isPending}
            onClick={() => walletAccount && claim({ vaultId, walletAccount })}
          >
            Claim
          </button>
        </>
      );
    }
    ```
  </Tab>
</Tabs>

## Handling errors

| Error / status                                                                 | Cause                                                                                                       | What to do                                                                                                                              |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `WalletProviderMethodUnavailableError`                                         | The connected wallet isn't an EVM wallet — vault actions are EVM-only.                                      | Prompt the user to connect or switch to an EVM wallet before showing Earn actions.                                                      |
| `depositToYieldVault`/`withdrawFromYieldVault` rejects with a validation error | `amount` has more decimal places than the asset supports.                                                   | Round the input to the asset's decimals before submitting.                                                                              |
| `claimYieldRewards` rejects                                                    | Nothing is claimable for this vault right now.                                                              | Re-check [`getYieldRewards`](/docs/javascript/reference/client/get-yield-rewards) and only show the claim action when `accrued` is non-zero. |
| `APIError` with `status === 403`                                               | The vault is private and not scoped to the current environment, or Earn isn't enabled for this environment. | Verify the vault came from your own `listYieldVaults` response rather than a hardcoded ID.                                              |

```js theme={"system"}
import { APIError } from '@dynamic-labs-sdk/client/core';

try {
  await depositToYieldVault({ vaultId, walletAccount, amount });
} catch (error) {
  if (error instanceof APIError && error.status === 403) {
    // Vault not accessible to this environment
  }
}
```

## See also

* [Transaction confirmation & simulation](/docs/javascript/building-ui/transaction-confirmation) — confirm the underlying transaction
* [Token balances & display](/docs/javascript/building-ui/token-balances-display) — show balances alongside vault positions
* [Building a swap UI](/docs/javascript/building-ui/swap-ui) — the equivalent flow for token swaps
