読了時間:20分
概要
Robinhood Chainは、イーサリアム上に構築された、許可不要でEVM互換のArbitrum Orbitレイヤー2であり、ネイティブのガストークンとしてETHを採用しています。個別銘柄やETFにちなんで名付けられたERC-20トークンである「ストックトークン」は、24時間365日利用可能で、他のスマートコントラクトと組み合わせることができ、標準的なイーサリアムツールやRPCエンドポイントを通じてアクセスできます。
このガイドでは、Quicknode RPC を使用して、 viem Robinhood Chainから株式トークンのデータを直接読み取るためです。トークンのメタデータ、総供給量、ウォレットの残高を取得するための4つの関数を含む、1つのTypeScriptスクリプトを作成します。 移籍 TSLA、AAPL、NVDA、AMZNなどの株式トークンのイベント履歴。
主な業務内容
- 以下をお読みください
名前,記号、および小数単一のマルチコールリクエストに含まれる各ストックトークンについて - フェッチ
totalSupply()各ストックトークンがオンチェーンでどれだけの量が発行されているかを確認するには - クエリ
balanceOf(wallet)追跡対象のすべてのトークンのウォレット残高を確認するには - デコード
移籍設定可能なブロック範囲における、指定されたトークンのイベント履歴
必要なもの
- Robinhood Chainのメインネットエンドポイントを備えたQuicknodeアカウント
- Node.js v20.6 以降および npm(ネイティブ用
.env読み込み中(経由)--env-file) - TypeScript および ERC-20 トークンの概念について基本的な知識があること
- 株式トークン(TSLA、AAPL、NVDA、AMZNなど)は、Robinhood Chain上のERC-20トークンです
- 標準的なERC-20の手法(
名前,記号,小数,総供給量,balanceOf) および移籍Quicknode Robinhood Chainの任意のRPCエンドポイントを経由したイベント処理 - viem's
マルチコールすべての読み取りを1つの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=モジュール
tsx ビルド工程を経ずにTypeScriptを直接実行しますが、お使いのエディタのTypeScript言語サーバーには依然として tsconfig.json 解決するために @types/node (そうでないと、次のようなグローバル変数がフラグとして検出されてしまいます: プロセス (エラーとして)表示され、プロジェクトがESMを対象としていることを確認するためです。プロジェクトのルートに1つ追加してください:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["node"],
"strict": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}
その --env-file=.env flag(以下で使用)は環境変数をネイティブに読み込むため、 dotenv 依存関係。
[作成] .env プロジェクトのルートディレクトリにあるファイル(このファイルは絶対にコミットしないでください):
RPC_URL=YOUR_QUICKNODE_ROBINHOOD_ENDPOINT
# オプション:アドレスを指定せずに残高コマンドを実行する場合にのみ必要
ウォレットアドレス=0xYourWalletAddress
あなたの RPC_URL, 以下の手順に従って、Robinhood Chainのエンドポイントを作成します。 Quicknode Robinhood クイックスタート.
スクリプトの設定
作成 read-stocks.ts。このファイルの冒頭では、viemクライアントの設定、トークンレジストリの定義、および4つの関数すべてで使用される単一の共有ABIの宣言が行われています。
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エンドポイントから直接取得したチェーンIDを使用して、Robinhood ChainをViemに登録します。 eth_chainIdですが、必要に応じて手動で指定することもできます。この contracts.multicall3 正規形へのエントリポイント Multicall3 デプロイメント。これはRobinhood Chainが標準アドレスに含めているもので、これがなければ、viemの multicall() をスローする ChainDoesNotSupportContract エラー。その トークン mapは、これら4つの操作すべてにおける唯一の信頼できる情報源です。単一の ABI この配列は、すべての関数と「Transfer」イベントを網羅しています。 TRANSFER_EVENT 指定されたイベントエントリを抽出し、以下で使用します getLogs.
その トークン 上記のマップでは、Robinhood Chainのメインネットのトークン契約アドレスを使用しています。Quicknodeは、Robinhood Chainのメインネットとテストネットの両方をサポートしています。このガイドではデフォルトでメインネットを使用しています。Robinhood Chainのテストネットでテストしたい場合は、設定を更新してください。 RPC_URL そして、トークン契約のアドレスを、対応するテストネット上のデプロイ先と置き換えてください。
株価データを閲覧する
チェーンクライアント、トークンレジストリ、ABIの準備が整ったところで、次にCLIディスパッチを追加し、以下のコマンドが実際に動作するようにします。これは、コマンド名を process.argv[2] そして、マッチング関数を呼び出します。これをセットアップコードの直下に追加してください。この関数が呼び出す4つの関数は、以降のセクションで順番に追加していくため、各関数が定義され次第、それぞれのコマンドを実行できるようになります。
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> 以下のcallは、コマンド名を process.argv[2]; 認識可能なコマンド(または引数)を指定せずに実行すると、メタデータとsupplyが同時に実行され、簡単な動作確認が行われます。
トークンのメタデータを読み取る
readMetadata フェッチ name(), symbol()、および decimals() 単一のマルチコールリクエストに含まれるすべてのトークンについて。
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);
});
}
マルチコール Multicall3 コントラクトを介して、すべての呼び出し(トークンごとに 3 件、10 銘柄で合計 30 件)を 1 つの RPC リクエストにまとめます。 allowFailure: true つまり、1つの不正なアドレスがあってもバッチ処理は中止されず、エラーは各エントリごとに表示されます。
npx tsx --env-file=.env read-stocks.ts メタデータ
期待される出力:
── トークンのメタデータ ──
シンボル 名称(オンチェーン) シンボル 小数点以下の桁数
───────────────────────────────────────────────────────
TSLA テスラ • ロビンフッド・トークン TSLA 18
AAPL Apple • Robinhood トークン AAPL 18
NVDA NVIDIA • ロビンフッド・トークン NVDA 18
...
テーブルが正常に処理されれば、各アドレスがメインネット上の有効なERC-20アドレスであることが確認されます。
総供給量を読む
readSupply フェッチ totalSupply() そして decimals() トークンごとに、生のbigintを人間が読みやすいトークン数量に変換します。
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と同様に、その最小単位で表示されます。A 総供給量 の 1000000000000000000000 小数点以下18桁の値は、1,000個のトークンに相当します。 formatUnits(supply, decimals) どのような小数点以下の桁数であっても、変換を処理します。
npx tsx --env-file=.env read-stocks.ts supply
期待される出力:
── 総供給量 ──
Sym 供給量(書式設定済み) 生データ
──────────────────────────────────────────────────
TSLA 1250000.0 1250000000000000000000000
AAPL 980000.5 980000500000000000000000
...
口座残高の読み取り
readBalances クエリ balanceOf(wallet) レジストリ内のすべてのトークンについて。ターゲットアドレスをCLI引数として指定するか、または WALLET_ADDRESS in .env.
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に到達する前にチェックサムを計算して検証を行い、形式不備のアドレスを早期に検出します。残高がゼロのトークンも出力されるため、追跡対象のすべての資産が出力に含まれます。
# ウォレットアドレスをCLI引数として渡す
npx tsx --env-file=.env read-stocks.ts balance 0xYourWalletAddress
# または、.env ファイルに WALLET_ADDRESS を設定し、引数なしで実行する
npx tsx --env-file=.env read-stocks.ts balance
期待される出力:
── 0xYour... の残高 ──
シンボル 残高 生データ
──────────────────────────────────────────────────
TSLA 10.5 10500000000000000000
AAPL 0.0 0
NVDA 25.0 25000000000000000000
...
最近の移籍情報を読む
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),
);
}
}
getLogs タイプ指定されたイベント ABI を使用して、インデックス付きデータをデコードします 出典: そして ~へ イベントのトピックに含まれるフィールドと、 値 データフィールドから自動的に取得されます。これにより、完全に型付けされた log.args 生の16進数文字列ではなく、オブジェクト。 TRANSFER_EVENT これは、先ほど次のコマンドで抽出された型付きABIエントリです。 .find(), したがって、脆弱なインデックス検索は発生しません。アドレス0からの送金は「ミント」、アドレス0への送金は「バーン」となります。 fromBlock 追加する 1n なぜなら eth_getLogs 両方の境界を包含境界として扱うため、これによりクエリされた範囲は正確に blockRange ブロック。
Quicknode RPC エンドポイントの上限 eth_getLogs リクエストあたり10,000ブロックの範囲で。もしこれを引き上げると BLOCK_RANGE それ以上になると範囲制限エラーが発生する場合は、クエリを小さなブロックに分割し、結果を結合してください。
# デフォルト: TSLA、直近5000ブロック
npx tsx --env-file=.env read-stocks.ts transfers
# カスタムトークンおよびブロック範囲
TOKEN=AAPL BLOCK_RANGE=10000 npx tsx --env-file=.env read-stocks.ts transfers
期待される出力:
── TSLAの送金(ブロック 15600823~15605823) ──
ブロック番号 差出人 受取人 金額
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
15605823 0x3d3871053e98b95d0d2e88d3c2c7d4659c855e36 0x2dda8e6fbc63b4be0291c9fcfc62a8abc12cf16d 5.0
15605824 0x0000000000000000000000000000000000000000 0x3d3871053e98b95d0d2e88d3c2c7d4659c855e36 100.0
その getLogs このアプローチでは、必要に応じて履歴データを読み込みます。継続的な監視を行うには、 Quicknode ストリーム これにより、この「Transfer」イベントフィルターを購読し、各ブロックの確定に合わせて新しいイベントをバックエンドにプッシュすることができ、ポーリングループは不要です。トークンコントラクトのアドレスを使用してフィルター(特定のアドレスへの送金のみを取得するなど)を設定すると、Streamsが自動的に宛先(Webhook、Postgres、S3など)にデータを配信します。
完了 read-stocks.ts - コピーして貼り付けると、4つの操作すべてが実行されます
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が発行する株式トークンには、標準インターフェースで公開されている範囲を超える追加のコントラクトロジックが含まれている場合があります。
注意すべき可能性のあるパターン:
- 送金制限:Robinhoodのエコシステム外での直接的なP2P送金を防止するための、送金元および受取先のアドレスに対するコンプライアンスチェック(例:KYCや管轄区域に基づく制限など)
- 管理された発行と焼却:供給量の変更には、任意の呼び出し元が実行できるのではなく、承認された発行者ロールが必要となる場合がある
- アップグレード可能なプロキシ:Robinhoodが発行するトークンは、OpenZeppelinのビーコンプロキシパターンを採用しています。つまり、すべてのトークン契約が単一のアップグレード可能な実装を共有しています。ERC-20インターフェースは安定していますが、その基盤となるロジックは変更される可能性があります
ERC-20 インターフェースでは、完全な読み取りアクセス権が与えられます。残高、供給量、イベント履歴はすべて、このガイドに示されているとおりに機能します。コントラクトの実装内容を詳細に確認するには、Robinhood Chain エクスプローラーで各トークンのアドレスを検索し、検証済みのソースコードを確認してから、コントラクトの内部構造に依存するアプリケーションを構築してください。
結論
このガイドでは、Quicknode RPC と viem を通じて、Robinhood Chain 上の任意の Stock Token のメタデータ、総供給量、ウォレット残高、および転送履歴を読み取る単一の TypeScript スクリプトを作成しました。このパターンは、同チェーン上の ERC-20 互換トークンにも同様に適用できます。つまり、 トークン オンチェーンで追加のストックトークンが上場されるにつれて、マップを更新します。
今後の手順
- Robinhood Chain RPC API リファレンス:Quicknode でサポートされている JSON-RPC メソッドの完全な一覧
- Robinhood エンドポイントセキュリティ:許可リスト、レート制限、JWT 認証を活用して Quicknode エンドポイントを保護しましょう
- Quicknode Streams:Postgres、S3、Snowflake、BigQuery、またはWebhookへのブロックチェーンデータのリアルタイム配信
- エンドポイントセキュリティのベストプラクティス:本番環境のエンドポイントにおける鍵のローテーション、レート制限、ドメインマスキング
- ERC-3643とは?:EVMチェーン上で準拠したトークン化された実世界資産(RWA)のためのトークン規格。RWAのより広範な文脈において
よくある質問
Robinhood Chain上のストックトークンは、どのトークン規格を採用していますか?
Stock TokensはERC-20トークンとして発行されます。標準的なERC-20のメソッド(name、symbol、decimals、totalSupply、balanceOf、getLogs for Transfer)はすべて、このガイドに示されている契約アドレスで動作します。契約には、基本インターフェースに加えて、コンプライアンスやアクセス制御に関する追加のロジックが含まれている場合があります。
メインネットを利用する前に、テスト用のテストネットはありますか?
はい。Robinhood Chainのテストネットは、チェーンID 46630で一般公開されています。ネットワーク設定の詳細については、https://docs.robinhood.com/chain/connecting をご覧ください(テストネットのトークンアドレスはメインネットとは異なり、すべてのストックトークンがテストネットで展開できるとは限りません)。
Robinhood Chainのガストークンとは何ですか?
ETHは、Arbitrum Orbit L2アーキテクチャに準拠したRobinhood Chainのネイティブガストークンです。すべての取引は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の公式リソースで正しいコントラクトアドレスを確認してください。
皆様からのフィードバックを心よりお待ちしております!❤️
ご意見や新しいトピックに関するご要望などがありましたら、ぜひお知らせください。皆様からのご連絡を心よりお待ちしております。
