Skip to main content

What We’re Building

A React (Next.js) app that connects Dynamic’s smart wallets to Deframe Pods yield strategies, allowing users to:
  • Deposit stablecoins into yield strategies
  • Withdraw from positions
  • Track yields and positions across multiple protocols
  • Enjoy gasless transactions with smart wallet bundling
Note: Deframe Pods requires bundled transactions, so users must connect with a smart wallet (like ZeroDev) to interact with yield strategies. If you want to take a quick look at the final code, check out the GitHub repository.

Building the Application

Project Setup

Start by creating a new Dynamic project with React, Viem, Wagmi, and Ethereum support:
npx create-dynamic-app@latest pods-dynamic-app --framework nextjs --library viem --wagmi true --chains ethereum --pm npm
cd pods-dynamic-app

Install Dependencies

Add the required dependencies for smart wallet support:
npm install @dynamic-labs/ethereum-aa
This installs Dynamic’s Ethereum Account Abstraction package which provides ZeroDev smart wallet connectors for gasless transactions.

Configure Dynamic Environment

Create a .env.local file with your Dynamic environment ID and Deframe Pods API key:
.env.local
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-environment-id-here
NEXT_PUBLIC_PODS_API_KEY=your-pods-api-key-here
NEXT_PUBLIC_PODS_API_URL=https://api.deframe.io
You can find your Environment ID in the Dynamic dashboard under Developer Settings → SDK & API Keys. For the Deframe Pods API key, visit Deframe to get your API key.

Configure Smart Wallets

Enable ZeroDev smart wallets in your Dynamic dashboard:
  1. Go to Developer SettingsSmart Wallets
  2. Enable ZeroDev provider
  3. Configure your ZeroDev project ID
This enables gasless transactions and transaction bundling for your users. If you want more information about using smart wallets with Dynamic, check out the Smart Wallets guide. You can use any smart wallet provider you like, but for this guide we’ll be using ZeroDev.

Configure Providers

Update src/lib/providers.tsx to include ZeroDev smart wallet connectors:
src/lib/providers.tsx
"use client";

import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core";
import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";
import { ZeroDevSmartWalletConnectors } from "@dynamic-labs/ethereum-aa";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "@/lib/wagmi";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,
      refetchOnWindowFocus: false,
    },
  },
});

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <DynamicContextProvider
      theme="auto"
      settings={{
        environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
        walletConnectors: [
          EthereumWalletConnectors,
          ZeroDevSmartWalletConnectors,
        ],
      }}
    >
      <WagmiProvider config={config}>
        <QueryClientProvider client={queryClient}>
          <DynamicWagmiConnector>{children}</DynamicWagmiConnector>
        </QueryClientProvider>
      </WagmiProvider>
    </DynamicContextProvider>
  );
}

Create Pods Client

Create src/lib/pods.ts for the Deframe Pods API client. This file contains all the API communication logic for interacting with the Deframe Pods service. It provides functions to fetch yield strategies, get transaction bytecode for deposits and withdrawals, and retrieve wallet positions. All type definitions are available in src/lib/pods-types.ts in the GitHub repository.
src/lib/pods.ts
import type {
  Strategy,
  Position,
  WalletPositions,
  StrategyDetailResponse,
  BytecodeResponse,
  StrategiesResponse,
  RawPosition,
  RawWalletPositions,
} from "./pods-types";

const PODS_API_BASE =
  process.env.NEXT_PUBLIC_PODS_API_URL || "https://api.deframe.io";
const PODS_API_KEY = process.env.NEXT_PUBLIC_PODS_API_KEY;

if (!PODS_API_KEY) {
  throw new Error(
    "NEXT_PUBLIC_PODS_API_KEY is not set in environment variables"
  );
}

async function fetchFromPodsAPI<T>(
  endpoint: string,
  options: RequestInit = {}
): Promise<T> {
  const url = `${PODS_API_BASE}${endpoint}`;

  const response = await fetch(url, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      "x-api-key": PODS_API_KEY,
      ...options.headers,
    },
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Pods API error: ${error}`);
  }

  return response.json();
}

export async function getStrategies(
  chainId?: number,
  limit?: number
): Promise<StrategiesResponse> {
  const queryParams = new URLSearchParams();
  if (limit) queryParams.set("limit", limit.toString());

  const endpoint = `/strategies${
    queryParams.toString() ? `?${queryParams.toString()}` : ""
  }`;
  const response = await fetchFromPodsAPI<StrategiesResponse>(endpoint);

  if (chainId) {
    return {
      ...response,
      data: response.data.filter(
        (strategy) => parseInt(strategy.networkId) === chainId
      ),
    };
  }

  return response;
}

export async function getStrategy(strategyId: string): Promise<Strategy> {
  const endpoint = `/strategies/${strategyId}`;
  const response = await fetchFromPodsAPI<StrategyDetailResponse>(endpoint);
  return {
    ...response.strategy,
    spotPosition: response.spotPosition,
  };
}

export async function getDepositBytecode(params: {
  strategyId: string;
  chainId: number;
  amount: string;
  asset: string;
  wallet: string;
}): Promise<BytecodeResponse> {
  const { strategyId, chainId, amount, asset, wallet } = params;

  const queryParams = new URLSearchParams({
    action: "lend",
    chainId: chainId.toString(),
    amount,
    asset,
    wallet,
  });

  const endpoint = `/strategies/${strategyId}/bytecode?${queryParams.toString()}`;
  return fetchFromPodsAPI<BytecodeResponse>(endpoint);
}

export async function getWithdrawBytecode(params: {
  strategyId: string;
  chainId: number;
  amount: string;
  asset: string;
  wallet: string;
}): Promise<BytecodeResponse> {
  const { strategyId, chainId, amount, asset, wallet } = params;

  const queryParams = new URLSearchParams({
    action: "withdraw",
    chainId: chainId.toString(),
    amount,
    asset,
    wallet,
  });

  const endpoint = `/strategies/${strategyId}/bytecode?${queryParams.toString()}`;
  return fetchFromPodsAPI<BytecodeResponse>(endpoint);
}

export async function getWalletPositions(
  address: string
): Promise<WalletPositions> {
  const endpoint = `/wallets/${address}`;
  const raw = await fetchFromPodsAPI<RawWalletPositions>(endpoint);

  const mapped: WalletPositions = {
    address,
    positions:
      (raw?.positions || []).map((p: RawPosition) => {
        const spot = p?.spotPosition ?? {};
        const current = spot?.currentPosition ?? {};
        const strat = p?.strategy ?? {};

        const assetDecimals =
          typeof strat?.assetDecimals === "number"
            ? String(strat.assetDecimals)
            : String(current?.decimals ?? "0");

        const balanceHumanized =
          typeof current?.humanized === "number"
            ? current.humanized
            : parseFloat(String(current?.humanized ?? 0));

        return {
          protocol: String(strat?.protocol ?? ""),
          asset: {
            address: String(strat?.asset ?? strat?.underlyingAsset ?? ""),
            decimals: assetDecimals,
            symbol: String(strat?.assetName ?? current?.asset ?? ""),
            name: String(strat?.assetName ?? current?.asset ?? ""),
          },
          balance: {
            raw: String(current?.value ?? "0"),
            humanized: isFinite(balanceHumanized) ? balanceHumanized : 0,
            decimals: assetDecimals,
          },
          balanceUSD: String(spot?.underlyingBalanceUSD ?? "0"),
          apy: String(spot?.apy ?? "0"),
          rewards: Array.isArray(p?.rewards) ? p.rewards : [],
          strategyId: String(strat?.id ?? ""),
        };
      }) ?? [],
  };

  return mapped;
}

export const client = {
  getStrategies,
  getStrategy,
  getDepositBytecode,
  getWithdrawBytecode,
  getWalletPositions,
};
This client provides methods to fetch strategies, get transaction bytecode, and retrieve wallet positions from the Deframe Pods API. The fetchFromPodsAPI helper function handles all HTTP requests with proper authentication headers. The getWalletPositions function includes normalization logic to transform the API response into the shape expected by the UI components. You can get detailed information about the Deframe Pods API here.

Create Transaction Operations Hook

This is where most of the core transaction logic lives. Create src/lib/useTransactionOperations.ts for handling deposits and withdrawals. Important: Deframe Pods currently requires bundled transactions, which means users must connect with a smart wallet (like ZeroDev) to interact with yield strategies. Regular wallets (EOAs) are not supported for deposits and withdrawals because the Pods API returns multiple transaction calls that need to be bundled together atomically. This hook:
  • Detects smart wallet - Checks if the user is connected via a ZeroDev smart wallet (required for Deframe Pods)
  • Bundles transactions - Combines multiple operations (approve, deposit, etc.) into a single atomic transaction
  • Handles gas sponsorship - Smart wallet transactions are gasless for users
  • Manages state - Tracks operation status and errors for UI feedback
The hook converts human-readable amounts (e.g., “1.5”) into the smallest unit format required by the blockchain (e.g., “1500000000000000000” for 18 decimals), fetches the transaction bytecode from the Pods API, and executes the transactions using the bundled method.
src/lib/useTransactionOperations.ts
import { useState, useCallback } from "react";
import { WalletClient } from "viem";
import { client as podsClient } from "./pods";
import type { Strategy, TransactionCall } from "./pods-types";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { isZeroDevConnector } from "@dynamic-labs/ethereum-aa";

export function useTransactionOperations(
  walletClient: WalletClient | null,
  selectedChainId: number
) {
  const { primaryWallet } = useDynamicContext();
  const [isOperating, setIsOperating] = useState(false);
  const [operationError, setOperationError] = useState<Error | null>(null);

  const getKernelClient = useCallback(async () => {
    if (!primaryWallet || !isEthereumWallet(primaryWallet)) return null;
    const { connector } = primaryWallet;
    if (!isZeroDevConnector(connector)) return null;
    await connector.getNetwork();
    return connector.getAccountAbstractionProvider({ withSponsorship: true });
  }, [primaryWallet]);

  const executeBundledTransaction = useCallback(
    async (calls: TransactionCall[]): Promise<string> => {
      setIsOperating(true);
      setOperationError(null);
      try {
        const kernelClient = await getKernelClient();
        if (!kernelClient) throw new Error("Smart wallet unavailable");

        const callData = await kernelClient.account.encodeCalls(calls);
        const userOpHash = await kernelClient.sendUserOperation({ callData });
        const receipt = await kernelClient.waitForUserOperationReceipt({
          hash: userOpHash,
        });
        return receipt.receipt.transactionHash as string;
      } catch (error) {
        const err = error instanceof Error ? error : new Error(String(error));
        setOperationError(err);
        throw err;
      } finally {
        setIsOperating(false);
      }
    },
    [getKernelClient]
  );

  const executeDeposit = async (
    strategy: Strategy,
    amount: string
  ) => {
    const walletAddress =
      primaryWallet?.address || walletClient?.account?.address;
    if (!walletAddress) {
      throw new Error("Wallet not connected");
    }

    setIsOperating(true);
    setOperationError(null);

    try {
      const decimals = strategy.assetDecimals;
      const amountInSmallestUnit = BigInt(
        Math.floor(parseFloat(amount) * 10 ** decimals)
      ).toString();

      const { bytecode } = await podsClient.getDepositBytecode({
        strategyId: strategy.id,
        chainId: selectedChainId,
        amount: amountInSmallestUnit,
        asset: strategy.assetName,
        wallet: walletAddress,
      });

      const kernelClient = await getKernelClient();
      if (kernelClient) {
        const calls: TransactionCall[] = bytecode.map((tx) => ({
          to: tx.to as `0x${string}`,
          value: BigInt(tx.value),
          data: tx.data as `0x${string}`,
        }));
        return await executeBundledTransaction(calls);
      }

      if (!walletClient?.account) throw new Error("Wallet client unavailable");
      let lastHash: string | undefined;
      for (const tx of bytecode) {
        const hash = await walletClient.sendTransaction({
          chain: walletClient.chain,
          account: walletClient.account,
          to: tx.to as `0x${string}`,
          value: BigInt(tx.value),
          data: tx.data as `0x${string}`,
        });
        lastHash = hash;
      }
      return lastHash!;
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      setOperationError(err);
      throw err;
    } finally {
      setIsOperating(false);
    }
  };

  const executeWithdraw = async (
    strategy: Strategy,
    amount: string
  ) => {
    const walletAddress =
      primaryWallet?.address || walletClient?.account?.address;
    if (!walletAddress) {
      throw new Error("Wallet not connected");
    }

    setIsOperating(true);
    setOperationError(null);

    try {
      const decimals = strategy.assetDecimals;
      const amountInSmallestUnit = BigInt(
        Math.floor(parseFloat(amount) * 10 ** decimals)
      ).toString();

      const { bytecode } = await podsClient.getWithdrawBytecode({
        strategyId: strategy.id,
        chainId: selectedChainId,
        amount: amountInSmallestUnit,
        asset: strategy.assetName,
        wallet: walletAddress,
      });

      const kernelClient = await getKernelClient();
      if (kernelClient) {
        const calls: TransactionCall[] = bytecode.map((tx) => ({
          to: tx.to as `0x${string}`,
          value: BigInt(tx.value),
          data: tx.data as `0x${string}`,
        }));
        return await executeBundledTransaction(calls);
      }

      if (!walletClient?.account) throw new Error("Wallet client unavailable");
      let lastHash: string | undefined;
      for (const tx of bytecode) {
        const hash = await walletClient.sendTransaction({
          account: walletClient.account,
          to: tx.to as `0x${string}`,
          value: BigInt(tx.value),
          data: tx.data as `0x${string}`,
        } as Parameters<typeof walletClient.sendTransaction>[0]);
        lastHash = hash;
      }
      return lastHash!;
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      setOperationError(err);
      throw err;
    } finally {
      setIsOperating(false);
    }
  };

  return {
    isOperating,
    operationError,
    executeDeposit,
    executeWithdraw,
  };
}
The executeBundledTransaction function uses ZeroDev’s account abstraction to encode multiple calls into a single user operation, which is then sponsored (gasless) and executed atomically. Since Deframe Pods requires bundled transactions, the fallback code for regular wallets (EOAs) is included for completeness but will not work with Deframe’s current API requirements. The hook returns isOperating and operationError states that can be used in your UI to show loading states and handle errors.

Build Yield Interface Component

Create src/components/YieldInterface.tsx for the main UI:
src/components/YieldInterface.tsx
"use client";

import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { useEffect, useState, useCallback } from "react";
import { useChainId, useSwitchChain } from "wagmi";
import { mainnet, base, polygon } from "viem/chains";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useTransactionOperations } from "../lib/useTransactionOperations";
import { getChainName } from "../lib/utils";
import { client as podsClient } from "../lib/pods";
import type {
  Strategy,
  WalletPositions,
  Position,
  PositionCardProps,
  StrategyCardProps,
} from "../lib/pods-types";

export function YieldInterface() {
  const { primaryWallet } = useDynamicContext();
  const wagmiChainId = useChainId();
  const { switchChain } = useSwitchChain();
  const [selectedChainId, setSelectedChainId] = useState<number>(base.id);
  const [isSwitching, setIsSwitching] = useState(false);
  const [chainError, setChainError] = useState<string | null>(null);
  const [refreshKey, setRefreshKey] = useState(0);
  const [lastTransaction, setLastTransaction] = useState<{
    type: string;
    hash: string;
    timestamp: number;
  } | null>(null);
  const [strategies, setStrategies] = useState<Strategy[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [positions, setPositions] = useState<WalletPositions | null>(null);

  // Use selectedChainId if wallet is connected and synced, otherwise use local state
  const chainId = primaryWallet ? wagmiChainId : selectedChainId;

  const fetchStrategies = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await podsClient.getStrategies(chainId, 100);
      const activeStrategies = response.data.filter(
        (s) => s.isActive !== false
      );
      setStrategies(activeStrategies);
      setRefreshKey((prev) => prev + 1);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setIsLoading(false);
    }
  }, [chainId]);

  useEffect(() => {
    fetchStrategies();
  }, [fetchStrategies]);

  useEffect(() => {
    const fetchPositions = async () => {
      if (!primaryWallet?.address) {
        setPositions(null);
        return;
      }

      try {
        const data = await podsClient.getWalletPositions(primaryWallet.address);
        setPositions(data);
      } catch (err) {
        console.error("Failed to fetch positions:", err);
        setPositions(null);
      }
    };

    fetchPositions();
  }, [primaryWallet?.address]);

  const handleSwitchChain = async (targetChainId: number) => {
    if (primaryWallet && isEthereumWallet(primaryWallet)) {
      setIsSwitching(true);
      setChainError(null);

      try {
        if (primaryWallet.connector.supportsNetworkSwitching()) {
          await primaryWallet.switchNetwork(targetChainId);
        } else if (switchChain) {
          await switchChain({ chainId: targetChainId as 1 | 8453 | 137 });
        } else {
          setChainError("Your wallet doesn't support network switching");
        }
      } catch (err) {
        console.error("Failed to switch chain:", err);
        setChainError("Failed to switch chain. Please try again.");
      } finally {
        setIsSwitching(false);
      }
    } else {
      setSelectedChainId(targetChainId);
      setChainError(null);
    }
  };

  useEffect(() => {
    if (lastTransaction) {
      const timer = setTimeout(() => {
        setLastTransaction(null);
      }, 10000);
      return () => clearTimeout(timer);
    }
  }, [lastTransaction]);

  useEffect(() => {
    if (primaryWallet && wagmiChainId) {
      setSelectedChainId(wagmiChainId);
    }
  }, [primaryWallet, wagmiChainId]);

  const { isOperating, executeDeposit, executeWithdraw } =
    useTransactionOperations(null, chainId);

  const handleDeposit = async (strategy: Strategy, amount: string) => {
    try {
      const hash = await executeDeposit(strategy, amount);
      if (hash) {
        setLastTransaction({
          type: "Deposit",
          hash,
          timestamp: Date.now(),
        });
        await fetchStrategies();
      }
    } catch (error) {
      console.error("Deposit failed:", error);
      setError(error instanceof Error ? error.message : "Deposit failed");
    }
  };

  const handleWithdraw = async (strategy: Strategy, amount: string) => {
    try {
      const hash = await executeWithdraw(strategy, amount);
      if (hash) {
        setLastTransaction({
          type: "Withdraw",
          hash,
          timestamp: Date.now(),
        });
        await fetchStrategies();
        if (primaryWallet?.address) {
          const data = await podsClient.getWalletPositions(
            primaryWallet.address
          );
          setPositions(data);
        }
      }
    } catch (error) {
      console.error("Withdraw failed:", error);
      setError(error instanceof Error ? error.message : "Withdraw failed");
    }
  };

  const handlePositionWithdraw = async (position: Position, amount: string) => {
    try {
      let strategy: Strategy | null = null;
      if (position.strategyId) {
        strategy = await podsClient.getStrategy(position.strategyId);
      }

      if (!strategy) {
        strategy = {
          asset: position.asset.address,
          protocol: position.protocol,
          assetName: position.asset.symbol,
          network: "",
          networkId: "",
          implementationSelector: position.protocol,
          startDate: "",
          underlyingAsset: position.asset.address,
          assetDecimals: parseInt(position.asset.decimals),
          underlyingDecimals: parseInt(position.asset.decimals),
          id: `${position.protocol}-${position.asset.symbol}`,
          fee: "0",
        };
      }

      await handleWithdraw(strategy, amount);
    } catch (e) {
      console.error("Failed to resolve strategy for withdraw", e);
      throw e;
    }
  };

  if (isSwitching) {
    return (
      <div className="min-h-screen flex items-center justify-center mt-24">
        <Card className="max-w-md mx-auto">
          <CardContent className="pt-6">
            <p className="text-center text-muted-foreground">
              Switching to {getChainName(chainId)}...
            </p>
          </CardContent>
        </Card>
      </div>
    );
  }

  return (
    <div key={`yield-${chainId}-${refreshKey}`} className="space-y-6 mt-6">
      <h1 className="text-3xl font-bold text-center">
        Yield Strategies with Dynamic
      </h1>

      {lastTransaction && (
        <Card className="max-w-5xl mx-auto bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
          <CardContent className="pt-6">
            <p className="text-center text-green-600 dark:text-green-400">
              ✅ {lastTransaction.type} transaction sent! Hash:{" "}
              {lastTransaction.hash.slice(0, 10)}...
            </p>
          </CardContent>
        </Card>
      )}

      {chainError && (
        <Card className="max-w-5xl mx-auto bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
          <CardContent className="pt-6">
            <div className="flex items-center justify-between">
              <p className="text-center text-yellow-600 dark:text-yellow-400">
                ⚠️ {chainError}
              </p>
              <Button
                variant="outline"
                size="sm"
                onClick={() => setChainError(null)}
                className="text-yellow-600 dark:text-yellow-400"
              >
                Dismiss
              </Button>
            </div>
          </CardContent>
        </Card>
      )}

      {error && (
        <Card className="max-w-5xl mx-auto bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800">
          <CardContent className="pt-6">
            <div className="flex items-center justify-between">
              <p className="text-center text-red-600 dark:text-red-400">
                ❌ {error}
              </p>
              <Button
                variant="outline"
                size="sm"
                onClick={() => setError(null)}
                className="text-red-600 dark:text-red-400"
              >
                Dismiss
              </Button>
            </div>
          </CardContent>
        </Card>
      )}

      {/* Open Positions Section */}
      {positions && positions.positions.length > 0 && (
        <Card className="max-w-5xl mx-auto">
          <CardHeader>
            <CardTitle>Your Open Positions</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
              {positions.positions.map((position, idx) => (
                <PositionCard
                  key={idx}
                  position={position}
                  isOperating={isOperating}
                  onWithdraw={handlePositionWithdraw}
                />
              ))}
            </div>
          </CardContent>
        </Card>
      )}

      {/* Available Strategies Section */}
      <Card className="max-w-5xl mx-auto">
        <CardHeader>
          <CardTitle>Available Strategies</CardTitle>
        </CardHeader>
        <CardContent>
          {isLoading ? (
            <p className="text-muted-foreground">Loading strategies...</p>
          ) : error ? (
            <p className="text-destructive">
              Error loading strategies: {error}
            </p>
          ) : strategies && strategies.length > 0 ? (
            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
              {strategies.map((strategy) => (
                <StrategyCard
                  key={strategy.id}
                  strategy={strategy}
                  isOperating={isOperating}
                  primaryWallet={primaryWallet}
                  onDeposit={handleDeposit}
                  onWithdraw={handleWithdraw}
                />
              ))}
            </div>
          ) : (
            <div className="text-center space-y-4">
              <p className="text-muted-foreground">
                No strategies found for {getChainName(chainId)}.
              </p>
              <div className="space-y-2">
                <p className="text-sm text-muted-foreground">
                  Try switching to a supported network:
                </p>
                <div className="flex flex-wrap gap-2 justify-center">
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => handleSwitchChain(mainnet.id)}
                    disabled={isSwitching || Number(chainId) === mainnet.id}
                  >
                    {mainnet.name}
                  </Button>
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => handleSwitchChain(base.id)}
                    disabled={isSwitching || Number(chainId) === base.id}
                  >
                    {base.name}
                  </Button>
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => handleSwitchChain(polygon.id)}
                    disabled={isSwitching || Number(chainId) === polygon.id}
                  >
                    {polygon.name}
                  </Button>
                </div>
              </div>
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  );
}

function PositionCard({
  position,
  isOperating,
  onWithdraw,
}: PositionCardProps) {
  const [amount, setAmount] = useState("");

  const handleAction = () => {
    if (!amount || parseFloat(amount) <= 0) return;
    onWithdraw(position, amount);
    setAmount("");
  };

  const apyPercent = (parseFloat(position.apy) * 100).toFixed(2);

  return (
    <Card className="border-blue-200 dark:border-blue-800">
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle>{position.asset.symbol}</CardTitle>
          <span className="text-sm text-muted-foreground">
            {position.protocol}
          </span>
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="space-y-2">
          <div className="flex items-center justify-between">
            <span className="text-sm text-muted-foreground">Balance</span>
            <span className="font-semibold">
              {position.balance.humanized.toFixed(4)} {position.asset.symbol}
            </span>
          </div>
          <div className="flex items-center justify-between">
            <span className="text-sm text-muted-foreground">USD Value</span>
            <span className="font-semibold">${position.balanceUSD}</span>
          </div>
          <div className="flex items-center justify-between">
            <span className="text-sm text-muted-foreground">Current APY</span>
            <span className="text-lg font-bold text-blue-600">
              {apyPercent}%
            </span>
          </div>
        </div>

        {position.rewards && position.rewards.length > 0 && (
          <div className="space-y-1">
            <p className="text-xs font-semibold text-muted-foreground">
              Rewards:
            </p>
            {position.rewards.map((reward, idx) => (
              <div key={idx} className="flex justify-between text-sm">
                <span>
                  {reward.amount} {reward.token.symbol}
                </span>
                <span className="text-muted-foreground">
                  ${reward.amountUSD}
                </span>
              </div>
            ))}
          </div>
        )}

        <div className="space-y-2 pt-2 border-t">
          <input
            type="number"
            placeholder="Amount to withdraw"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            className="w-full px-3 py-2 border rounded-md"
            disabled={isOperating}
            max={position.balance.humanized}
          />
          <Button
            onClick={handleAction}
            variant="outline"
            disabled={
              isOperating ||
              !amount ||
              parseFloat(amount) <= 0 ||
              parseFloat(amount) > position.balance.humanized
            }
            className="w-full"
          >
            Withdraw
          </Button>
        </div>
      </CardContent>
    </Card>
  );
}

function StrategyCard({
  strategy,
  isOperating,
  primaryWallet,
  onDeposit,
  onWithdraw,
}: StrategyCardProps) {
  const [amount, setAmount] = useState("");
  const [isDeposit, setIsDeposit] = useState(true);

  const handleAction = () => {
    if (!amount || parseFloat(amount) <= 0) return;

    if (isDeposit) {
      onDeposit(strategy, amount);
    } else {
      onWithdraw(strategy, amount);
    }

    setAmount("");
  };

  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle>{strategy.assetName}</CardTitle>
          <span className="text-sm text-muted-foreground">
            {strategy.protocol}
          </span>
        </div>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="flex gap-2">
          <Button
            variant={isDeposit ? "default" : "outline"}
            size="sm"
            onClick={() => setIsDeposit(true)}
            disabled={isOperating}
          >
            Deposit
          </Button>
          <Button
            variant={!isDeposit ? "default" : "outline"}
            size="sm"
            onClick={() => setIsDeposit(false)}
            disabled={isOperating}
          >
            Withdraw
          </Button>
        </div>

        <div className="space-y-2">
          <input
            type="number"
            placeholder="Amount"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            className="w-full px-3 py-2 border rounded-md"
            disabled={isOperating || !primaryWallet}
          />
          <Button
            onClick={handleAction}
            disabled={
              isOperating ||
              !primaryWallet ||
              !amount ||
              parseFloat(amount) <= 0
            }
            className="w-full"
          >
            {isDeposit ? "Deposit" : "Withdraw"}
          </Button>
        </div>

        {!primaryWallet && (
          <p className="text-xs text-muted-foreground text-center">
            Connect wallet to interact
          </p>
        )}
      </CardContent>
    </Card>
  );
}
This component displays available yield strategies and user positions. It handles deposits and withdrawals using smart wallet bundling (required for Deframe Pods). Key Features:
  • View strategies without wallet: Strategies are displayed even when no wallet is connected, allowing users to browse available options before connecting
  • Default chain: The interface defaults to Base network when no wallet is connected
  • Chain switching: Users can switch between networks (Ethereum, Base, Polygon) to view strategies for different chains, even without a wallet connected
  • Smart wallet integration: When a wallet is connected, the component automatically syncs to the wallet’s current chain and enables transaction execution

Run the Application

Start the development server:
npm run dev
The application will be available at http://localhost:3000.

Configure CORS

Add your local development URL to the CORS origins in your Dynamic dashboard under Developer Settings > CORS Origins.

How It Works

Smart Wallet Bundling

When users connect via a ZeroDev smart wallet, the application:
  1. Detects Smart Wallet - Checks if the user is connected via a ZeroDev smart wallet
  2. Fetches Transaction Bytecode - Gets the required transaction calls from the Deframe Pods API
  3. Bundles Transactions - Combines multiple operations (approve, deposit, etc.) into a single user operation
  4. Sponsors Gas - Transactions are gasless for the user
  5. Executes Atomically - All operations succeed or fail together

Smart Wallet Requirement

Since Deframe Pods requires bundled transactions, users must connect with a smart wallet (like ZeroDev) to interact with yield strategies. Regular wallets (EOAs) cannot execute the multiple transaction calls that Deframe Pods requires, as these need to be bundled together atomically.

Conclusion

If you want to take a look at the full source code, check out the GitHub repository. This integration demonstrates how Dynamic’s smart wallets can seamlessly connect to Deframe Pods yield strategies, providing users with:
  • Gasless Transactions - Smart wallet transactions are sponsored
  • Transaction Bundling - Multiple operations in a single atomic transaction
  • Multi-Protocol Access - Unified interface for various DeFi protocols

Additional Resources