Skip to content

Commit cdd5ef2

Browse files
Blackman99claude
andcommitted
fix: move rate limiting into adapters, make requests sequential
Each adapter now owns its own RateLimiter and gates every individual HTTP request. The two internal fetches (normal + token txs) run sequentially instead of in parallel. Without an API key, Tronscan is limited to 1 req/s and Etherscan to 2 req/s to avoid 429 errors from browser cross-origin requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent af4860f commit cdd5ef2

3 files changed

Lines changed: 21 additions & 24 deletions

File tree

packages/core/src/adapters/etherscan/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
FetchTxOptions,
55
RawTransaction,
66
} from '../../adapter/types'
7+
import { RateLimiter } from '../../utils/rate-limiter'
78

89
export interface EtherscanAdapterConfig {
910
apiKey?: string
@@ -20,15 +21,17 @@ export class EtherscanAdapter implements DataSource {
2021
hasAddressTags: false,
2122
returnsPrebuiltGraph: false,
2223
supportedChains: ['Ethereum'],
23-
rateLimit: 5,
24+
rateLimit: 2,
2425
}
2526

2627
private apiKey: string
2728
private baseUrl: string
29+
private limiter: RateLimiter
2830

2931
constructor(config: EtherscanAdapterConfig = {}) {
3032
this.apiKey = config.apiKey || ''
3133
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL
34+
this.limiter = new RateLimiter(config.apiKey ? 5 : 2)
3235
}
3336

3437
async fetchTransactions(
@@ -37,11 +40,12 @@ export class EtherscanAdapter implements DataSource {
3740
const page = opts.page ?? 1
3841
const pageSize = opts.pageSize ?? 100
3942

40-
// Fetch normal transactions and token transfers in parallel
41-
const [normalTxs, tokenTxs] = await Promise.all([
42-
this.fetchNormalTxs(opts.address, page, pageSize),
43-
this.fetchTokenTxs(opts.address, page, pageSize),
44-
])
43+
// Sequential to respect rate limits
44+
await this.limiter.acquire()
45+
const normalTxs = await this.fetchNormalTxs(opts.address, page, pageSize)
46+
47+
await this.limiter.acquire()
48+
const tokenTxs = await this.fetchTokenTxs(opts.address, page, pageSize)
4549

4650
let allTxs = [...normalTxs, ...tokenTxs]
4751

packages/core/src/adapters/tronscan/index.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
FetchTxOptions,
55
RawTransaction,
66
} from '../../adapter/types'
7+
import { RateLimiter } from '../../utils/rate-limiter'
78

89
export interface TronscanAdapterConfig {
910
apiKey?: string
@@ -19,15 +20,18 @@ export class TronscanAdapter implements DataSource {
1920
hasAddressTags: false,
2021
returnsPrebuiltGraph: false,
2122
supportedChains: ['Tron'],
22-
rateLimit: 3,
23+
rateLimit: 1,
2324
}
2425

2526
private apiKey: string
2627
private baseUrl: string
28+
private limiter: RateLimiter
2729

2830
constructor(config: TronscanAdapterConfig = {}) {
2931
this.apiKey = config.apiKey || ''
3032
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL
33+
// Tronscan free tier is very restrictive from browser cross-origin
34+
this.limiter = new RateLimiter(config.apiKey ? 3 : 1)
3135
}
3236

3337
async fetchTransactions(
@@ -36,11 +40,12 @@ export class TronscanAdapter implements DataSource {
3640
const pageSize = opts.pageSize ?? 50
3741
const start = ((opts.page ?? 1) - 1) * pageSize
3842

39-
// Fetch TRX transfers and TRC20 transfers in parallel
40-
const [trxTxs, trc20Txs] = await Promise.all([
41-
this.fetchTrxTransfers(opts.address, start, pageSize),
42-
this.fetchTrc20Transfers(opts.address, start, pageSize),
43-
])
43+
// Sequential to respect rate limits
44+
await this.limiter.acquire()
45+
const trxTxs = await this.fetchTrxTransfers(opts.address, start, pageSize)
46+
47+
await this.limiter.acquire()
48+
const trc20Txs = await this.fetchTrc20Transfers(opts.address, start, pageSize)
4449

4550
let allTxs = [...trxTxs, ...trc20Txs]
4651

packages/core/src/graph/builder.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { TxGraph, TxNode, TxEdge } from '../types'
22
import type { DataSource } from '../adapter/types'
33
import { aggregateTransactions } from './aggregator'
4-
import { RateLimiter } from '../utils/rate-limiter'
54

65
export interface BuilderOptions {
76
maxDepth?: number
@@ -74,7 +73,6 @@ export class GraphBuilder {
7473
const nodes = new Map<string, TxNode>()
7574
const allEdges: TxEdge[] = []
7675
const visited = new Set<string>()
77-
const rateLimiter = new RateLimiter(this.adapter.capabilities.rateLimit)
7876

7977
const rootNode: TxNode = {
8078
address: rootAddress,
@@ -105,8 +103,6 @@ export class GraphBuilder {
105103
totalNodes: nodes.size,
106104
})
107105

108-
await rateLimiter.acquire()
109-
110106
let page = 1
111107
let hasMore = true
112108
const allTxs = []
@@ -123,10 +119,6 @@ export class GraphBuilder {
123119
allTxs.push(...result.transactions)
124120
hasMore = result.hasMore
125121
page++
126-
127-
if (hasMore) {
128-
await rateLimiter.acquire()
129-
}
130122
}
131123

132124
const edges = aggregateTransactions(allTxs, this.options.direction)
@@ -203,9 +195,6 @@ export class GraphBuilder {
203195
chain: string,
204196
existing: TxGraph
205197
): Promise<TxGraph> {
206-
const rateLimiter = new RateLimiter(this.adapter.capabilities.rateLimit)
207-
await rateLimiter.acquire()
208-
209198
let page = 1
210199
let hasMore = true
211200
const allTxs = []
@@ -222,7 +211,6 @@ export class GraphBuilder {
222211
allTxs.push(...result.transactions)
223212
hasMore = result.hasMore
224213
page++
225-
if (hasMore) await rateLimiter.acquire()
226214
}
227215

228216
const edges = aggregateTransactions(allTxs, this.options.direction)

0 commit comments

Comments
 (0)