# Bắt đầu với RPC Robinhood Chain Testnet: đọc không cần key, log WebSocket, rồi mainnet

> Source: https://docs.blockvectra.com/vi/guides/robinhood-testnet-starter/

BlockVectra phục vụ Robinhood Chain Testnet (Chain ID 46630) qua JSON-RPC và WebSocket, và cùng API key cũng dùng được trên Robinhood Chain mainnet. Hướng dẫn này đi theo mẫu [Robinhood Chain Testnet starter](https://github.com/blockvectra/robinhood-testnet-starter?ref=docs-testnet-starter) qua ba bước: đọc testnet không cần key, mở tài khoản bằng chương trình và nhận luồng log, rồi chuyển cùng key sang mainnet. Để biết tham số endpoint, chính sách phương thức và dữ liệu cổ phiếu token hóa, xem [hướng dẫn Robinhood Chain](https://docs.blockvectra.com/vi/guides/robinhood-chain/).

## Chạy mẫu

```bash
git clone https://github.com/blockvectra/robinhood-testnet-starter
cd robinhood-testnet-starter
npm install
npm run typecheck
```

Node.js 18 trở lên. Ba script là `npm run step1`, `npm run step2` và `npm run step3`. Biến tùy chọn (`BLOCKVECTRA_API_KEY`, `WALLET_PRIVATE_KEY`) được đọc từ shell; không có gì tự động nạp `.env`.

## Bước 1: đọc testnet không cần key

Endpoint công khai `https://api.blockvectra.com/v1/robinhood_testnet/public` phục vụ phương thức JSON-RPC cho ví mà không cần tài khoản hay API key. Truy vấn trực tiếp Chain ID EIP-155:

```bash
curl -s "https://api.blockvectra.com/v1/robinhood_testnet/public" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
```

Phản hồi là `{"jsonrpc":"2.0","id":1,"result":"0xb626"}`; `0xb626` là Chain ID thập lục phân, tương đương 46630 trong hệ thập phân.

Mẫu đọc mục chuỗi từ `GET /v1/chains` và tạo viem client từ đó, nên tên chuỗi và ID không được viết cứng. Từ `src/shared.ts`:

```ts
import { createPublicClient, defineChain, http, webSocket } from "viem";

export const API_BASE = "https://api.blockvectra.com/v1";

export interface ChainEntry {
  chain: string;
  name: string;
  chain_id: number;
  data: boolean;
  ws: boolean;
  subscriptions: string[];
  public?: { url: string };
}

export async function getChain(slug: string): Promise<ChainEntry> {
  const res = await fetch(`${API_BASE}/chains`);
  if (!res.ok) throw new Error(`GET /v1/chains failed: ${res.status}`);
  const { chains } = (await res.json()) as { chains: ChainEntry[] };
  const entry = chains.find((c) => c.chain === slug);
  if (!entry) throw new Error(`${slug} not found in /v1/chains`);
  return entry;
}

export function toViemChain(entry: ChainEntry, rpcUrl: string) {
  return defineChain({
    id: entry.chain_id,
    name: entry.name,
    nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
    rpcUrls: { default: { http: [rpcUrl] } },
  });
}

export function httpClient(entry: ChainEntry, url: string, apiKey?: string) {
  return createPublicClient({
    chain: toViemChain(entry, url),
    transport: http(url, apiKey ? { fetchOptions: { headers: { "x-api-key": apiKey } } } : undefined),
  });
}

export function wsClient(entry: ChainEntry, apiKey: string) {
  const base = "wss://api.blockvectra.com/v1";
  const url = `${base}/${entry.chain}/${apiKey}`;
  return createPublicClient({ chain: toViemChain(entry, url), transport: webSocket(url) });
}
```

Với mô-đun đó, `src/step1-public-read.ts` đọc Chain ID, khối mới nhất và số dư địa chỉ:

```ts
import { formatEther, type Address } from "viem";
import { API_BASE, getChain, httpClient } from "./shared.js";

const SLUG = "robinhood_testnet";
// Any address works; override with the first CLI argument.
const address = (process.argv[2] ?? "0x0000000000000000000000000000000000000000") as Address;

const chain = await getChain(SLUG);
const client = httpClient(chain, `${API_BASE}/${SLUG}/public`);

const [chainId, blockNumber, balance] = await Promise.all([
  client.getChainId(),
  client.getBlockNumber(),
  client.getBalance({ address }),
]);

console.log(`chain       : ${chain.name} (${SLUG})`);
console.log(`eth_chainId : ${chainId} (0x${chainId.toString(16)})`);
console.log(`block       : ${blockNumber}`);
console.log(`balance     : ${formatEther(balance)} ETH  (${address})`);
```

## Bước 2: mở tài khoản bằng chương trình và đăng ký log

`src/step2-key-and-logs.ts` đăng nhập bằng ví (SIWE, EIP-191) tại `https://console-api.blockvectra.com/v1`. Lần đăng nhập đầu tiên tạo tài khoản; client sau đó tạo API key. Tài khoản mới nhận 30,000,000 CU khi đăng ký — không cần thẻ tín dụng.

```ts
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import type { Hex } from "viem";

const CONSOLE_API = "https://console-api.blockvectra.com/v1";
const REF = "gh-robinhood-testnet-starter";

async function post<T>(path: string, body: unknown, token?: string): Promise<T> {
  const res = await fetch(`${CONSOLE_API}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
  return (await res.json()) as T;
}

async function provisionKey(): Promise<string> {
  const pk = (process.env.WALLET_PRIVATE_KEY as Hex | undefined) ?? generatePrivateKey();
  const account = privateKeyToAccount(pk);
  console.log(`wallet: ${account.address}`);

  // SIWE: ask for a challenge, sign it verbatim (EIP-191), log in. A first sign-in creates the account.
  const { message } = await post<{ message: string }>("/auth/siwe/challenge", {
    address: account.address,
    purpose: "login",
  });
  const signature = await account.signMessage({ message });
  const login = await post<{ session: { token: string }; account_created: boolean }>("/auth/siwe/login", {
    message,
    signature,
    ref: REF,
  });
  console.log(`account_created: ${login.account_created}`);

  const created = await post<{ api_key: string }>("/keys", { label: "robinhood-testnet-starter" }, login.session.token);
  return created.api_key;
}

const apiKey = process.env.BLOCKVECTRA_API_KEY?.trim() || (await provisionKey());
```

Nếu đã có key, `export BLOCKVECTRA_API_KEY=...` bỏ qua đăng ký. Khi không có `WALLET_PRIVATE_KEY`, script tạo ví tạm trong bộ nhớ và bỏ khi kết thúc chạy; đặt `WALLET_PRIVATE_KEY` từ môi trường cục bộ nếu muốn đăng nhập lại cùng tài khoản.

URL WebSocket đặt key trong đường dẫn: `wss://api.blockvectra.com/v1/robinhood_testnet/{api_key}`. Mẫu kiểm tra cờ `ws` hiện tại và danh sách `subscriptions` từ `GET /v1/chains` trước khi đăng ký:

```ts
const chain = await getChain("robinhood_testnet");
if (!chain.ws || !chain.subscriptions.includes("logs")) {
  throw new Error(`robinhood_testnet does not offer a logs subscription in /v1/chains`);
}
console.log(`subscriptions (from /v1/chains): ${chain.subscriptions.join(", ")}`);

const client = wsClient(chain, apiKey);
const stop = client.watchEvent({
  address: "0x1234567890123456789012345678901234567890",
  onLogs: (logs) => {
    for (const l of logs) console.log(`block ${l.blockNumber} ${l.address} topic0=${l.topics[0]}`);
  },
  onError: (e) => console.error("subscription error:", e.message),
});
```

Mỗi bộ lọc đăng ký `logs` phải có `address` hoặc `topic0`; thay địa chỉ ví dụ bằng hợp đồng muốn theo dõi. Xem [hướng dẫn đăng ký WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) để biết quy tắc lọc, mã đóng kết nối và kết nối lại.

## Bước 3: chuyển cùng key sang mainnet

Chuyển sang mainnet chỉ cần đổi một từ: dùng slug `robinhood_mainnet` và cùng API key. `src/step3-mainnet.ts` đọc số khối mainnet và gọi Data API cho token cổ phiếu Robinhood:

```ts
const apiKey = process.env.BLOCKVECTRA_API_KEY?.trim();
if (!apiKey) throw new Error("Set BLOCKVECTRA_API_KEY (run step 2 first, or create a key in the console).");

const mainnet = await getChain("robinhood_mainnet");

// JSON-RPC with the key: the chain slug changes.
const client = httpClient(mainnet, `${API_BASE}/${mainnet.chain}`, apiKey);
console.log(`mainnet block: ${await client.getBlockNumber()}`);

// Data API: Robinhood stock-token leaderboard (see the Robinhood Chain guide).
const res = await fetch(`${API_BASE}/data/${mainnet.chain}/stocks`, { headers: { "x-api-key": apiKey } });
if (!res.ok) throw new Error(`Data API failed: ${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2).slice(0, 2000));
```

Cùng endpoint bảng xếp hạng trong curl:

```bash
curl -s -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  "https://api.blockvectra.com/v1/data/robinhood_mainnet/stocks"
```

Data API chỉ có trên mainnet: `GET /v1/chains` báo `data: false` cho `robinhood_testnet` và `data: true` cho `robinhood_mainnet`. Để biết tham số yêu cầu và cấu trúc phản hồi, xem [hướng dẫn cổ phiếu token hóa](https://docs.blockvectra.com/vi/guides/stocks/).

## Khám phá giới hạn và khả năng từ API

Đọc `GET /v1/chains` để biết các cờ `jsonrpc`, `data`, `ws` và `subscriptions` hiện tại, phương thức JSON-RPC được phép, khoảng khối log, thời gian lưu trạng thái và endpoint công khai của từng chuỗi. Đọc `GET /v1/plans` để biết giá và giới hạn gói hiện tại.

```bash
curl -s "https://api.blockvectra.com/v1/chains"
```

## Hướng dẫn liên quan

* [Tìm URL RPC testnet và giới hạn](https://blockvectra.com/vi/chains/robinhood_testnet/).
* [Nhận ETH thử nghiệm từ faucet testnet](https://docs.blockvectra.com/vi/guides/robinhood-testnet-faucet/) trước khi gửi giao dịch.

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

* [Duyệt danh mục bộ dữ liệu](https://blockvectra.com/vi/data/) để xem mọi bộ dữ liệu BlockVectra lập chỉ mục.
* [Xem gói miễn phí và giá](https://blockvectra.com/vi/pricing/#free) để kiểm tra những gì tài khoản bao gồm.
* [Đăng nhập bảng điều khiển](https://console.blockvectra.com/login/?next=%2Fkeys%2F) để tạo API key.
