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

# Build Escrow dApp

> Build a complete escrow dApp with minting, balance tracking, and escrow management

# Build Escrow dApp

In this section, you'll build a complete escrow dApp that allows users to:

* Claim mock tokens (Mock Coin, Mock TBTC, Mock zSUI)
* View token balances
* Create, accept, and cancel escrow transactions

## Overview

The escrow dApp consists of:

1. **Balance Display** - Show all token balances
2. **Faucet Components** - Claim mock tokens for testing
3. **Escrow Manager** - Create and manage escrow transactions

## 1. Display Token Balances

Create a component to display all token balances including native SUI and mock tokens.

```tsx title="src/components/Balance.tsx" theme={null}
import { useSuiClientQuery, useCurrentAccount } from "@mysten/dapp-kit";
import { Card, CardContent } from "./ui/card";

const PACKAGE_ID =
  "0xfe02aaaf954b752272ea188d398e36d1d117d3641f4b90d21b2f0df3dfcf18a2";

export default function Balance() {
  const account = useCurrentAccount();

  // Native SUI balance
  const { data: suiData } = useSuiClientQuery(
    "getBalance",
    {
      owner: account?.address as string,
    },
    {
      enabled: !!account,
    },
  );

  // Mock Coin balance
  const { data: mockCoinData } = useSuiClientQuery(
    "getBalance",
    {
      owner: account?.address as string,
      coinType: `${PACKAGE_ID}::mock_coin::MOCK_COIN`,
    },
    {
      enabled: !!account,
    },
  );

  // Mock TBTC balance
  const { data: mockTbtcData } = useSuiClientQuery(
    "getBalance",
    {
      owner: account?.address as string,
      coinType: `${PACKAGE_ID}::mock_tbtc::MOCK_TBTC`,
    },
    {
      enabled: !!account,
    },
  );

  // Mock zSUI balance
  const { data: mockZsuiData } = useSuiClientQuery(
    "getBalance",
    {
      owner: account?.address as string,
      coinType: `${PACKAGE_ID}::mock_zsui::MOCK_ZSUI`,
    },
    {
      enabled: !!account,
    },
  );

  const suiBalance = Number(suiData?.totalBalance ?? 0) / 1_000_000_000;
  const mockCoinBalance =
    Number(mockCoinData?.totalBalance ?? 0) / 1_000_000_000;
  const mockTbtcBalance =
    Number(mockTbtcData?.totalBalance ?? 0) / 1_000_000_000;
  const mockZsuiBalance =
    Number(mockZsuiData?.totalBalance ?? 0) / 1_000_000_000;

  return (
    <div className="flex gap-4 flex-wrap">
      <Card className="w-fit">
        <CardContent>
          <span>SUI: {suiBalance.toFixed(2)}</span>
        </CardContent>
      </Card>
      <Card className="w-fit">
        <CardContent>
          <span>Mock Coin: {mockCoinBalance.toFixed(2)}</span>
        </CardContent>
      </Card>
      <Card className="w-fit">
        <CardContent>
          <span>Mock TBTC: {mockTbtcBalance.toFixed(2)}</span>
        </CardContent>
      </Card>
      <Card className="w-fit">
        <CardContent>
          <span>Mock zSUI: {mockZsuiBalance.toFixed(2)}</span>
        </CardContent>
      </Card>
    </div>
  );
}
```

**Key Features:**

* Queries balances for all coin types
* Converts from smallest units (9 decimals) to human-readable format
* Displays all balances in cards

## 2. Faucet

Create components to claim test tokens. All three mint components follow the same pattern.

### Faucet Mock Coin

```tsx title="src/components/transaction/Faucet.tsx" theme={null}
import { Transaction } from "@mysten/sui/transactions";
import {
  useSignAndExecuteTransaction,
  useSuiClient,
  useCurrentAccount,
} from "@mysten/dapp-kit";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "../ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { TransactionDialog } from "../ui/dialog-transaction";
import { useState } from "react";

const PACKAGE_ID =
  "0xfe02aaaf954b752272ea188d398e36d1d117d3641f4b90d21b2f0df3dfcf18a2";
const FAUCET_ID =
  "0x4f5135f2706e1371adf34002e351c76d9c42d0b3a10c0a5dcc32e0f7605d48b0";

type CoinType = "MOCK_COIN" | "MOCK_TBTC" | "MOCK_ZSUI";

const coinConfigs = {
  MOCK_COIN: {
    label: "Mock Coin",
    typeArg: `${PACKAGE_ID}::mock_coin::MOCK_COIN`,
  },
  MOCK_TBTC: {
    label: "Mock TBTC",
    typeArg: `${PACKAGE_ID}::mock_tbtc::MOCK_TBTC`,
  },
  MOCK_ZSUI: {
    label: "Mock zSUI",
    typeArg: `${PACKAGE_ID}::mock_zsui::MOCK_ZSUI`,
  },
};

function FaucetTab({ coinType }: { coinType: CoinType }) {
  const account = useCurrentAccount();
  const client = useSuiClient();
  const queryClient = useQueryClient();
  const { mutate: signAndExecuteTransaction } = useSignAndExecuteTransaction();
  const [dialogOpen, setDialogOpen] = useState(false);
  const [txDigest, setTxDigest] = useState("");

  const config = coinConfigs[coinType];

  const handleClaim = () => {
    if (!account) return;

    const tx = new Transaction();

    tx.moveCall({
      target: `${PACKAGE_ID}::faucet::claim`,
      typeArguments: [config.typeArg],
      arguments: [tx.object(FAUCET_ID), tx.object("0x6")],
    });

    signAndExecuteTransaction(
      {
        transaction: tx,
      },
      {
        onSuccess: (result) => {
          console.log(`${config.label} claim successful!`, result);
          setTxDigest(result.digest);
          setDialogOpen(true);
          client.waitForTransaction({ digest: result.digest });
          // Invalidate balance queries to refresh balance display
          queryClient.invalidateQueries({ queryKey: ["sui", "getBalance"] });
        },
        onError: (error) => {
          console.error(`${config.label} claim failed:`, error);
        },
      },
    );
  };

  return (
    <>
      <TransactionDialog
        open={dialogOpen}
        onOpenChange={setDialogOpen}
        digest={txDigest}
        title={`${config.label} Claimed Successfully!`}
        description={`You have successfully claimed 10,000 ${config.label}.`}
      />
      <div className="space-y-4">
        <div className="text-sm text-muted-foreground">
          You can claim 10,000 {config.label} every 1 minute.
        </div>
        <Button onClick={handleClaim} disabled={!account}>
          Claim {config.label}
        </Button>
      </div>
    </>
  );
}

export default function Faucet() {
  return (
    <Card className="w-full max-w-2xl">
      <CardHeader>
        <CardTitle>Faucet</CardTitle>
      </CardHeader>
      <CardContent>
        <Tabs defaultValue="mock_coin" className="w-full">
          <TabsList className="grid w-full grid-cols-3">
            <TabsTrigger value="mock_coin" asChild>
              <Button variant="noShadow">Mock Coin</Button>
            </TabsTrigger>
            <TabsTrigger value="mock_tbtc" asChild>
              <Button variant="noShadow">Mock TBTC</Button>
            </TabsTrigger>
            <TabsTrigger value="mock_zsui" asChild>
              <Button variant="noShadow">Mock zSUI</Button>
            </TabsTrigger>
          </TabsList>

          <TabsContent value="mock_coin">
            <FaucetTab coinType="MOCK_COIN" />
          </TabsContent>

          <TabsContent value="mock_tbtc">
            <FaucetTab coinType="MOCK_TBTC" />
          </TabsContent>

          <TabsContent value="mock_zsui">
            <FaucetTab coinType="MOCK_ZSUI" />
          </TabsContent>
        </Tabs>
      </CardContent>
    </Card>
  );
}
```

**Key Features:**

* Automatically fetches TreasuryCap objects from user's wallet
* Displays treasury caps in a dropdown
* Accepts human-readable amounts (e.g., "1" = 1 token)
* Converts to smallest units before sending transaction

## 3. Escrow Manager

Create a unified component to manage all escrow operations with tabs.

```tsx title="src/components/transaction/Escrow.tsx" theme={null}
import { Transaction } from "@mysten/sui/transactions";
import {
  useSignAndExecuteTransaction,
  useSuiClient,
  useCurrentAccount,
  useSuiClientQuery,
} from "@mysten/dapp-kit";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "../ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { Input } from "../ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { TransactionDialog } from "../ui/dialog-transaction";
import { useState, useEffect, useMemo } from "react";

const PACKAGE_ID =
  "0xfe02aaaf954b752272ea188d398e36d1d117d3641f4b90d21b2f0df3dfcf18a2";

const coinTypes = [
  { value: "0x2::sui::SUI", label: "Sui" },
  { value: `${PACKAGE_ID}::mock_coin::MOCK_COIN`, label: "Mock Coin" },
  { value: `${PACKAGE_ID}::mock_tbtc::MOCK_TBTC`, label: "Mock TBTC" },
  { value: `${PACKAGE_ID}::mock_zsui::MOCK_ZSUI`, label: "Mock zSUI" },
  { value: "custom", label: "Custom Coin Type" },
];

export default function Escrow() {
  const account = useCurrentAccount();
  const client = useSuiClient();
  const queryClient = useQueryClient();
  const { mutate: signAndExecuteTransaction } = useSignAndExecuteTransaction();

  // Create Escrow State
  const [depositCoinId, setDepositCoinId] = useState("");
  const [depositCoinType, setDepositCoinType] = useState(
    `${PACKAGE_ID}::mock_coin::MOCK_COIN`,
  );
  const [isCustomDeposit, setIsCustomDeposit] = useState(false);
  const [paymentCoinType, setPaymentCoinType] = useState(
    `${PACKAGE_ID}::mock_zsui::MOCK_ZSUI`,
  );
  const [isCustomPayment, setIsCustomPayment] = useState(false);

  const [depositAmount, setDepositAmount] = useState("");
  const [requestedAmount, setRequestedAmount] = useState("1");

  // Accept Escrow State
  const [acceptEscrowId, setAcceptEscrowId] = useState("");
  const [acceptPaymentCoinId, setAcceptPaymentCoinId] = useState("");
  const [acceptDepositType, setAcceptDepositType] = useState(
    "mock_coin::MOCK_COIN",
  );
  const [acceptPaymentType, setAcceptPaymentType] = useState(
    "mock_zsui::MOCK_ZSUI",
  );

  // Cancel Escrow State
  const [cancelEscrowId, setCancelEscrowId] = useState("");
  const [cancelDepositType, setCancelDepositType] = useState(
    "mock_coin::MOCK_COIN",
  );
  const [cancelPaymentType, setCancelPaymentType] = useState(
    "mock_zsui::MOCK_ZSUI",
  );

  // Transaction Dialog State
  const [dialogOpen, setDialogOpen] = useState(false);
  const [txDigest, setTxDigest] = useState("");
  const [txTitle, setTxTitle] = useState("");
  const [txDescription, setTxDescription] = useState("");

  // Fetch deposit coins for create escrow
  const { data: depositCoins } = useSuiClientQuery(
    "getCoins",
    {
      owner: account?.address as string,
      coinType: depositCoinType,
    },
    {
      enabled: !!account,
    },
  );

  // Fetch payment coins for accept escrow
  const { data: paymentCoins } = useSuiClientQuery(
    "getCoins",
    {
      owner: account?.address as string,
      coinType: acceptPaymentType,
    },
    {
      enabled: !!account && !!acceptPaymentType,
    },
  );

  // Fetch escrow object details for auto-detection
  const { data: escrowObject } = useSuiClientQuery(
    "getObject",
    {
      id: acceptEscrowId,
      options: {
        showType: true,
        showContent: true,
      },
    },
    {
      enabled: !!acceptEscrowId,
    },
  );

  // Fetch escrow object for Cancel tab
  const { data: cancelEscrowObject } = useSuiClientQuery(
    "getObject",
    {
      id: cancelEscrowId,
      options: {
        showType: true,
        showContent: true,
      },
    },
    {
      enabled: !!cancelEscrowId,
    },
  );

  // Auto-detect coin types from escrow object
  useEffect(() => {
    if (escrowObject?.data?.type) {
      const type = escrowObject.data.type;
      // Extract type parameters from: 0xPACKAGE::simple_escrow::Escrow<DepositType, PaymentType>
      const match = type.match(
        /Escrow<(.+)::(\w+)::(\w+),\s*(.+)::(\w+)::(\w+)>/,
      );

      if (match) {
        const depositPackage = match[1];
        const depositModule = match[2];
        const depositStruct = match[3];
        const paymentPackage = match[4];
        const paymentModule = match[5];
        const paymentStruct = match[6];

        // Set the coin types
        setAcceptDepositType(
          `${depositPackage}::${depositModule}::${depositStruct}`,
        );
        setAcceptPaymentType(
          `${paymentPackage}::${paymentModule}::${paymentStruct}`,
        );
      }
    }
  }, [escrowObject]);

  // Auto-detect coin types for Cancel tab
  useEffect(() => {
    if (cancelEscrowObject?.data?.type) {
      const type = cancelEscrowObject.data.type;
      const match = type.match(
        /Escrow<(.+)::(\w+)::(\w+),\s*(.+)::(\w+)::(\w+)>/,
      );

      if (match) {
        const depositPackage = match[1];
        const depositModule = match[2];
        const depositStruct = match[3];
        const paymentPackage = match[4];
        const paymentModule = match[5];
        const paymentStruct = match[6];

        setCancelDepositType(
          `${depositPackage}::${depositModule}::${depositStruct}`,
        );
        setCancelPaymentType(
          `${paymentPackage}::${paymentModule}::${paymentStruct}`,
        );
      }
    }
  }, [cancelEscrowObject]);

  const availableDepositCoins = useMemo(
    () => depositCoins?.data || [],
    [depositCoins?.data],
  );
  const availablePaymentCoins = useMemo(
    () => paymentCoins?.data || [],
    [paymentCoins?.data],
  );

  // Auto-select payment coin when payment coins are loaded
  useEffect(() => {
    if (availablePaymentCoins.length > 0 && !acceptPaymentCoinId) {
      // Auto-select the first available payment coin
      setAcceptPaymentCoinId(availablePaymentCoins[0].coinObjectId);
    }
  }, [availablePaymentCoins, acceptPaymentCoinId]);

  // Ensure deposit and payment coin types are different
  useEffect(() => {
    if (depositCoinType === paymentCoinType) {
      // Find a different coin type
      const differentCoin = coinTypes.find((c) => c.value !== depositCoinType);
      if (differentCoin) {
        setPaymentCoinType(differentCoin.value);
      }
    }
  }, [depositCoinType, paymentCoinType]);

  const handleCreate = () => {
    if (!account || !depositCoinId) return;

    const tx = new Transaction();
    const depositType = depositCoinType;
    const paymentType = paymentCoinType;

    // Convert to smallest units (multiply by 1e9)
    const requestAmountInSmallestUnit = BigInt(
      Math.floor(Number(requestedAmount) * 1_000_000_000),
    );
    const depositAmountInSmallestUnit = BigInt(
      Math.floor(Number(depositAmount) * 1_000_000_000),
    );

    const [depositCoin] = tx.splitCoins(tx.object(depositCoinId), [
      tx.pure.u64(depositAmountInSmallestUnit),
    ]);

    tx.moveCall({
      target: `${PACKAGE_ID}::simple_escrow::create_escrow`,
      typeArguments: [depositType, paymentType],
      arguments: [depositCoin, tx.pure.u64(requestAmountInSmallestUnit)],
    });

    signAndExecuteTransaction(
      { transaction: tx },
      {
        onSuccess: (result) => {
          console.log("Escrow created!", result);
          setTxDigest(result.digest);
          setTxTitle("Escrow Created Successfully!");
          setTxDescription(
            `Your escrow has been created with ${requestedAmount} ${
              paymentCoinType.split("::")[1] || "Payment Coin"
            } requested.`,
          );
          setDialogOpen(true);
          client.waitForTransaction({ digest: result.digest });
          // Invalidate balance and coins queries
          queryClient.invalidateQueries({ queryKey: ["sui", "getBalance"] });
          queryClient.invalidateQueries({ queryKey: ["sui", "getCoins"] });
        },
        onError: (error) => console.error("Create failed:", error),
      },
    );
  };

  const handleAccept = () => {
    if (!account || !acceptEscrowId || !acceptPaymentCoinId) return;

    const tx = new Transaction();
    const depositType = acceptDepositType;
    const paymentType = acceptPaymentType;

    if (!escrowObject?.data?.content) return;
    const escrowContent = escrowObject.data.content;

    if (escrowContent.dataType !== "moveObject") {
      console.error("Escrow object is not a move object");
      return;
    }

    const requestedAmount = (escrowContent.fields as Record<string, unknown>)
      .requested_amount as string;

    const [paymentCoin] = tx.splitCoins(tx.object(acceptPaymentCoinId), [
      tx.pure.u64(requestedAmount),
    ]);

    tx.moveCall({
      target: `${PACKAGE_ID}::simple_escrow::accept_escrow`,
      typeArguments: [depositType, paymentType],
      arguments: [tx.object(acceptEscrowId), paymentCoin],
    });

    signAndExecuteTransaction(
      { transaction: tx },
      {
        onSuccess: (result) => {
          console.log("Escrow accepted!", result);
          setTxDigest(result.digest);
          setTxTitle("Escrow Accepted Successfully!");
          setTxDescription(
            "You have accepted the escrow. The deposit has been sent to you and the payment to the seller.",
          );
          setDialogOpen(true);
          client.waitForTransaction({ digest: result.digest });
          // Invalidate balance and coins queries
          queryClient.invalidateQueries({ queryKey: ["sui", "getBalance"] });
          queryClient.invalidateQueries({ queryKey: ["sui", "getCoins"] });
          // Escrow object is deleted, so we don't need to invalidate it, but we might want to clear the form
          setAcceptEscrowId("");
        },
        onError: (error) => console.error("Accept failed:", error),
      },
    );
  };

  const handleCancel = () => {
    if (!account || !cancelEscrowId) return;

    const tx = new Transaction();
    const depositType = cancelDepositType;
    const paymentType = cancelPaymentType;

    tx.moveCall({
      target: `${PACKAGE_ID}::simple_escrow::cancel_escrow`,
      typeArguments: [depositType, paymentType],
      arguments: [tx.object(cancelEscrowId)],
    });

    signAndExecuteTransaction(
      { transaction: tx },
      {
        onSuccess: (result) => {
          console.log("Escrow cancelled!", result);
          setTxDigest(result.digest);
          setTxTitle("Escrow Cancelled Successfully!");
          setTxDescription(
            "You have cancelled the escrow and received your deposit back.",
          );
          setDialogOpen(true);
          client.waitForTransaction({ digest: result.digest });
          // Invalidate balance and coins queries
          queryClient.invalidateQueries({ queryKey: ["sui", "getBalance"] });
          queryClient.invalidateQueries({ queryKey: ["sui", "getCoins"] });
        },
        onError: (error) => console.error("Cancel failed:", error),
      },
    );
  };

  return (
    <>
      <TransactionDialog
        open={dialogOpen}
        onOpenChange={setDialogOpen}
        digest={txDigest}
        title={txTitle}
        description={txDescription}
      />
      <Card className="w-full max-w-2xl">
        <CardHeader>
          <CardTitle>Escrow Manager</CardTitle>
        </CardHeader>
        <CardContent>
          <Tabs defaultValue="create" className="w-full">
            <TabsList className="grid w-full grid-cols-3">
              <TabsTrigger value="create" asChild>
                <Button variant="noShadow">Create</Button>
              </TabsTrigger>
              <TabsTrigger value="accept" asChild>
                <Button variant="noShadow">Accept</Button>
              </TabsTrigger>

              <TabsTrigger value="cancel" asChild>
                <Button variant="noShadow">Cancel</Button>
              </TabsTrigger>
            </TabsList>

            <TabsContent value="create" className="space-y-4">
              <div>
                <label className="text-sm font-medium">Deposit Coin Type</label>
                <select
                  className="w-full p-2 border rounded"
                  value={isCustomDeposit ? "custom" : depositCoinType}
                  onChange={(e) => {
                    const value = e.target.value;
                    if (value === "custom") {
                      setIsCustomDeposit(true);
                      setDepositCoinType("");
                    } else {
                      setIsCustomDeposit(false);
                      setDepositCoinType(value);
                    }
                    setDepositCoinId(""); // Reset selection when type changes
                  }}
                >
                  {coinTypes.map((coin) => (
                    <option key={coin.value} value={coin.value}>
                      {coin.label}
                    </option>
                  ))}
                </select>
                {isCustomDeposit && (
                  <Input
                    className="mt-2"
                    placeholder="Enter custom coin type (e.g. 0x...::module::COIN)"
                    value={depositCoinType}
                    onChange={(e) => setDepositCoinType(e.target.value)}
                  />
                )}
              </div>
              <div>
                <label className="text-sm font-medium">
                  Select Coin to Deposit
                </label>
                {availableDepositCoins.length > 0 ? (
                  <select
                    className="w-full p-2 border rounded"
                    value={depositCoinId}
                    onChange={(e) => {
                      setDepositCoinId(e.target.value);
                      const selectedCoin = availableDepositCoins.find(
                        (c) => c.coinObjectId === e.target.value,
                      );
                      if (selectedCoin) {
                        const balance = (
                          Number(selectedCoin.balance) / 1_000_000_000
                        ).toString();
                        setDepositAmount(balance);
                        setRequestedAmount(balance);
                      }
                    }}
                  >
                    <option value="">Select a coin</option>
                    {availableDepositCoins.map((coin) => (
                      <option key={coin.coinObjectId} value={coin.coinObjectId}>
                        {coin.coinObjectId.slice(0, 6)}...
                        {coin.coinObjectId.slice(-4)} -{" "}
                        {(Number(coin.balance) / 1_000_000_000).toFixed(2)}
                      </option>
                    ))}
                  </select>
                ) : (
                  <p className="text-sm text-gray-500">
                    No coins of this type in your wallet
                  </p>
                )}
              </div>
              <div>
                <label className="text-sm font-medium">Deposit Amount</label>
                <Input
                  type="number"
                  value={depositAmount}
                  onChange={(e) => {
                    const val = e.target.value;
                    setDepositAmount(val);
                    setRequestedAmount(val);
                  }}
                  step="0.000000001"
                  min="0"
                />
              </div>
              <div>
                <label className="text-sm font-medium">
                  Payment Coin Type (Requested)
                </label>
                <select
                  className="w-full p-2 border rounded"
                  value={isCustomPayment ? "custom" : paymentCoinType}
                  onChange={(e) => {
                    const value = e.target.value;
                    if (value === "custom") {
                      setIsCustomPayment(true);
                      setPaymentCoinType("");
                    } else {
                      setIsCustomPayment(false);
                      setPaymentCoinType(value);
                    }
                  }}
                >
                  {coinTypes.map((coin) => (
                    <option
                      key={coin.value}
                      value={coin.value}
                      disabled={
                        coin.value !== "custom" &&
                        coin.value === depositCoinType
                      }
                    >
                      {coin.label}
                      {coin.value !== "custom" && coin.value === depositCoinType
                        ? " (same as deposit)"
                        : ""}
                    </option>
                  ))}
                </select>
                {isCustomPayment && (
                  <Input
                    className="mt-2"
                    placeholder="Enter custom coin type (e.g. 0x...::module::COIN)"
                    value={paymentCoinType}
                    onChange={(e) => setPaymentCoinType(e.target.value)}
                  />
                )}
                {depositCoinType === paymentCoinType && (
                  <p className="text-sm text-red-500 mt-1">
                    ⚠️ Deposit and payment coin must be different
                  </p>
                )}
              </div>
              <div>
                <div className="flex items-center justify-between mb-2">
                  <label className="text-sm font-medium">
                    Requested Amount (1:1 Match)
                  </label>
                </div>
                <Input
                  type="number"
                  placeholder="1"
                  value={requestedAmount}
                  readOnly
                  disabled
                  className="bg-gray-100 cursor-not-allowed"
                  step="0.000000001"
                  min="0"
                />
              </div>
              <Button
                onClick={handleCreate}
                disabled={
                  !account ||
                  !depositCoinId ||
                  depositCoinType === paymentCoinType
                }
              >
                Create Escrow
              </Button>
            </TabsContent>

            <TabsContent value="accept" className="space-y-4">
              <div>
                <label className="text-sm font-medium">Escrow Object ID</label>
                <Input
                  type="text"
                  placeholder="0x..."
                  value={acceptEscrowId}
                  onChange={(e) => setAcceptEscrowId(e.target.value)}
                />
              </div>
              <div>
                <label className="text-sm font-medium">
                  Deposit Coin Type (What you receive) - Auto-detected
                </label>
                <Input
                  type="text"
                  value={
                    coinTypes.find((c) => c.value === acceptDepositType)
                      ?.label || acceptDepositType
                  }
                  disabled
                  className="bg-gray-100 cursor-not-allowed"
                />
              </div>
              <div>
                <label className="text-sm font-medium">
                  Payment Coin Type (What you pay) - Auto-detected
                </label>
                <Input
                  type="text"
                  value={
                    coinTypes.find((c) => c.value === acceptPaymentType)
                      ?.label || acceptPaymentType
                  }
                  disabled
                  className="bg-gray-100 cursor-not-allowed"
                />
              </div>
              {escrowObject?.data?.content?.dataType === "moveObject" && (
                <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
                  <div>
                    <label className="text-xs font-medium text-gray-500 uppercase">
                      You Will Receive
                    </label>
                    <p className="text-lg font-bold text-green-600">
                      {(
                        Number(
                          (
                            escrowObject.data.content.fields as Record<
                              string,
                              unknown
                            >
                          ).deposit ||
                            (
                              (
                                escrowObject.data.content.fields as Record<
                                  string,
                                  unknown
                                >
                              ).deposit as { fields: { value: string } }
                            )?.fields?.value ||
                            (
                              (
                                escrowObject.data.content.fields as Record<
                                  string,
                                  unknown
                                >
                              ).deposit as { fields: { balance: string } }
                            )?.fields?.balance ||
                            0,
                        ) / 1_000_000_000
                      ).toFixed(2)}{" "}
                      {coinTypes.find((c) => c.value === acceptDepositType)
                        ?.label || "Coins"}
                    </p>
                  </div>
                  <div>
                    <label className="text-xs font-medium text-gray-500 uppercase">
                      You Will Pay
                    </label>
                    <p className="text-lg font-bold text-red-600">
                      {(
                        Number(
                          (
                            escrowObject.data.content.fields as Record<
                              string,
                              unknown
                            >
                          ).requested_amount || 0,
                        ) / 1_000_000_000
                      ).toFixed(2)}{" "}
                      {coinTypes.find((c) => c.value === acceptPaymentType)
                        ?.label || "Coins"}
                    </p>
                  </div>
                </div>
              )}
              <div>
                <label className="text-sm font-medium">
                  Selected Payment Coin - Auto-selected
                </label>
                {availablePaymentCoins.length > 0 ? (
                  <Input
                    type="text"
                    value={
                      acceptPaymentCoinId
                        ? `${acceptPaymentCoinId.slice(
                            0,
                            6,
                          )}...${acceptPaymentCoinId.slice(-4)} - ${(
                            Number(
                              availablePaymentCoins.find(
                                (c) => c.coinObjectId === acceptPaymentCoinId,
                              )?.balance || 0,
                            ) / 1_000_000_000
                          ).toFixed(2)}`
                        : "No coin selected"
                    }
                    disabled
                    className="bg-gray-100 cursor-not-allowed"
                  />
                ) : (
                  <p className="text-sm text-red-500">
                    ⚠️ No payment coins of this type in your wallet
                  </p>
                )}
              </div>
              {escrowObject?.data?.content?.dataType === "moveObject" &&
                (escrowObject.data.content.fields as Record<string, unknown>)
                  .creator === account?.address && (
                  <p className="text-sm text-yellow-600 font-medium mb-2">
                    ⚠️ Warning: You are the creator of this escrow. Accepting it
                    means swapping with yourself.
                  </p>
                )}
              <Button
                onClick={handleAccept}
                disabled={!account || !acceptEscrowId || !acceptPaymentCoinId}
              >
                Accept Escrow
              </Button>
            </TabsContent>

            <TabsContent value="cancel" className="space-y-4">
              <div>
                <label className="text-sm font-medium">Escrow Object ID</label>
                <Input
                  type="text"
                  placeholder="0x..."
                  value={cancelEscrowId}
                  onChange={(e) => setCancelEscrowId(e.target.value)}
                />
              </div>
              <div>
                <label className="text-sm font-medium">
                  Deposit Coin Type - Auto-detected
                </label>
                <Input
                  type="text"
                  value={
                    coinTypes.find((c) => c.value === cancelDepositType)
                      ?.label || cancelDepositType
                  }
                  disabled
                  className="bg-gray-100 cursor-not-allowed"
                />
              </div>
              <div>
                <label className="text-sm font-medium">
                  Payment Coin Type - Auto-detected
                </label>
                <Input
                  type="text"
                  value={
                    coinTypes.find((c) => c.value === cancelPaymentType)
                      ?.label || cancelPaymentType
                  }
                  disabled
                  className="bg-gray-100 cursor-not-allowed"
                />
              </div>
              <p className="text-sm text-red-600">
                ⚠️ Cancel escrow to get your deposit back. Only works if no
                buyer has paid yet.
              </p>
              <Button
                onClick={handleCancel}
                disabled={!account || !cancelEscrowId}
              >
                Cancel Escrow
              </Button>
            </TabsContent>
          </Tabs>
        </CardContent>
      </Card>
    </>
  );
}
```

## Escrow Flow

### 1. Setup (Seller)

1. Claim tokens using faucet components
2. Check balance in Balance component

### 2. Create Escrow (Seller)

1. Go to **Create** tab
2. Select deposit coin type
3. Choose specific coin from dropdown
4. Select payment coin type to request
5. Enter amount to request
6. Click "Create Escrow"

### 3. Accept Escrow (Buyer)

1. Get escrow object ID from seller
2. Go to **Accept** tab
3. Paste escrow object ID
4. Coin types auto-detect
5. Payment coin auto-selects
6. Verify "You Will Receive" and "You Will Pay" amounts
7. Click "Accept Escrow"

### Alternative: Cancel Escrow (Seller)

1. Go to **Cancel** tab (only before buyer accepts)
2. Paste escrow object ID
3. Coin types auto-detect
4. Click "Cancel Escrow" to get deposit back

## Best Practices

### Amount Handling

Always convert amounts to smallest units:

```typescript theme={null}
const amountInSmallestUnit = Math.floor(Number(amount) * 1_000_000_000);
```

### Coin Queries

Use specific coin types for accurate queries:

```typescript theme={null}
coinType: `${PACKAGE_ID}::mock_coin::MOCK_COIN`;
```

### Auto-Detection

Parse escrow type to extract coin types:

```typescript theme={null}
const match = type.match(/Escrow<(.+)::(\w+)::(\w+),\s*(.+)::(\w+)::(\w+)>/);
```

### Error Handling

Always handle success and error callbacks:

```typescript theme={null}
{
  onSuccess: (result) => console.log("Success!", result),
  onError: (error) => console.error("Error:", error),
}
```

## Next Steps

* Add loading states for better UX
* Implement transaction history
* Add escrow listing view
* Create notification system for escrow events
* Add escrow cancellation refund tracking

***

**Congratulations!** You've built a complete escrow dApp with minting, balance tracking, and full escrow management.
