如何读取Robinhood链上的股票代币数据
本文是一份技术教程,指导如何使用Quicknode RPC和viem库读取Robinhood Chain上的Stock Token(如TSLA、AAPL等)数据。通过一个TypeScript脚本,实现四个功能:批量查询代币元数据、总供应量、钱包余额和转账事件历史。文章详细介绍了项目设置、RPC客户端配置、multicall批量调用、事件解码等步骤,并提供了完整的代码示例和预期输出。
概述
Robinhood Chain 是一个无需许可、兼容 EVM 的 Arbitrum Orbit Layer-2,构建在以太坊之上,使用 ETH 作为原生 Gas 代币。股票代币(Stock Tokens)是代指个股和 ETF 的 ERC-20 代币,可 24/7 全天候使用,可与其他智能合约组合,并通过标准以太坊工具和 RPC 端点访问。
在本指南中,你将使用 Quicknode RPC 和 viem 直接从 Robinhood Chain 读取股票代币数据。你将构建一个包含四个函数的 TypeScript 脚本,用于检索 TSLA、AAPL、NVDA 和 AMZN 等股票代币的元数据、总供应量、钱包余额以及 Transfer 事件历史。
你将完成的任务
- 在单个 multicall 请求中读取每个股票代币的
name、symbol和decimals - 获取
totalSupply()以查看每个股票代币在链上铸造了多少 - 查询
balanceOf(wallet)以检查所有跟踪代币的钱包余额 - 解码所选代币在可配置区块范围内的
Transfer事件历史
你需要准备
- 一个拥有 Robinhood Chain 主网端点的 Quicknode 账户
- 安装 Node.js v20.6 或更高版本以及 npm(用于通过
--env-file原生加载.env) - 基本熟悉 TypeScript 和 ERC-20 代币概念
TL;DR
- 股票代币(TSLA、AAPL、NVDA、AMZN 等)是 Robinhood Chain 上的 ERC-20 代币
- 标准 ERC-20 方法(
name、symbol、decimals、totalSupply、balanceOf)和Transfer事件可通过任何 Quicknode Robinhood Chain RPC 端点工作 - viem 的
multicall将所有读取批量合并到单个 RPC 调用中;formatUnits(supply, decimals)将原始 bigint 转换为完整代币数量 - Quicknode Streams 允许你实时获取更新到目标位置,无需轮询
项目设置
从命令行创建项目目录、安装依赖并配置 package.json:
mkdir robinhood-chain-reader && cd robinhood-chain-reader
npm init -y
npm install viem
npm install -D tsx typescript @types/node
npm pkg set type=module
tsx 可以直接运行 TypeScript,无需构建步骤,但编辑器的 TypeScript 语言服务仍然需要一个 tsconfig.json 来解析 @types/node(否则会标记 process 等全局变量为错误)并告知项目目标为 ESM。在项目根目录添加一个:
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["node"],
"strict": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}
--env-file=.env 标志(稍后使用)原生加载环境变量,因此无需 dotenv 依赖。
在项目根目录创建一个 .env 文件(切勿提交此文件):
.env
RPC_URL=YOUR_QUICKNODE_ROBINHOOD_ENDPOINT
## Optional: only needed to run the balance command without passing an address
WALLET_ADDRESS=0xYourWalletAddress
要获取你的 RPC_URL,请按照 Quicknode Robinhood 快速入门 创建一个 Robinhood Chain 端点。
配置脚本
创建 read-stocks.ts。文件顶部设置 viem 客户端,定义代币注册表,并声明所有四个函数共用的单一共享 ABI。
read-stocks.ts(设置部分)
import {
createPublicClient,
http,
defineChain,
parseAbi,
formatUnits,
getAddress,
Address,
} from 'viem';
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error('RPC_URL is not set in .env');
const { result: chainIdHex } = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }),
}).then((res) => res.json());
const client = createPublicClient({
chain: defineChain({
id: parseInt(chainIdHex, 16),
name: 'Robinhood Chain',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [RPC_URL] } },
contracts: {
multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' },
},
}),
transport: http(RPC_URL),
});
const TOKENS: Record<string, { name: string; address: Address }> = {
TSLA: { name: 'Tesla Inc.', address: '0x322F0929c4625eD5bAd873c95208D54E1c003b2d' },
AAPL: { name: 'Apple Inc.', address: '0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9' },
NVDA: { name: 'NVIDIA Corporation', address: '0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC' },
AMZN: { name: 'Amazon.com Inc.', address: '0x12f190a9F9d7D37a250758b26824B97CE941bF54' },
MSFT: { name: 'Microsoft Corp.', address: '0xe93237C50D904957Cf27E7B1133b510C669c2e74' },
GOOGL: { name: 'Alphabet Inc.', address: '0x2e0847E8910a9732eB3fb1bb4b70a580ADAD4FE3' },
META: { name: 'Meta Platforms', address: '0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35' },
MSTR: { name: 'MicroStrategy', address: '0xec262a75e413fAfD0dF80480274532C79D42da09' },
SPY: { name: 'SPDR S&P 500 ETF', address: '0x117cc2133c37B721F49dE2A7a74833232B3B4C0C' },
QCOM: { name: 'Qualcomm Inc.', address: '0x0f17206447090e464C277571124dD2688E48AEA9' },
};
const SYMBOLS = Object.keys(TOKENS);
const ABI = parseAbi([\
'function name() view returns (string)',\
'function symbol() view returns (string)',\
'function decimals() view returns (uint8)',\
'function totalSupply() view returns (uint256)',\
'function balanceOf(address) view returns (uint256)',\
'event Transfer(address indexed from, address indexed to, uint256 value)',\
]);
const TRANSFER_EVENT = ABI.find((x) => x.type === 'event' && x.name === 'Transfer')!;
const col = (s: string, w: number) => s.slice(0, w).padEnd(w);
const padCol = (s: string, w: number) => s.padEnd(w);
defineChain 使用从 RPC 端点通过 eth_chainId 获取的链 ID 向 viem 注册 Robinhood Chain,但如有需要,你也可以手动指定。contracts.multicall3 条目指向标准的 Multicall3 部署,Robinhood Chain 在标准地址上包含该部署;没有它,viem 的 multicall() 会抛出 ChainDoesNotSupportContract 错误。TOKENS 映射是所有四个操作的单一真实来源。一个单一的 ABI 数组涵盖所有函数和 Transfer 事件;TRANSFER_EVENT 提取类型化的事件条目,供 getLogs 使用。
主网和测试网地址
上面的 TOKENS 映射使用 Robinhood Chain 主网代币合约地址。Quicknode 同时支持 Robinhood Chain 主网和测试网。本指南默认使用主网;如果你想在 Robinhood Chain 测试网上测试,请更新你的 RPC_URL 并将代币合约地址替换为匹配的测试网部署。
读取股票数据
在链客户端、代币注册表和 ABI 就绪后,接下来添加 CLI 调度,以便下面的命令实际执行操作。它从 process.argv[2] 读取命令名称并调用匹配的函数。将其直接添加到设置代码下方;你将在接下来的章节中按顺序添加它调用的四个函数,这样每个命令在其函数存在后即可运行。
read-stocks.ts - CLI 调度
const cmd = process.argv[2];
const walletArg = process.argv[3] ?? process.env.WALLET_ADDRESS;
switch (cmd) {
case 'metadata':
await readMetadata();
break;
case 'supply':
await readSupply();
break;
case 'balance':
if (!walletArg) { console.error('Usage: npx tsx --env-file=.env read-stocks.ts balance <address>'); process.exit(1); }
await readBalances(getAddress(walletArg));
break;
case 'transfers':
await readTransfers(
process.env.TOKEN ?? 'TSLA',
parseInt(process.env.BLOCK_RANGE ?? '5000', 10),
);
break;
default:
await readMetadata();
await readSupply();
break;
}
以下每个 npx tsx --env-file=.env read-stocks.ts <command> 调用将命令名称作为 process.argv[2] 传递;运行未识别的命令(或无参数)将同时运行 metadata 和 supply,作为快速健全性检查。
读取代币元数据
readMetadata 在单个 multicall 请求中获取每个代币的 name()、symbol() 和 decimals()。
read-stocks.ts - readMetadata()
async function readMetadata() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [\
{ address, abi: ABI, functionName: 'name' as const },\
{ address, abi: ABI, functionName: 'symbol' as const },\
{ address, abi: ABI, functionName: 'decimals' as const },\
];
});
const results = await client.multicall({ contracts, allowFailure: true });
const rows = SYMBOLS.map((sym, i) => {
const base = i * 3;
return {
sym,
name: results[base].status === 'success' ? String(results[base].result) : '?',
symbol: results[base+1].status === 'success' ? String(results[base+1].result) : '?',
decimals: results[base+2].status === 'success' ? String(results[base+2].result) : '?',
};
});
const symWidth = Math.max('Sym'.length, ...rows.map((row) => row.sym.length));
const nameWidth = Math.max('Name (onchain)'.length, ...rows.map((row) => row.name.length));
const symbolWidth = Math.max('Symbol'.length, ...rows.map((row) => row.symbol.length));
const decimalsWidth = 'Decimals'.length;
const tableWidth = symWidth + nameWidth + symbolWidth + decimalsWidth + 6;
console.log('\n── Token Metadata ──');
console.log(padCol('Sym', symWidth), padCol('Name (onchain)', nameWidth), padCol('Symbol', symbolWidth), 'Decimals');
console.log('─'.repeat(tableWidth));
rows.forEach(({ sym, name, symbol, decimals }) => {
console.log(padCol(sym, symWidth), padCol(name, nameWidth), padCol(symbol, symbolWidth), decimals);
});
}
multicall 将所有调用(每个代币 3 个,10 只股票共 30 个)通过 Multicall3 合约批量合并到单个 RPC 请求中。allowFailure: true 意味着一个错误的地址不会中止批量操作;每个条目的错误会单独显示。
npx tsx --env-file=.env read-stocks.ts metadata
预期输出:
── Token Metadata ──
Sym Name (onchain) Symbol Decimals
───────────────────────────────────────────────────────
TSLA Tesla • Robinhood Token TSLA 18
AAPL Apple • Robinhood Token AAPL 18
NVDA NVIDIA • Robinhood Token NVDA 18
...
成功显示表格确认每个地址都是主网上的活跃 ERC-20。
读取总供应量
readSupply 获取每个代币的 totalSupply() 和 decimals(),然后将原始 bigint 转换为人类可读的代币数量。
read-stocks.ts - readSupply()
async function readSupply() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [\
{ address, abi: ABI, functionName: 'totalSupply' as const },\
{ address, abi: ABI, functionName: 'decimals' as const },\
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log('\n── Total Supply ──');
console.log(col('Sym', 6), col('Supply (formatted)', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const supplyR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (supplyR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const supply = supplyR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(supply, decimals), 22), supply.toString());
});
}
股票代币以其最小单位计价,类似于 wei。totalSupply 为 1000000000000000000000 且 decimals 为 18 时,等于 1,000 个完整代币。formatUnits(supply, decimals) 处理任何小数精度的转换。
npx tsx --env-file=.env read-stocks.ts supply
预期输出:
── Total Supply ──
Sym Supply (formatted) Raw
──────────────────────────────────────────────────
TSLA 1250000.0 1250000000000000000000000
AAPL 980000.5 980000500000000000000000
...
读取地址余额
readBalances 查询注册表中每个代币的 balanceOf(wallet)。将目标地址作为 CLI 参数传递,或在 .env 中设置 WALLET_ADDRESS。
read-stocks.ts - readBalances()
async function readBalances(wallet: Address) {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [\
{ address, abi: ABI, functionName: 'balanceOf' as const, args: [wallet] },\
{ address, abi: ABI, functionName: 'decimals' as const },\
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log(`\n── Balances for ${wallet} ──`);
console.log(col('Sym', 6), col('Balance', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const balR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (balR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const balance = balR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(balance, decimals), 22), balance.toString());
});
}
getAddress(在上述 CLI 调度中使用)在到达 RPC 之前对钱包参数进行校验和验证,及早捕获格式错误的地址。余额为零的代币也会被打印出来,以便输出涵盖所有跟踪的资产。
## Pass the wallet address as a CLI argument
npx tsx --env-file=.env read-stocks.ts balance 0xYourWalletAddress
## Or set WALLET_ADDRESS in .env and run without an argument
npx tsx --env-file=.env read-stocks.ts balance
预期输出:
── Balances for 0xYour... ──
Sym Balance Raw
──────────────────────────────────────────────────
TSLA 10.5 10500000000000000000
AAPL 0.0 0
NVDA 25.0 25000000000000000000
...
读取近期转账
readTransfers 获取所选代币在可配置的近期区块范围内的 Transfer 事件。
read-stocks.ts - readTransfers()
async function readTransfers(symbol: string, blockRange: number) {
const token = TOKENS[symbol];
if (!token) {
console.error(`Unknown token: ${symbol}. Available: ${SYMBOLS.join(', ')}`);
return;
}
const decimals = await client.readContract({
address: token.address,
abi: ABI,
functionName: 'decimals',
});
const latest = await client.getBlockNumber();
const fromBlock = latest - BigInt(blockRange) + 1n;
console.log(`\n── ${symbol} Transfers (blocks ${fromBlock}–${latest}) ──`);
const logs = await client.getLogs({
address: token.address,
event: TRANSFER_EVENT,
fromBlock,
toBlock: latest,
});
if (!logs.length) {
console.log('No transfers in this range. Try a larger BLOCK_RANGE.');
return;
}
console.log(col('Block', 12), col('From', 44), col('To', 44), 'Value');
console.log('─'.repeat(110));
for (const log of logs) {
const { from, to, value } = log.args as { from: string; to: string; value: bigint };
console.log(
col(log.blockNumber?.toString() ?? '-', 12),
col(from, 44),
col(to, 44),
formatUnits(value, decimals),
);
}
}
使用类型化事件 ABI 的 getLogs 自动从事件主题解码索引参数 from 和 to,以及从数据字段解码 value。你获得一个完全类型化的 log.args 对象,而不是原始十六进制字符串。TRANSFER_EVENT 是之前通过 .find() 提取的类型化 ABI 条目,因此不存在脆弱的索引查找。从零地址的转账是铸造;向零地址的转账是销毁。fromBlock 加 1n 是因为 eth_getLogs 将两个边界都视为包含,因此这使查询范围恰好为 blockRange 个区块。
Quicknode RPC 端点将对 eth_getLogs 的每个请求限制在 10,000 个区块范围内。如果你将 BLOCK_RANGE 提高到超过该值并遇到范围限制错误,请将查询拆分为更小的块并合并结果。
## Default: TSLA, last 5000 blocks
npx tsx --env-file=.env read-stocks.ts transfers
## Custom token and block range
TOKEN=AAPL BLOCK_RANGE=10000 npx tsx --env-file=.env read-stocks.ts transfers
预期输出:
── TSLA Transfers (blocks 15600823–15605823) ──
Block From To Value
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
15605823 0x3d3871053e98b95d0d2e88d3c2c7d4659c855e36 0x2dda8e6fbc63b4be0291c9fcfc62a8abc12cf16d 5.0
15605824 0x0000000000000000000000000000000000000000 0x3d3871053e98b95d0d2e88d3c2c7d4659c855e36 100.0
使用 Quicknode Streams 实时追踪转账
getLogs 方法按需读取历史数据。对于持续监控,Quicknode Streams 允许你订阅相同的 Transfer 事件过滤器,并在每个区块完成时自动将新事件推送到你的后端,无需轮询循环。使用代币合约地址配置一个过滤器(例如仅获取到特定地址的转账),Streams 会自动将结果投递到你的目的地(例如 webhook、Postgres 或 S3)。
完整 read-stocks.ts - 复制并粘贴以运行所有四个操作
read-stocks.ts
import {
createPublicClient,
http,
defineChain,
parseAbi,
formatUnits,
getAddress,
Address,
} from 'viem';
// ── Chain ─────────────────────────────────────────────────────────────────────
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error('RPC_URL is not set in .env');
const { result: chainIdHex } = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }),
}).then((res) => res.json());
const client = createPublicClient({
chain: defineChain({
id: parseInt(chainIdHex, 16),
name: 'Robinhood Chain',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [RPC_URL] } },
contracts: {
multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' },
},
}),
transport: http(RPC_URL),
});
// ── Token Registry ────────────────────────────────────────────────────────────
const TOKENS: Record<string, { name: string; address: Address }> = {
TSLA: { name: 'Tesla Inc.', address: '0x322F0929c4625eD5bAd873c95208D54E1c003b2d' },
AAPL: { name: 'Apple Inc.', address: '0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9' },
NVDA: { name: 'NVIDIA Corporation', address: '0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC' },
AMZN: { name: 'Amazon.com Inc.', address: '0x12f190a9F9d7D37a250758b26824B97CE941bF54' },
MSFT: { name: 'Microsoft Corp.', address: '0xe93237C50D904957Cf27E7B1133b510C669c2e74' },
GOOGL: { name: 'Alphabet Inc.', address: '0x2e0847E8910a9732eB3fb1bb4b70a580ADAD4FE3' },
META: { name: 'Meta Platforms', address: '0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35' },
MSTR: { name: 'MicroStrategy', address: '0xec262a75e413fAfD0dF80480274532C79D42da09' },
SPY: { name: 'SPDR S&P 500 ETF', address: '0x117cc2133c37B721F49dE2A7a74833232B3B4C0C' },
QCOM: { name: 'Qualcomm Inc.', address: '0x0f17206447090e464C277571124dD2688E48AEA9' },
};
const SYMBOLS = Object.keys(TOKENS);
// ── ABI ───────────────────────────────────────────────────────────────────────
const ABI = parseAbi([\
'function name() view returns (string)',\
'function symbol() view returns (string)',\
'function decimals() view returns (uint8)',\
'function totalSupply() view returns (uint256)',\
'function balanceOf(address) view returns (uint256)',\
'event Transfer(address indexed from, address indexed to, uint256 value)',\
]);
const TRANSFER_EVENT = ABI.find((x) => x.type === 'event' && x.name === 'Transfer')!;
const col = (s: string, w: number) => s.slice(0, w).padEnd(w);
const padCol = (s: string, w: number) => s.padEnd(w);
// ── 1. Read Metadata ──────────────────────────────────────────────────────────
async function readMetadata() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [\
{ address, abi: ABI, functionName: 'name' as const },\
{ address, abi: ABI, functionName: 'symbol' as const },\
{ address, abi: ABI, functionName: 'decimals' as const },\
];
});
const results = await client.multicall({ contracts, allowFailure: true });
const rows = SYMBOLS.map((sym, i) => {
const base = i * 3;
return {
sym,
name: results[base].status === 'success' ? String(results[base].result) : '?',
symbol: results[base+1].status === 'success' ? String(results[base+1].result) : '?',
decimals: results[base+2].status === 'success' ? String(results[base+2].result) : '?',
};
});
const symWidth = Math.max('Sym'.length, ...rows.map((row) => row.sym.length));
const nameWidth = Math.max('Name (onchain)'.length, ...rows.map((row) => row.name.length));
const symbolWidth = Math.max('Symbol'.length, ...rows.map((row) => row.symbol.length));
const decimalsWidth = 'Decimals'.length;
const tableWidth = symWidth + nameWidth + symbolWidth + decimalsWidth + 6;
console.log('\n── Token Metadata ──');
console.log(padCol('Sym', symWidth), padCol('Name (onchain)', nameWidth), padCol('Symbol', symbolWidth), 'Decimals');
console.log('─'.repeat(tableWidth));
rows.forEach(({ sym, name, symbol, decimals }) => {
console.log(padCol(sym, symWidth), padCol(name, nameWidth), padCol(symbol, symbolWidth), decimals);
});
}
// ── 2. Read Total Supply ──────────────────────────────────────────────────────
async function readSupply() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [\
{ address, abi: ABI, functionName: 'totalSupply' as const },\
{ address, abi: ABI, functionName: 'decimals' as const },\
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log('\n── Total Supply ──');
console.log(col('Sym', 6), col('Supply (formatted)', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const supplyR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (supplyR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const supply = supplyR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(supply, decimals), 22), supply.toString());
});
}
// ── 3. Read Balances ──────────────────────────────────────────────────────────
async function readBalances(wallet: Address) {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [\
{ address, abi: ABI, functionName: 'balanceOf' as const, args: [wallet] },\
{ address, abi: ABI, functionName: 'decimals' as const },\
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log(`\n── Balances for ${wallet} ──`);
console.log(col('Sym', 6), col('Balance', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const balR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (balR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const balance = balR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(balance, decimals), 22), balance.toString());
});
}
// ── 4. Read Transfers ─────────────────────────────────────────────────────────
async function readTransfers(symbol: string, blockRange: number) {
const token = TOKENS[symbol];
if (!token) {
console.error(`Unknown token: ${symbol}. Available: ${SYMBOLS.join(', ')}`);
return;
}
const decimals = await client.readContract({
address: token.address,
abi: ABI,
functionName: 'decimals',
});
const latest = await client.getBlockNumber();
const fromBlock = latest - BigInt(blockRange) + 1n;
console.log(`\n── ${symbol} Transfers (blocks ${fromBlock}–${latest}) ──`);
const logs = await client.getLogs({
address: token.address,
event: TRANSFER_EVENT,
fromBlock,
toBlock: latest,
});
if (!logs.length) {
console.log('No transfers in this range. Try a larger BLOCK_RANGE.');
return;
}
console.log(col('Block', 12), col('From', 44), col('To', 44), 'Value');
console.log('─'.repeat(110));
for (const log of logs) {
const { from, to, value } = log.args as { from: string; to: string; value: bigint };
console.log(
col(log.blockNumber?.toString() ?? '-', 12),
col(from, 44),
col(to, 44),
formatUnits(value, decimals),
);
}
}
// ── CLI Dispatch ──────────────────────────────────────────────────────────────
const cmd = process.argv[2];
const walletArg = process.argv[3] ?? process.env.WALLET_ADDRESS;
switch (cmd) {
case 'metadata':
await readMetadata();
break;
case 'supply':
await readSupply();
break;
case 'balance':
if (!walletArg) { console.error('Usage: npx tsx --env-file=.env read-stocks.ts balance <address>'); process.exit(1); }
await readBalances(getAddress(walletArg));
break;
case 'transfers':
await readTransfers(
process.env.TOKEN ?? 'TSLA',
parseInt(process.env.BLOCK_RANGE ?? '5000', 10),
);
break;
default:
await readMetadata();
await readSupply();
break;
}
超越 ERC-20
上述脚本涵盖了标准 ERC-20 接口,足以读取任何股票代币的余额、供应量和转账历史。然而,Robinhood 发行的股票代币可能包含超出标准接口暴露范围的额外合约逻辑。
需要注意的可能模式:
- 转账限制:对发送方和接收方地址进行合规性检查(例如 KYC 或司法管辖区限制),这些限制阻止在 Robinhood 生态系统之外进行直接的 P2P 转账。
- 受控铸造和销毁:供应量变化可能需要授权铸造者角色,而不是任何调用者都可以进行。
- 可升级代理:Robinhood 发行的代币使用 OpenZeppelin 信标代理模式,这意味着所有代币合约共享一个可升级的实现。ERC-20 接口保持稳定,但底层逻辑可能发生变化。
ERC-20 接口为你提供完全读取权限:余额、供应量和事件历史都如本指南所示正常工作。要检查完整的合约实现,请在 Robinhood Chain 浏览器中查找每个代币地址,并在构建依赖合约内部逻辑的应用程序之前查看已验证的源代码。
结论
在本指南中,你构建了一个 TypeScript 脚本,通过 Quicknode RPC 和 viem 读取 Robinhood Chain 上任何股票代币的元数据、总供应量、钱包余额和转账历史。相同的模式适用于链上任何兼容 ERC-20 的代币:当更多股票代币在链上列出时,向 TOKENS 映射中添加更多条目即可。
下一步
- Robinhood Chain RPC API 参考:Quicknode 上支持的 JSON-RPC 方法完整列表
- Robinhood 端点安全:通过白名单、速率限制和 JWT 认证保护你的 Quicknode 端点
- Quicknode Streams:实时区块链数据投递到 Postgres、S3、Snowflake、BigQuery 或 webhook
- 端点安全最佳实践:生产端点的密钥轮换、速率限制和域掩码
- 什么是 ERC-3643?:EVM 链上合规代币化真实世界资产的代币标准,提供更广泛的 RWA 背景
常见问题
Robinhood Chain 上的股票代币使用什么代币标准?
股票代币以 ERC-20 代币形式发行。标准 ERC-20 读取方法(name、symbol、decimals、totalSupply、balanceOf、getLogs for Transfer)都适用于本指南中所示的合约地址。合约可能包含超出基本接口的额外合规性和访问控制逻辑。
在使用主网之前,是否有测试网可以用于测试?
是的。Robinhood Chain 的测试网公开可用,链 ID 为 46630。网络配置详情可在 https://docs.robinhood.com/chain/connecting 获取(测试网代币地址与主网不同,且并非所有股票代币都部署在测试网上)。
Robinhood Chain 上的 Gas 代币是什么?
ETH 是 Robinhood Chain 的原生 Gas 代币,与其 Arbitrum Orbit L2 架构一致。所有交易均以 ETH 支付。
如何实时追踪代币转账?
使用 Quicknode Streams 订阅任何代币合约的 Transfer 事件。配置一个流过滤器,匹配 Robinhood Chain 上的 Transfer 事件主题。Streams 会在每个区块到达时自动将解码后的事件推送到你选择的目的地(webhook、Postgres、S3),无需轮询 getLogs。完整设置详情请访问 https://www.quicknode.com/docs/streams
在哪里可以找到 Robinhood Chain 上股票代币的合约地址?
本指南在 TOKENS 映射中包含了 TSLA、AAPL、NVDA、AMZN、MSFT、GOOGL、META、MSTR、SPY 和 QCOM 示例的主网合约地址。对于其他资产或测试网使用,请在使用之前在生产代码中通过 Robinhood Chain 浏览器或官方 Robinhood Chain 资源确认正确的合约地址。
我们 ❤️ 反馈!
如果你有任何反馈或对新主题的建议,请告诉我们。我们期待你的意见。
- 原文链接: quicknode.com/guides/rob...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~