data
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStellar Data: RPC + Horizon
Stellar数据:RPC + Horizon
API access for reading chain state. Stellar RPC is the preferred entry point for new projects; Horizon remains for legacy and historical-query workflows. For deeper history beyond RPC's 7-day window, use Hubble/Galexie.
用于读取链上状态的API访问方式。Stellar RPC是新项目的首选入口;Horizon仍适用于旧版及历史查询工作流。若需获取RPC 7天窗口之外的深度历史数据,可使用Hubble/Galexie。
When to use this skill
何时使用此技能
- Calling Stellar RPC methods (,
getLatestLedger,getLedgerEntries,getEvents,simulateTransaction)sendTransaction - Querying Horizon endpoints (accounts, transactions, operations, effects, ledgers)
- Streaming live events or operations
- Pulling historical data beyond RPC's 7-day window (Hubble, Galexie)
- Choosing between RPC and Horizon for a given workflow
- 调用Stellar RPC方法(、
getLatestLedger、getLedgerEntries、getEvents、simulateTransaction)sendTransaction - 查询Horizon端点(账户、交易、操作、影响、账本)
- 流式获取实时事件或操作
- 获取RPC 7天窗口之外的历史数据(Hubble、Galexie)
- 为特定工作流选择RPC或Horizon
Related skills
相关技能
- Building transactions to send →
../dapp/SKILL.md - Soroban contract simulation and event emission →
../soroban/SKILL.md - Asset balance and trustline lookups →
../assets/SKILL.md - Standards (SEP-7 deeplinks, SEP-10 auth) →
../standards/SKILL.md
- 构建待发送交易 →
../dapp/SKILL.md - Soroban合约模拟与事件触发 →
../soroban/SKILL.md - 资产余额与信任线查询 →
../assets/SKILL.md - 标准协议(SEP-7深度链接、SEP-10认证) →
../standards/SKILL.md
Overview
概述
Stellar provides two API paradigms:
| API | Status | Use Case |
|---|---|---|
| Stellar RPC | Preferred | Soroban, real-time state, new projects |
| Horizon | Legacy-focused | Historical data, legacy applications |
Recommendation: Use Stellar RPC for all new projects. Use Horizon mainly for historical queries and legacy compatibility paths.
Stellar提供两种API范式:
| API | 状态 | 使用场景 |
|---|---|---|
| Stellar RPC | 推荐使用 | Soroban、实时状态查询、新项目 |
| Horizon | 聚焦旧版兼容 | 历史数据查询、旧版应用 |
建议:所有新项目均使用Stellar RPC。Horizon主要用于历史查询和旧版兼容场景。
Quick Navigation
快速导航
- RPC methods and usage: Stellar RPC
- Horizon endpoints and streaming: Horizon API (Legacy)
- Migration strategy: Migration: Horizon to RPC
- Data history/indexing options: Historical Data Access
- Environment setup and endpoints: Network Configuration
- RPC方法与使用:Stellar RPC
- Horizon端点与流处理:Horizon API(旧版)
- 迁移策略:迁移:从Horizon到RPC
- 数据历史/索引选项:历史数据访问
- 环境配置与端点:网络配置
Stellar RPC
Stellar RPC
Endpoints
端点
Note: SDF directly provides Futurenet public RPC. For Mainnet RPC, select a provider from the RPC providers directory.
| Network | RPC URL |
|---|---|
| Mainnet | Provider-specific endpoint (see RPC providers directory) |
| Testnet | |
| Futurenet | |
| Local | |
Setup
配置
typescript
import * as StellarSdk from "@stellar/stellar-sdk";
const rpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");typescript
import * as StellarSdk from "@stellar/stellar-sdk";
const rpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");Key Methods
核心方法
Get Account
获取账户信息
typescript
const account = await rpc.getAccount(publicKey);
// Returns account with sequence number for transaction buildingtypescript
const account = await rpc.getAccount(publicKey);
// 返回包含交易构建所需序列号的账户信息Get Health
检查服务健康状态
typescript
const health = await rpc.getHealth();
// { status: "healthy" }typescript
const health = await rpc.getHealth();
// { status: "healthy" }Get Latest Ledger
获取最新账本
typescript
const ledger = await rpc.getLatestLedger();
// { id: "...", sequence: 123456, protocolVersion: 25 }typescript
const ledger = await rpc.getLatestLedger();
// { id: "...", sequence: 123456, protocolVersion: 25 }Get Ledger Entries
获取账本条目
typescript
// Read contract storage
const key = StellarSdk.xdr.LedgerKey.contractData(
new StellarSdk.xdr.LedgerKeyContractData({
contract: new StellarSdk.Address(contractId).toScAddress(),
key: StellarSdk.xdr.ScVal.scvSymbol("Counter"),
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
})
);
const entries = await rpc.getLedgerEntries(key);
if (entries.entries.length > 0) {
const value = StellarSdk.scValToNative(
entries.entries[0].val.contractData().val()
);
}typescript
// 读取合约存储
const key = StellarSdk.xdr.LedgerKey.contractData(
new StellarSdk.xdr.LedgerKeyContractData({
contract: new StellarSdk.Address(contractId).toScAddress(),
key: StellarSdk.xdr.ScVal.scvSymbol("Counter"),
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
})
);
const entries = await rpc.getLedgerEntries(key);
if (entries.entries.length > 0) {
const value = StellarSdk.scValToNative(
entries.entries[0].val.contractData().val()
);
}Simulate Transaction
模拟交易
typescript
const simulation = await rpc.simulateTransaction(transaction);
if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
console.error("Simulation failed:", simulation.error);
} else if (StellarSdk.rpc.Api.isSimulationSuccess(simulation)) {
console.log("Cost:", simulation.cost);
console.log("Result:", simulation.result);
}typescript
const simulation = await rpc.simulateTransaction(transaction);
if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
console.error("模拟失败:", simulation.error);
} else if (StellarSdk.rpc.Api.isSimulationSuccess(simulation)) {
console.log("成本:", simulation.cost);
console.log("结果:", simulation.result);
}Send Transaction
发送交易
typescript
const response = await rpc.sendTransaction(signedTransaction);
if (response.status === "PENDING") {
// Poll for result
let result = await rpc.getTransaction(response.hash);
while (result.status === "NOT_FOUND") {
await new Promise(r => setTimeout(r, 1000));
result = await rpc.getTransaction(response.hash);
}
if (result.status === "SUCCESS") {
console.log("Success:", result.returnValue);
} else {
console.error("Failed:", result.status);
}
}typescript
const response = await rpc.sendTransaction(signedTransaction);
if (response.status === "PENDING") {
// 轮询获取结果
let result = await rpc.getTransaction(response.hash);
while (result.status === "NOT_FOUND") {
await new Promise(r => setTimeout(r, 1000));
result = await rpc.getTransaction(response.hash);
}
if (result.status === "SUCCESS") {
console.log("成功:", result.returnValue);
} else {
console.error("失败:", result.status);
}
}Get Transaction
获取交易信息
typescript
const tx = await rpc.getTransaction(txHash);
// status: "SUCCESS" | "FAILED" | "NOT_FOUND"
// returnValue: ScVal (for contract calls)
// ledger: numbertypescript
const tx = await rpc.getTransaction(txHash);
// status: "SUCCESS" | "FAILED" | "NOT_FOUND"
// returnValue: ScVal(合约调用场景)
// ledger: 数字Get Events
获取事件
typescript
const events = await rpc.getEvents({
startLedger: 1000000,
filters: [
{
type: "contract",
contractIds: [contractId],
topics: [
["*", StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64")],
],
},
],
});
for (const event of events.events) {
console.log("Event:", event.topic, event.value);
}typescript
const events = await rpc.getEvents({
startLedger: 1000000,
filters: [
{
type: "contract",
contractIds: [contractId],
topics: [
["*", StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64")],
],
},
],
});
for (const event of events.events) {
console.log("事件:", event.topic, event.value);
}RPC Limitations
RPC限制
- 7-day history for most methods: ,
getTransaction, etc. only cover recent datagetEvents - exception: "Infinite Scroll" feature queries any ledger back to genesis via the data lake
getLedgers - No streaming: Poll for updates (no WebSocket)
- Contract-focused: Limited classic Stellar data
- 多数方法仅支持7天历史:、
getTransaction等仅覆盖近期数据getEvents - 例外:"无限滚动"功能可通过数据湖查询从创世区块到当前的任意账本
getLedgers - 无原生流处理:需通过轮询获取更新(无WebSocket支持)
- 聚焦合约场景:对经典Stellar数据的支持有限
Horizon API (Legacy)
Horizon API(旧版)
Endpoints
端点
| Network | Horizon URL |
|---|---|
| Mainnet | |
| Testnet | |
| Local | |
| 网络 | Horizon URL |
|---|---|
| 主网 | |
| 测试网 | |
| 本地节点 | |
Setup
配置
typescript
import * as StellarSdk from "@stellar/stellar-sdk";
const server = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");typescript
import * as StellarSdk from "@stellar/stellar-sdk";
const server = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");Common Operations
常见操作
Load Account
加载账户信息
typescript
const account = await server.loadAccount(publicKey);
// Full account details including balances, signers, datatypescript
const account = await server.loadAccount(publicKey);
// 包含余额、签名者、数据等完整账户详情Get Account Balances
获取账户余额
typescript
const account = await server.loadAccount(publicKey);
for (const balance of account.balances) {
if (balance.asset_type === "native") {
console.log("XLM:", balance.balance);
} else {
console.log(`${balance.asset_code}:`, balance.balance);
}
}typescript
const account = await server.loadAccount(publicKey);
for (const balance of account.balances) {
if (balance.asset_type === "native") {
console.log("XLM:", balance.balance);
} else {
console.log(`${balance.asset_code}:`, balance.balance);
}
}Get Transactions
获取交易记录
typescript
// Account transactions
const transactions = await server
.transactions()
.forAccount(publicKey)
.order("desc")
.limit(10)
.call();
// Specific transaction
const tx = await server
.transactions()
.transaction(txHash)
.call();typescript
// 账户交易记录
const transactions = await server
.transactions()
.forAccount(publicKey)
.order("desc")
.limit(10)
.call();
// 特定交易详情
const tx = await server
.transactions()
.transaction(txHash)
.call();Get Operations
获取操作记录
typescript
const operations = await server
.operations()
.forAccount(publicKey)
.order("desc")
.limit(20)
.call();
for (const op of operations.records) {
console.log(op.type, op.created_at);
}typescript
const operations = await server
.operations()
.forAccount(publicKey)
.order("desc")
.limit(20)
.call();
for (const op of operations.records) {
console.log(op.type, op.created_at);
}Get Payments
获取支付记录
typescript
const payments = await server
.payments()
.forAccount(publicKey)
.order("desc")
.call();
for (const payment of payments.records) {
if (payment.type === "payment") {
console.log(
`${payment.from} -> ${payment.to}: ${payment.amount} ${payment.asset_code || "XLM"}`
);
}
}typescript
const payments = await server
.payments()
.forAccount(publicKey)
.order("desc")
.call();
for (const payment of payments.records) {
if (payment.type === "payment") {
console.log(
`${payment.from} -> ${payment.to}: ${payment.amount} ${payment.asset_code || "XLM"}`
);
}
}Get Effects
获取影响记录
typescript
const effects = await server
.effects()
.forAccount(publicKey)
.limit(50)
.call();typescript
const effects = await server
.effects()
.forAccount(publicKey)
.limit(50)
.call();Streaming (Server-Sent Events)
流处理(服务器发送事件)
typescript
// Stream transactions
const closeHandler = server
.transactions()
.forAccount(publicKey)
.cursor("now")
.stream({
onmessage: (tx) => {
console.log("New transaction:", tx.hash);
},
onerror: (error) => {
console.error("Stream error:", error);
},
});
// Close stream when done
closeHandler();typescript
// 流式获取交易
const closeHandler = server
.transactions()
.forAccount(publicKey)
.cursor("now")
.stream({
onmessage: (tx) => {
console.log("新交易:", tx.hash);
},
onerror: (error) => {
console.error("流处理错误:", error);
},
});
// 使用完毕后关闭流
closeHandler();Submit Transaction
提交交易
typescript
try {
const result = await server.submitTransaction(signedTransaction);
console.log("Success:", result.hash);
} catch (error) {
if (error.response?.data?.extras?.result_codes) {
console.error("Error codes:", error.response.data.extras.result_codes);
}
}typescript
try {
const result = await server.submitTransaction(signedTransaction);
console.log("成功:", result.hash);
} catch (error) {
if (error.response?.data?.extras?.result_codes) {
console.error("错误码:", error.response.data.extras.result_codes);
}
}Pagination
分页
typescript
// First page
let page = await server.transactions().forAccount(publicKey).limit(10).call();
// Next page
if (page.records.length > 0) {
page = await page.next();
}
// Previous page
page = await page.prev();typescript
// 第一页
let page = await server.transactions().forAccount(publicKey).limit(10).call();
// 下一页
if (page.records.length > 0) {
page = await page.next();
}
// 上一页
page = await page.prev();Migration: Horizon to RPC
迁移:从Horizon到RPC
Account Loading
账户加载
typescript
// Horizon (old)
const account = await horizonServer.loadAccount(publicKey);
// RPC (new)
const account = await rpc.getAccount(publicKey);
// Note: RPC returns less data, just what's needed for transactionstypescript
// Horizon(旧版)
const account = await horizonServer.loadAccount(publicKey);
// RPC(新版)
const account = await rpc.getAccount(publicKey);
// 注意:RPC返回的数据更少,仅包含交易构建所需内容Transaction Submission
交易提交
typescript
// Horizon (for classic transactions)
const result = await horizonServer.submitTransaction(tx);
// RPC (for Soroban transactions)
const response = await rpc.sendTransaction(tx);
const result = await pollForResult(response.hash);typescript
// Horizon(适用于经典交易)
const result = await horizonServer.submitTransaction(tx);
// RPC(适用于Soroban交易)
const response = await rpc.sendTransaction(tx);
const result = await pollForResult(response.hash);Historical Data
历史数据
typescript
// Horizon - full history
const allTxs = await horizonServer
.transactions()
.forAccount(publicKey)
.call();
// RPC - most methods limited to 7 days
// Exception: getLedgers can query back to genesis (Infinite Scroll)
// For full historical data, use:
// 1. Hubble (SDF's BigQuery dataset)
// 2. Galexie (data pipeline)
// 3. Your own indexertypescript
// Horizon - 完整历史数据
const allTxs = await horizonServer
.transactions()
.forAccount(publicKey)
.call();
// RPC - 多数方法仅支持7天数据
// 例外:getLedgers可通过无限滚动查询至创世区块
// 如需完整历史数据,可使用:
// 1. Hubble(SDF的BigQuery数据集)
// 2. Galexie(数据处理管道)
// 3. 自定义索引器Streaming Replacement
流处理替代方案
typescript
// Horizon - native streaming
server.payments().stream({ onmessage: handlePayment });
// RPC - polling (no native streaming)
async function pollForUpdates() {
const lastLedger = await rpc.getLatestLedger();
// Check for new events/transactions
// Repeat on interval
}
setInterval(pollForUpdates, 5000);typescript
// Horizon - 原生流处理
server.payments().stream({ onmessage: handlePayment });
// RPC - 轮询(无原生流处理)
async function pollForUpdates() {
const lastLedger = await rpc.getLatestLedger();
// 检查新事件/交易
// 按间隔重复执行
}
setInterval(pollForUpdates, 5000);Historical Data Access
历史数据访问
For data older than 7 days (not available via most RPC methods; can reach genesis via Infinite Scroll):
getLedgers对于超过7天的数据(多数RPC方法无法获取;可通过无限滚动查询至创世区块):
getLedgersHubble (BigQuery)
Hubble(BigQuery)
sql
-- Query Stellar data in BigQuery
SELECT *
FROM `crypto-stellar.crypto_stellar.history_transactions`
WHERE source_account = 'G...'
ORDER BY created_at DESC
LIMIT 100sql
-- 在BigQuery中查询Stellar数据
SELECT *
FROM `crypto-stellar.crypto_stellar.history_transactions`
WHERE source_account = 'G...'
ORDER BY created_at DESC
LIMIT 100Galexie
Galexie
Self-hosted data pipeline for processing Stellar ledger data:
自托管的数据处理管道,用于处理Stellar账本数据:
Data Lake
数据湖
RPC "Infinite Scroll" is powered by the Stellar data lake — a cloud-based object store (SEP-0054 format):
- Public access: (AWS Open Data)
s3://aws-public-blockchain/v1.1/stellar/ledgers/pubnet - Self-host: Use Galexie to export to AWS S3 or Google Cloud Storage
- Hosted: Quasar (Lightsail Network) provides hosted Galexie Data Lake + Archive RPC endpoints
- Size: ~3.8TB, growing ~0.5TB/year
- Cost: ~$160/month self-hosted ($60 compute + $100 storage)
- Docs: https://developers.stellar.org/docs/data/apis/rpc/admin-guide/data-lake-integration
RPC的"无限滚动"功能由Stellar数据湖提供支持——一个基于云的对象存储(SEP-0054格式):
- 公共访问:(AWS开放数据)
s3://aws-public-blockchain/v1.1/stellar/ledgers/pubnet - 自托管:使用Galexie导出至AWS S3或Google Cloud Storage
- 托管服务:Quasar(Lightsail Network)提供托管的Galexie数据湖 + 归档RPC端点
- 数据大小:约3.8TB,每年增长约0.5TB
- 成本:自托管约160美元/月(60美元计算资源 + 100美元存储)
- 文档:https://developers.stellar.org/docs/data/apis/rpc/admin-guide/data-lake-integration
Third-Party Indexers
第三方索引器
For complex queries, event streaming, or custom data pipelines beyond what RPC/Horizon provide:
- Mercury — Stellar-native indexer with Retroshades, GraphQL API (https://mercurydata.app)
- SubQuery — Multi-chain indexer with Stellar/Soroban support, event handlers (https://subquery.network)
- Goldsky — Real-time data replication pipelines and subgraphs (https://goldsky.com)
- StellarExpert API — Free, no-auth REST API for assets, accounts, ledger resolution (https://stellar.expert/openapi.html)
See the full indexer directory: https://developers.stellar.org/docs/data/indexers
如需复杂查询、事件流或超出RPC/Horizon能力的自定义数据管道:
- Mercury — Stellar原生索引器,支持Retroshades和GraphQL API(https://mercurydata.app)
- SubQuery — 多链索引器,支持Stellar/Soroban及事件处理(https://subquery.network)
- Goldsky — 实时数据复制管道和子图(https://goldsky.com)
- StellarExpert API — 免费无认证REST API,支持资产、账户、账本解析(https://stellar.expert/openapi.html)
Network Configuration
网络配置
For a React/Next.js-specific setup, see frontend-stellar-sdk.md. For mainnet RPC, setfrom a provider in the RPC providers directory.STELLAR_MAINNET_RPC_URL
Environment-Based Setup
基于环境变量的配置
typescript
// lib/stellar-config.ts
import * as StellarSdk from "@stellar/stellar-sdk";
type NetworkConfig = {
rpcUrl: string;
horizonUrl: string;
networkPassphrase: string;
friendbotUrl: string | null;
};
const requireEnv = (name: string): string => {
const value = process.env[name];
if (!value) throw new Error(`Missing required env var: ${name}`);
return value;
};
const configs: Record<string, NetworkConfig> = {
mainnet: {
rpcUrl: requireEnv("STELLAR_MAINNET_RPC_URL"),
horizonUrl: "https://horizon.stellar.org",
networkPassphrase: StellarSdk.Networks.PUBLIC,
friendbotUrl: null,
},
testnet: {
rpcUrl: "https://soroban-testnet.stellar.org",
horizonUrl: "https://horizon-testnet.stellar.org",
networkPassphrase: StellarSdk.Networks.TESTNET,
friendbotUrl: "https://friendbot.stellar.org",
},
local: {
rpcUrl: "http://localhost:8000/soroban/rpc",
horizonUrl: "http://localhost:8000",
networkPassphrase: "Standalone Network ; February 2017",
friendbotUrl: "http://localhost:8000/friendbot",
},
};
const network = process.env.STELLAR_NETWORK || "testnet";
export const config = configs[network];
export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);
export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);typescript
// lib/stellar-config.ts
import * as StellarSdk from "@stellar/stellar-sdk";
type NetworkConfig = {
rpcUrl: string;
horizonUrl: string;
networkPassphrase: string;
friendbotUrl: string | null;
};
const requireEnv = (name: string): string => {
const value = process.env[name];
if (!value) throw new Error(`缺失必要环境变量: ${name}`);
return value;
};
const configs: Record<string, NetworkConfig> = {
mainnet: {
rpcUrl: requireEnv("STELLAR_MAINNET_RPC_URL"),
horizonUrl: "https://horizon.stellar.org",
networkPassphrase: StellarSdk.Networks.PUBLIC,
friendbotUrl: null,
},
testnet: {
rpcUrl: "https://soroban-testnet.stellar.org",
horizonUrl: "https://horizon-testnet.stellar.org",
networkPassphrase: StellarSdk.Networks.TESTNET,
friendbotUrl: "https://friendbot.stellar.org",
},
local: {
rpcUrl: "http://localhost:8000/soroban/rpc",
horizonUrl: "http://localhost:8000",
networkPassphrase: "Standalone Network ; February 2017",
friendbotUrl: "http://localhost:8000/friendbot",
},
};
const network = process.env.STELLAR_NETWORK || "testnet";
export const config = configs[network];
export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);
export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);Best Practices
最佳实践
Use RPC for:
使用RPC的场景:
- New application development
- Soroban contract interactions
- Transaction simulation and submission
- Real-time account state
- 新应用开发
- Soroban合约交互
- 交易模拟与提交
- 实时账户状态查询
Use Horizon for:
使用Horizon的场景:
- Historical transaction queries
- Payment streaming
- Legacy application maintenance
- Rich account metadata
- 历史交易查询
- 支付流处理
- 旧版应用维护
- 丰富的账户元数据查询
Error Handling
错误处理
typescript
// RPC errors
try {
const result = await rpc.sendTransaction(tx);
} catch (error) {
if (error.code === 400) {
// Invalid transaction
} else if (error.code === 503) {
// Service unavailable
}
}
// Horizon errors
try {
const result = await horizon.submitTransaction(tx);
} catch (error) {
const extras = error.response?.data?.extras;
if (extras?.result_codes) {
// Detailed error codes
console.log("Transaction:", extras.result_codes.transaction);
console.log("Operations:", extras.result_codes.operations);
}
}typescript
// RPC错误处理
try {
const result = await rpc.sendTransaction(tx);
} catch (error) {
if (error.code === 400) {
// 无效交易
} else if (error.code === 503) {
// 服务不可用
}
}
// Horizon错误处理
try {
const result = await horizon.submitTransaction(tx);
} catch (error) {
const extras = error.response?.data?.extras;
if (extras?.result_codes) {
// 详细错误码
console.log("交易:", extras.result_codes.transaction);
console.log("操作:", extras.result_codes.operations);
}
}Rate Limiting
速率限制
Both RPC and Horizon have rate limits:
- Use exponential backoff for retries
- Cache responses where appropriate
- Consider running your own nodes for high-volume applications
typescript
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (error.response?.status === 429) {
// Rate limited - exponential backoff
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
} else {
throw error;
}
}
}
throw lastError;
}RPC和Horizon均有速率限制:
- 重试时使用指数退避策略
- 合理缓存响应结果
- 高流量应用建议自行部署节点
typescript
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (error.response?.status === 429) {
// 触发速率限制 - 指数退避
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
} else {
throw error;
}
}
}
throw lastError;
}