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

# Xây dựng trình theo dõi danh mục đầu tư trên Solana

> Xây dựng trình theo dõi danh mục đầu tư Solana, hiển thị toàn bộ tài sản nắm giữ của bất kỳ ví nào cùng với hoạt động trực tiếp và lịch sử trên chuỗi.

export const PortfolioTrackerDemo = () => {
  const [apiKey, setApiKey] = useState('');
  const [ownerAddress, setOwnerAddress] = useState('86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [portfolio, setPortfolio] = useState(null);
  const [history, setHistory] = useState(null);
  const [live, setLive] = useState([]);
  const [liveStatus, setLiveStatus] = useState('idle');
  const wsRef = useRef(null);
  const rpc = async (method, params) => {
    const res = await fetch(`https://mainnet.helius-rpc.com/?api-key=${apiKey}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: 'quickstart',
        method,
        params
      })
    });
    const json = await res.json();
    if (json.error) throw new Error(json.error.message || "Lỗi RPC");
    return json.result;
  };
  const stopLive = () => {
    if (wsRef.current) {
      try {
        wsRef.current.close();
      } catch (e) {}
      wsRef.current = null;
    }
    setLiveStatus('idle');
  };
  useEffect(() => stopLive, []);
  const handleLoad = async e => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setPortfolio(null);
    setHistory(null);
    stopLive();
    setLive([]);
    try {
      const assets = await rpc('getAssetsByOwner', {
        ownerAddress,
        page: 1,
        limit: 20,
        displayOptions: {
          showFungible: true,
          showNativeBalance: true
        }
      });
      const txns = await rpc('getTransactionsForAddress', [ownerAddress, {
        transactionDetails: 'signatures',
        sortOrder: 'desc',
        limit: 10,
        filters: {
          tokenAccounts: 'balanceChanged'
        }
      }]);
      const items = assets.items || [];
      const tokens = items.filter(i => (i.interface || '').includes('Fungible'));
      const nfts = items.filter(i => !(i.interface || '').includes('Fungible'));
      setPortfolio({
        sol: (assets.nativeBalance && assets.nativeBalance.lamports ? assets.nativeBalance.lamports : 0) / 1e9,
        tokenCount: tokens.length,
        nftCount: nfts.length,
        tokens: tokens.slice(0, 6),
        nfts: nfts.slice(0, 4)
      });
      setHistory(txns.data || []);
    } catch (err) {
      const msg = err.message || '';
      setError((/too big|too large/i).test(msg) ? "Ví này chứa quá nhiều tài sản nên không thể tải trong bản demo. Hãy thử một ví có ít token và NFT hơn hoặc giảm giới hạn trong getAssetsByOwner." : msg || "Không thể tải ví");
    } finally {
      setLoading(false);
    }
  };
  const startLive = () => {
    if (!apiKey) {
      setError("Trước tiên, hãy nhập khóa API");
      return;
    }
    setLiveStatus('connecting');
    const ws = new WebSocket(`wss://mainnet.helius-rpc.com/?api-key=${apiKey}`);
    wsRef.current = ws;
    ws.onopen = () => {
      setLiveStatus('live');
      ws.send(JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'transactionSubscribe',
        params: [{
          accountInclude: [ownerAddress],
          tokenAccounts: 'balanceChanged'
        }, {
          commitment: 'confirmed',
          encoding: 'jsonParsed',
          transactionDetails: 'signatures',
          maxSupportedTransactionVersion: 1
        }]
      }));
    };
    ws.onmessage = event => {
      const msg = JSON.parse(event.data);
      const sig = msg && msg.params && msg.params.result && msg.params.result.signature;
      if (sig) setLive(prev => [{
        sig,
        at: new Date().toLocaleTimeString()
      }, ...prev].slice(0, 8));
    };
    ws.onerror = () => setLiveStatus('error');
    ws.onclose = () => {
      if (wsRef.current === ws) setLiveStatus('idle');
    };
  };
  const short = s => s ? `${s.slice(0, 6)}...${s.slice(-6)}` : '';
  const btnClass = loading ? 'px-4 py-2 font-medium rounded-full bg-gray-300 dark:bg-gray-700 cursor-not-allowed' : 'px-4 py-2 font-medium rounded-full bg-primary hover:bg-primary/80 text-white';
  const dotClass = liveStatus === 'live' ? 'inline-block w-2 h-2 rounded-full bg-green-500 animate-pulse' : liveStatus === 'error' ? 'inline-block w-2 h-2 rounded-full bg-red-500' : 'inline-block w-2 h-2 rounded-full bg-yellow-500';
  return <div className="p-4 border dark:border-zinc-950/80 rounded-xl bg-white dark:bg-zinc-950/80 shadow-sm">
      <form onSubmit={handleLoad} className="space-y-4">
        <div>
          <label className="block text-sm text-zinc-950/70 dark:text-white/70 mb-1">Khóa API của bạn</label>
          <input type="text" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="Nhập khóa API của bạn" className="w-full p-2 border rounded dark:bg-zinc-900 dark:border-zinc-700" required />
        </div>
        <div>
          <label className="block text-sm text-zinc-950/70 dark:text-white/70 mb-1">Địa chỉ ví</label>
          <input type="text" value={ownerAddress} onChange={e => setOwnerAddress(e.target.value)} placeholder="Địa chỉ ví Solana" className="w-full p-2 border rounded dark:bg-zinc-900 dark:border-zinc-700" required />
        </div>
        <div className="flex gap-2">
          <button type="submit" disabled={loading} className={btnClass}>
            {loading ? "Đang tải..." : "Tải ví"}
          </button>
          {liveStatus === 'live' || liveStatus === 'connecting' ? <button type="button" onClick={stopLive} className="px-4 py-2 font-medium rounded-full border border-zinc-300 dark:border-zinc-700">Dừng nguồn cấp dữ liệu trực tiếp</button> : <button type="button" onClick={startLive} className="px-4 py-2 font-medium rounded-full border border-zinc-300 dark:border-zinc-700">Phát trực tiếp</button>}
        </div>
      </form>
      <p className="mt-3 text-xs text-zinc-500 dark:text-zinc-400">
        Khóa API của bạn chỉ được dùng trong trình duyệt để gọi trực tiếp đến Helius. Khóa này không bao giờ được gửi đến bất kỳ nơi nào khác.
      </p>
      {error ? <div className="mt-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded text-red-700 dark:text-red-300">
          <strong>Lỗi:</strong> {error}
        </div> : null}
      {portfolio ? <div className="mt-6 space-y-4">
          <div className="grid grid-cols-2 gap-3 text-center">
            <div className="p-3 rounded-lg border border-zinc-200 dark:border-zinc-800">
              <div className="text-xl font-semibold text-zinc-950 dark:text-white">{portfolio.sol.toFixed(3)}</div>
              <div className="text-xs text-zinc-500 dark:text-zinc-400">SOL</div>
            </div>
            <div className="p-3 rounded-lg border border-zinc-200 dark:border-zinc-800">
              <div className="text-xl font-semibold text-zinc-950 dark:text-white">{portfolio.nftCount}</div>
              <div className="text-xs text-zinc-500 dark:text-zinc-400">NFT</div>
            </div>
          </div>
          {portfolio.tokens.length > 0 ? <div>
              <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white">Token</h4>
              <ul className="space-y-1 list-none pl-0 m-0">
                {portfolio.tokens.map(t => {
    const info = t.token_info || ({});
    const amount = (info.balance || 0) / 10 ** (info.decimals || 0);
    const name = t.content && t.content.metadata && t.content.metadata.name || info.symbol || "Token không xác định";
    const img = t.content && t.content.links ? t.content.links.image : '';
    return <li key={t.id} className="flex items-center gap-3 text-sm my-0">
                      <img src={img || ''} alt="" className="w-7 h-7 rounded-full bg-zinc-200 dark:bg-zinc-800 my-0 shrink-0" />
                      <span className="flex-1 truncate text-zinc-800 dark:text-zinc-200">{name}</span>
                      <span className="font-mono text-zinc-600 dark:text-zinc-400">{amount.toLocaleString(undefined, {
      maximumFractionDigits: 4
    })}</span>
                    </li>;
  })}
              </ul>
            </div> : null}
          {portfolio.nfts.length > 0 ? <div className="grid grid-cols-4 gap-2">
              {portfolio.nfts.map(nft => <img key={nft.id} src={nft.content && nft.content.files && nft.content.files[0] ? nft.content.files[0].uri : ''} alt={nft.content && nft.content.metadata ? nft.content.metadata.name : 'NFT'} className="rounded-lg border border-zinc-200 dark:border-zinc-800 aspect-square object-cover my-0" />)}
            </div> : null}
        </div> : null}
      {history && history.length > 0 ? <div className="mt-6">
          <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white">Hoạt động gần đây</h4>
          <ul className="space-y-1 text-sm font-mono list-none pl-0 m-0">
            {history.map(tx => <li key={tx.signature} className="flex items-center justify-between text-zinc-600 dark:text-zinc-400 my-0">
                <span className="flex items-center gap-2">
                  <span className={tx.err ? 'inline-block w-2 h-2 rounded-full bg-red-500' : 'inline-block w-2 h-2 rounded-full bg-green-500'} />
                  <a href={'https://orbmarkets.io/tx/' + tx.signature} target="_blank" rel="noreferrer" className="hover:underline">{short(tx.signature)}</a>
                </span>
                <span className="text-xs">{tx.blockTime ? new Date(tx.blockTime * 1000).toLocaleDateString() : ''}</span>
              </li>)}
          </ul>
        </div> : null}
      {liveStatus !== 'idle' || live.length > 0 ? <div className="mt-6">
          <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white flex items-center gap-2">
            Nguồn cấp dữ liệu trực tiếp
            <span className={dotClass} />
            <span className="text-xs font-normal text-zinc-500 dark:text-zinc-400">{liveStatus}</span>
          </h4>
          {live.length === 0 ? <p className="text-sm text-zinc-500 dark:text-zinc-400">Đang chờ giao dịch tiếp theo liên quan đến ví này.</p> : <ul className="space-y-1 text-sm font-mono list-none pl-0 m-0">
              {live.map((item, i) => <li key={i} className="flex items-center justify-between text-zinc-600 dark:text-zinc-400 my-0">
                  <span className="flex items-center gap-2">
                    <span className="inline-block w-2 h-2 rounded-full bg-green-500" />
                    <a href={'https://orbmarkets.io/tx/' + item.sig} target="_blank" rel="noreferrer" className="hover:underline">{short(item.sig)}</a>
                  </span>
                  <span className="text-xs">{item.at}</span>
                </li>)}
            </ul>}
        </div> : null}
    </div>;
};

Trong hướng dẫn này, bạn sẽ xây dựng một bảng điều khiển hiển thị tài sản mà bất kỳ địa chỉ Solana nào đang nắm giữ và hoạt động hiện tại của địa chỉ đó. Bạn sẽ truy xuất toàn bộ danh mục token, NFT và SOL trong một lệnh gọi [DAS](/docs/vi/api-reference/das/getassetsbyowner), tải lịch sử giao dịch bằng [`getTransactionsForAddress`](/docs/vi/rpc/gettransactionsforaddress) độc quyền của Helius và truyền phát trực tiếp hoạt động mới qua [`transactionSubscribe`](/docs/vi/rpc/websocket/transaction-subscribe). Trước tiên, bạn sẽ dùng thử dưới dạng bản demo nhúng, sau đó xây dựng thành một ứng dụng Next.js hoàn chỉnh.

## Dùng thử trực tiếp

Dán khóa API và địa chỉ ví bất kỳ để xem ba phần hoạt động cùng nhau: danh mục đầu tư, lịch sử và nguồn cấp dữ liệu trực tiếp. Đây chính là dữ liệu mà ứng dụng của bạn sẽ hiển thị.

<PortfolioTrackerDemo />

***

## Xây dựng dưới dạng ứng dụng Next.js

<Card title="Grab the full app on GitHub" icon="github" href="https://github.com/helius-labs/quickstart-portfolio-tracker">
  Sao chép toàn bộ dự án khởi đầu — ứng dụng Next.js hoàn chỉnh từ hướng dẫn này, sẵn sàng để `npm install && npm run dev`.
</Card>

Tiện ích ở trên gọi trực tiếp Helius từ trình duyệt, làm lộ khóa API của bạn. Trong ứng dụng thực tế, bạn cần giữ khóa trên máy chủ. Hướng dẫn này sử dụng Next.js App Router với hai trình xử lý tuyến để **khóa của bạn không bao giờ được gửi đến máy khách**: một trình xử lý làm proxy cho các lệnh gọi RPC, trình xử lý còn lại chuyển tiếp nguồn cấp WebSocket dưới dạng Server-Sent Events.

<Info>
  **Điều kiện tiên quyết:** Node.js 18+ và một [khóa API Helius](https://dashboard.helius.dev/api-keys).
</Info>

<Steps>
  <Step title="Scaffold the project">
    ```bash theme={"system"}
    npx create-next-app@latest portfolio-tracker --ts --app --tailwind --no-src-dir --eslint --yes
    cd portfolio-tracker
    npm install helius-sdk ws
    npm install --save-dev @types/ws
    ```

    Bạn sẽ sử dụng [Helius SDK](https://github.com/helius-labs/helius-sdk) cho các lệnh gọi RPC và `ws` cho nguồn cấp WebSocket trực tiếp.

    Thêm khóa của bạn vào `.env.local`:

    ```bash .env.local theme={"system"}
    HELIUS_API_KEY=YOUR_API_KEY
    ```
  </Step>

  <Step title="Proxy RPC calls through a route handler">
    Cách này giữ khóa API của bạn ở phía máy chủ. Máy khách gửi một phương thức và các tham số đến đây, sau đó trình xử lý chạy chúng thông qua Helius SDK. SDK sẽ đính kèm khóa và tạo yêu cầu cho bạn.

    ```typescript app/api/helius/route.ts theme={"system"}
    import { NextRequest, NextResponse } from "next/server";
    import { createHelius } from "helius-sdk";

    export const runtime = "nodejs";

    const helius = createHelius({
      apiKey: process.env.HELIUS_API_KEY!,
      network: "mainnet",
    });

    export async function POST(req: NextRequest) {
      const { method, params } = await req.json();
      try {
        // The two Helius calls that power this app: the full portfolio and its history.
        const result =
          method === "getAssetsByOwner"
            ? await helius.getAssetsByOwner(params)
            : method === "getTransactionsForAddress"
            ? await helius.getTransactionsForAddress(params)
            : undefined;

        if (result === undefined) {
          return NextResponse.json(
            { error: { message: `Unsupported method: ${method}` } },
            { status: 400 }
          );
        }
        return NextResponse.json({ result });
      } catch (e) {
        return NextResponse.json({ error: { message: (e as Error).message } });
      }
    }
    ```
  </Step>

  <Step title="Relay the live feed over Server-Sent Events">
    Trình duyệt không thể mở WebSocket Helius nếu không có khóa. Thay vào đó, hãy mở WebSocket trên máy chủ và truyền phát các chữ ký đến máy khách bằng SSE.

    ```typescript app/api/stream/route.ts [expandable] theme={"system"}
    import { NextRequest } from "next/server";
    import WebSocket from "ws";

    export const runtime = "nodejs";

    export async function GET(req: NextRequest) {
      const address = req.nextUrl.searchParams.get("address");
      if (!address) return new Response("Missing address", { status: 400 });

      const stream = new ReadableStream({
        start(controller) {
          const encoder = new TextEncoder();
          const send = (data: unknown) =>
            controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));

          const ws = new WebSocket(
            `wss://mainnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`
          );

          ws.on("open", () => {
            ws.send(
              JSON.stringify({
                jsonrpc: "2.0",
                id: 1,
                method: "transactionSubscribe",
                params: [
                  // tokenAccounts: "balanceChanged" also matches the wallet's token accounts,
                  // so incoming SPL transfers (which touch an ATA, not the wallet pubkey) show up.
                  { accountInclude: [address], tokenAccounts: "balanceChanged" },
                  {
                    commitment: "confirmed",
                    encoding: "jsonParsed",
                    transactionDetails: "signatures",
                    maxSupportedTransactionVersion: 1,
                  },
                ],
              })
            );
          });

          ws.on("message", (raw) => {
            const msg = JSON.parse(raw.toString());
            const sig = msg?.params?.result?.signature;
            if (sig) send({ signature: sig, at: Date.now() });
          });

          // Tear down the upstream socket when the client disconnects.
          req.signal.addEventListener("abort", () => {
            ws.close();
            controller.close();
          });
        },
      });

      return new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          Connection: "keep-alive",
        },
      });
    }
    ```
  </Step>

  <Step title="Render the dashboard">
    Trang tải danh mục đầu tư và lịch sử thông qua proxy, sau đó đăng ký nguồn cấp SSE để nhận các bản cập nhật trực tiếp.

    ```tsx app/page.tsx [expandable] theme={"system"}
    "use client";

    import { useEffect, useState } from "react";

    const rpc = async (method: string, params: unknown) => {
      const res = await fetch("/api/helius", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ jsonrpc: "2.0", id: "1", method, params }),
      });
      const json = await res.json();
      if (json.error) throw new Error(json.error.message);
      return json.result;
    };

    export default function Home() {
      const [address, setAddress] = useState(
        "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY"
      );
      const [portfolio, setPortfolio] = useState<any>(null);
      const [history, setHistory] = useState<any[]>([]);
      const [live, setLive] = useState<any[]>([]);
      const [streamAddress, setStreamAddress] = useState<string | null>(null);
      const [error, setError] = useState<string | null>(null);

      const load = async () => {
        setError(null);
        setLive([]);
        setStreamAddress(address); // point the live feed at the wallet you just loaded
        try {
          const [assets, txns] = await Promise.all([
            rpc("getAssetsByOwner", {
              ownerAddress: address,
              page: 1,
              limit: 20,
              displayOptions: { showFungible: true, showNativeBalance: true },
            }),
            rpc("getTransactionsForAddress", [
              address,
              { transactionDetails: "signatures", sortOrder: "desc", limit: 10, filters: { tokenAccounts: "balanceChanged" } },
            ]),
          ]);
          const items = assets.items ?? [];
          setPortfolio({
            sol: (assets.nativeBalance?.lamports ?? 0) / 1e9,
            tokens: items.filter((i: any) => `${i.interface}`.includes("Fungible")),
            nfts: items.filter((i: any) => !`${i.interface}`.includes("Fungible")),
          });
          setHistory(txns.data ?? []);
        } catch (e: any) {
          setError(e.message);
        }
      };

      // Open the live feed only after Load runs, and reopen only when the loaded wallet changes
      // (not on every keystroke in the input).
      useEffect(() => {
        if (!streamAddress) return;
        const source = new EventSource(`/api/stream?address=${streamAddress}`);
        source.onmessage = (e) =>
          setLive((prev) => [JSON.parse(e.data), ...prev].slice(0, 10));
        return () => source.close();
      }, [streamAddress]);

      return (
        <main className="max-w-2xl mx-auto p-8 space-y-6">
          <div className="flex gap-2">
            <input
              className="flex-1 border rounded p-2"
              value={address}
              onChange={(e) => setAddress(e.target.value)}
            />
            <button className="px-4 rounded bg-black text-white" onClick={load}>
              Load
            </button>
          </div>

          {error && <p className="text-red-600">{error}</p>}

          {portfolio && (
            <section className="space-y-6">
              <Stat label="SOL balance" value={portfolio.sol.toFixed(3)} />

              {portfolio.tokens.length > 0 && (
                <div>
                  <h2 className="font-semibold mb-2">Tokens ({portfolio.tokens.length})</h2>
                  <ul className="space-y-1 list-none pl-0">
                    {portfolio.tokens.map((t: any) => {
                      const info = t.token_info ?? {};
                      const amount = (info.balance ?? 0) / 10 ** (info.decimals ?? 0);
                      return (
                        <li key={t.id} className="flex items-center gap-3">
                          <img
                            src={t.content?.links?.image ?? ""}
                            alt=""
                            className="w-8 h-8 rounded-full bg-zinc-200 shrink-0"
                          />
                          <span className="flex-1 truncate">
                            {t.content?.metadata?.name ?? info.symbol ?? "Unknown token"}
                          </span>
                          <span className="font-mono text-sm">
                            {amount.toLocaleString(undefined, { maximumFractionDigits: 4 })}
                          </span>
                        </li>
                      );
                    })}
                  </ul>
                </div>
              )}

              {portfolio.nfts.length > 0 && (
                <div>
                  <h2 className="font-semibold mb-2">NFTs ({portfolio.nfts.length})</h2>
                  <div className="grid grid-cols-3 gap-3">
                    {portfolio.nfts.map((n: any) => (
                      <img
                        key={n.id}
                        src={n.content?.files?.[0]?.uri ?? n.content?.links?.image ?? ""}
                        alt={n.content?.metadata?.name ?? "NFT"}
                        className="rounded-lg aspect-square object-cover border"
                      />
                    ))}
                  </div>
                </div>
              )}
            </section>
          )}

          {history.length > 0 && (
            <section>
              <h2 className="font-semibold mb-2">History</h2>
              <ul className="font-mono text-sm space-y-1 list-none pl-0">
                {history.map((tx) => (
                  <li key={tx.signature}>
                    {tx.err ? "❌" : "✅"}{" "}
                    <a
                      href={`https://orbmarkets.io/tx/${tx.signature}`}
                      target="_blank"
                      rel="noreferrer"
                      className="underline hover:text-black"
                    >
                      {tx.signature.slice(0, 16)}…
                    </a>
                  </li>
                ))}
              </ul>
            </section>
          )}

          {live.length > 0 && (
            <section>
              <h2 className="font-semibold mb-2">Live ⚡</h2>
              <ul className="font-mono text-sm space-y-1 list-none pl-0">
                {live.map((e, i) => (
                  <li key={i}>
                    <a
                      href={`https://orbmarkets.io/tx/${e.signature}`}
                      target="_blank"
                      rel="noreferrer"
                      className="underline hover:text-black"
                    >
                      {e.signature.slice(0, 16)}…
                    </a>
                  </li>
                ))}
              </ul>
            </section>
          )}
        </main>
      );
    }

    function Stat({ label, value }: { label: string; value: string | number }) {
      return (
        <div className="border rounded-lg p-3">
          <div className="text-xl font-semibold">{value}</div>
          <div className="text-xs text-zinc-500">{label}</div>
        </div>
      );
    }
    ```
  </Step>

  <Step title="Run it">
    ```bash theme={"system"}
    npm run dev
    ```

    Mở [http://localhost:3000](http://localhost:3000), dán địa chỉ ví rồi nhấn **Tải**. Danh mục đầu tư và lịch sử sẽ xuất hiện ngay lập tức, còn nguồn cấp trực tiếp sẽ được cập nhật khi có giao dịch mới liên quan đến ví.
  </Step>
</Steps>

## Cơ chế hoạt động

### Một lệnh gọi DAS cho toàn bộ danh mục đầu tư

`getAssetsByOwner` với `showFungible: true` và `showNativeBalance: true` trả về token có thể thay thế, NFT (cả loại tiêu chuẩn **và** nén) cùng số dư SOL gốc trong một phản hồi duy nhất, vì vậy bạn không cần thực hiện các lệnh gọi `getBalance` hoặc `getTokenAccountsByOwner` riêng biệt. Phân tách `items` theo `interface` để nhóm token và NFT riêng biệt.

Mỗi mục chứa siêu dữ liệu riêng, vì vậy cùng một lệnh gọi có thể cung cấp dữ liệu cho cả hai danh sách. Sử dụng `content.metadata.name` cho tên và `content.links.image` cho biểu trưng hoặc hình minh họa. Token có thể thay thế cũng bao gồm một đối tượng `token_info` với `balance` thô, `decimals` và `symbol` — chia `balance` cho `10 ** decimals` để nhận số lượng ở dạng con người có thể đọc được. Không phải token nào cũng có tên hoặc biểu trưng, vì vậy hãy xử lý dự phòng phù hợp khi thiếu các dữ liệu này.

### Toàn bộ lịch sử trong một yêu cầu

`getTransactionsForAddress` trả về lịch sử giao dịch của ví. Khi dùng cùng `filters.tokenAccounts: "balanceChanged"`, nó cũng bao gồm các giao dịch chuyển trên những tài khoản token liên kết của ví, tức hoạt động mà `getSignaturesForAddress` thông thường sẽ bỏ sót. Sử dụng `transactionDetails: "full"` để nhận dữ liệu giao dịch đầy đủ thay vì chỉ có chữ ký.

### Cập nhật theo thời gian thực mà không cần thăm dò

`transactionSubscribe` gửi một thông báo mỗi khi có giao dịch liên quan đến ví, vì vậy bạn không bao giờ phải thăm dò. Việc thêm `tokenAccounts: "balanceChanged"` vào bộ lọc cũng sẽ khớp với các tài khoản token của ví, nhờ đó các giao dịch chuyển SPL đến sẽ xuất hiện trong nguồn cấp dữ liệu — chính là hoạt động được tính trong lệnh gọi lịch sử. Chuyển tiếp đăng ký thông qua trình xử lý tuyến phía máy chủ giúp khóa API không bị gửi đến máy khách.

## Các bước tiếp theo

<CardGroup cols={2}>
  <Card title="getAssetsByOwner" icon="images" href="/docs/vi/api-reference/das/getassetsbyowner">
    Tất cả tùy chọn hiển thị, phân trang và cấu trúc phản hồi tài sản đầy đủ.
  </Card>

  <Card title="getTransactionsForAddress" icon="clock-rotate-left" href="/docs/vi/rpc/gettransactionsforaddress">
    Bộ lọc nâng cao theo thời gian, slot, token, hướng và số lượng.
  </Card>

  <Card title="transactionSubscribe" icon="bolt" href="/docs/vi/rpc/websocket/transaction-subscribe">
    Các tùy chọn bộ lọc và cấu trúc payload cho nguồn cấp WebSocket trực tiếp.
  </Card>

  <Card title="Deploy your own program" icon="rocket" href="/docs/vi/quickstart/deploy-program">
    Hướng tiếp theo: triển khai một chương trình Solana lên devnet thông qua Helius.
  </Card>
</CardGroup>
