> ## 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.

# 在 Solana 上构建投资组合追踪器

> 构建一个 Solana 投资组合追踪器，显示任何钱包的完整持仓以及其实时和历史链上活动。

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 || 'RPC error');
    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) ? 'This wallet holds too many assets to load in the demo. Try a wallet with fewer tokens and NFTs, or lower the limit in getAssetsByOwner.' : msg || 'Failed to load wallet');
    } finally {
      setLoading(false);
    }
  };
  const startLive = () => {
    if (!apiKey) {
      setError('Enter your API key first');
      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: 0
        }]
      }));
    };
    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">Your API Key</label>
          <input type="text" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="Enter your API key" 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">Wallet Address</label>
          <input type="text" value={ownerAddress} onChange={e => setOwnerAddress(e.target.value)} placeholder="Solana wallet address" 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 ? 'Loading...' : 'Load Wallet'}
          </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">Stop live feed</button> : <button type="button" onClick={startLive} className="px-4 py-2 font-medium rounded-full border border-zinc-300 dark:border-zinc-700">Go live</button>}
        </div>
      </form>
      <p className="mt-3 text-xs text-zinc-500 dark:text-zinc-400">
        Your API key is used only in your browser to call Helius directly. It is never sent anywhere else.
      </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>Error:</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">NFTs</div>
            </div>
          </div>
          {portfolio.tokens.length > 0 ? <div>
              <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white">Tokens</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 || 'Unknown token';
    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">Recent activity</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">
            Live feed
            <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">Waiting for the next transaction that touches this wallet.</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>;
};

在本指南中，您将构建一个仪表板，显示任何 Solana 地址的持有情况和当前活动。您将通过单次 [DAS](/zh/api-reference/das/getassetsbyowner) 调用提取其完整的代币、NFT 和 SOL 投资组合，使用 Helius 独有的 [`getTransactionsForAddress`](/zh/rpc/gettransactionsforaddress) 加载交易历史，并通过 [`transactionSubscribe`](/zh/rpc/websocket/transaction-subscribe) 实时流式传输新活动。您将首先以嵌入式演示形式尝试，然后将其构建为完整的 Next.js 应用程序。

## 实时尝试

粘贴您的 API 密钥和任何钱包地址，以查看投资组合、历史记录和实时供稿三部分如何协同工作。这是您的应用程序将渲染的相同数据。

<PortfolioTrackerDemo />

***

## 将其构建为 Next.js 应用程序

<Card title="在 GitHub 上获取完整应用程序" icon="github" href="https://github.com/helius-labs/quickstart-portfolio-tracker">
  克隆完整的入门应用程序 — 本次演练的完成版 Next.js 应用程序，已准备好 `npm install && npm run dev`。
</Card>

上述小部件直接从浏览器调用 Helius，从而暴露了您的 API 密钥。在真实应用中，您需将密钥保存在服务器上。本次演练使用 Next.js 应用路由及两个路由处理器，因此 **您的密钥不会到达客户端**：一个代理 RPC 调用，另一个将 WebSocket 供稿作为服务端事件传递。

<Info>
  **先决条件：** Node.js 18+ 和一个 [Helius API 密钥](https://dashboard.helius.dev/api-keys)。
</Info>

<Steps>
  <Step title="搭建项目">
    ```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
    ```

    您将使用 [Helius SDK](https://github.com/helius-labs/helius-sdk) 进行 RPC 调用，并使用 `ws` 进行实时 WebSocket 供稿。

    将您的密钥添加到 `.env.local`：

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

  <Step title="通过路由处理器代理 RPC 调用">
    这会将您的 API 密钥保留在服务器端。客户端在此处发布一个方法和参数，处理器通过 Helius SDK 运行它们，该 SDK 会附加您的密钥并为您构建请求。

    ```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="通过服务端事件传递实时供稿">
    浏览器无法在没有密钥的情况下打开 Helius WebSocket。相反，在服务器上打开 WebSocket，并通过 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: 0,
                  },
                ],
              })
            );
          });

          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="渲染仪表板">
    页面通过代理加载投资组合和历史记录，然后订阅 SSE 供稿以获取实时更新。

    ```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="运行">
    ```bash theme={"system"}
    npm run dev
    ```

    打开 [http://localhost:3000](http://localhost:3000)，粘贴一个钱包地址，然后点击 **Load**。您会立即看到投资组合和历史记录，并且实时供稿将随着新交易触及钱包而完成。
  </Step>
</Steps>

## 发生了什么

### 一个 DAS 调用即可获取整个投资组合

`getAssetsByOwner` 与 `showFungible: true` 和 `showNativeBalance: true` 会在单个响应中返回可替代代币、NFT（标准和压缩）以及原生 SOL 余额，因此您不需要单独的 `getBalance` 或 `getTokenAccountsByOwner` 调用。通过 `items` 按 `interface` 分割以对代币和 NFT 进行分组。

每个项目都带有自己的元数据，因此同一调用可以为两个列表提供支持。使用 `content.metadata.name` 获取名称，使用 `content.links.image` 获取标识或艺术作品。可替代代币还包括一个 `token_info` 对象，内含原始 `balance`，`decimals` 和 `symbol` — 将 `balance` 除以 `10 ** decimals` 以获得易于理解的金额。并非每种代币都会附带名称或标识，因此在缺失时请优雅地降级处理。

### 一次请求完成完整历史

`getTransactionsForAddress` 返回钱包的交易历史，通过 `filters.tokenAccounts: "balanceChanged"` 它还包括钱包的关联代币账户上的转账，这是普通 `getSignaturesForAddress` 所遗漏的活动。使用 `transactionDetails: "full"` 获取完整的交易数据而不仅仅是签名。

### 无需轮询的实时更新

每当交易触及钱包时，`transactionSubscribe` 会推送一条消息，因此您无需轮询。在筛选器中添加 `tokenAccounts: "balanceChanged"` 还可以匹配钱包的代币账户，因此传入的 SPL 转账会出现在供稿中 — 正是历史调用计算的活动。通过服务器端路由处理器中继订阅可防止您的 API 密钥泄露到客户端。

## 下一步

<CardGroup cols={2}>
  <Card title="getAssetsByOwner" icon="images" href="/zh/api-reference/das/getassetsbyowner">
    所有显示选项、分页和完整资产响应格式。
  </Card>

  <Card title="getTransactionsForAddress" icon="clock-rotate-left" href="/zh/rpc/gettransactionsforaddress">
    按时间、槽位、代币、方向和金额的高级过滤。
  </Card>

  <Card title="transactionSubscribe" icon="bolt" href="/zh/rpc/websocket/transaction-subscribe">
    实时 WebSocket 供稿的过滤选项和有效负载格式。
  </Card>

  <Card title="部署您自己的程序" icon="rocket" href="/zh/quickstart/deploy-program">
    下一步：通过 Helius 将 Solana 程序部署到 devnet。
  </Card>
</CardGroup>
