@spacesops/wdk-react-native-core 1.0.0-beta.68 → 1.0.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacesops/wdk-react-native-core",
3
- "version": "1.0.0-beta.68",
3
+ "version": "1.0.0-beta.69",
4
4
  "description": "Core functionality for React Native wallets - wallet management, balance fetching, and worklet operations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Tests for TransactionService
3
+ */
4
+
5
+ import { setIndexerConfig } from '../../store/indexerConfigStore'
6
+ import { TransactionService } from '../../services/transactionService'
7
+
8
+ const mockFetch = jest.fn()
9
+ global.fetch = mockFetch as typeof fetch
10
+
11
+ describe('TransactionService', () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks()
14
+ setIndexerConfig({
15
+ baseUrl: 'https://wdk-api.tether.io',
16
+ apiKey: 'test-api-key',
17
+ })
18
+ })
19
+
20
+ afterEach(() => {
21
+ setIndexerConfig(null)
22
+ })
23
+
24
+ it('fetches and parses token transfers', async () => {
25
+ mockFetch.mockResolvedValueOnce({
26
+ ok: true,
27
+ status: 200,
28
+ json: async () => ({
29
+ transfers: [
30
+ {
31
+ blockchain: 'ethereum',
32
+ transactionHash: '0xabc',
33
+ token: 'xaut',
34
+ amount: '1.5',
35
+ timestamp: 1700000000,
36
+ from: '0xfrom',
37
+ to: '0xto',
38
+ },
39
+ ],
40
+ }),
41
+ })
42
+
43
+ const transfers = await TransactionService.fetchTokenTransfers(
44
+ 'ethereum',
45
+ 'xaut',
46
+ '0xWallet'
47
+ )
48
+
49
+ expect(mockFetch).toHaveBeenCalledWith(
50
+ 'https://wdk-api.tether.io/api/v1/ethereum/xaut/0xWallet/token-transfers',
51
+ expect.objectContaining({
52
+ method: 'GET',
53
+ headers: expect.objectContaining({
54
+ 'x-api-key': 'test-api-key',
55
+ }),
56
+ })
57
+ )
58
+ expect(transfers).toHaveLength(1)
59
+ expect(transfers[0].token).toBe('xaut')
60
+ expect(transfers[0].amount).toBe('1.5')
61
+ })
62
+
63
+ it('returns empty list on 404', async () => {
64
+ mockFetch.mockResolvedValueOnce({
65
+ ok: false,
66
+ status: 404,
67
+ text: async () => 'not found',
68
+ })
69
+
70
+ const transfers = await TransactionService.fetchTokenTransfers(
71
+ 'ethereum',
72
+ 'usdt',
73
+ '0xWallet'
74
+ )
75
+ expect(transfers).toEqual([])
76
+ })
77
+
78
+ it('aggregates transfers across networks and dedupes', async () => {
79
+ mockFetch
80
+ .mockResolvedValueOnce({
81
+ ok: true,
82
+ status: 200,
83
+ json: async () => ({ transfers: [] }),
84
+ })
85
+ .mockResolvedValueOnce({
86
+ ok: true,
87
+ status: 200,
88
+ json: async () => ({
89
+ transfers: [
90
+ {
91
+ blockchain: 'ethereum',
92
+ transactionHash: '0x1',
93
+ token: 'usdt',
94
+ amount: '10',
95
+ timestamp: 100,
96
+ from: '0xa',
97
+ to: '0xb',
98
+ },
99
+ ],
100
+ }),
101
+ })
102
+ .mockResolvedValueOnce({
103
+ ok: true,
104
+ status: 200,
105
+ json: async () => ({
106
+ transfers: [
107
+ {
108
+ blockchain: 'ethereum',
109
+ transactionHash: '0x2',
110
+ token: 'xaut',
111
+ amount: '0.5',
112
+ timestamp: 200,
113
+ from: '0xc',
114
+ to: '0xd',
115
+ },
116
+ ],
117
+ }),
118
+ })
119
+
120
+ const tokenConfigs = {
121
+ ethereum: {
122
+ native: { address: null, symbol: 'ETH', name: 'Ethereum', decimals: 18 },
123
+ tokens: [
124
+ {
125
+ address: '0xusdt',
126
+ symbol: 'USDT',
127
+ name: 'Tether USD',
128
+ decimals: 6,
129
+ },
130
+ {
131
+ address: '0xxaut',
132
+ symbol: 'XAUT',
133
+ name: 'Tether Gold',
134
+ decimals: 6,
135
+ },
136
+ ],
137
+ },
138
+ }
139
+
140
+ const list = await TransactionService.fetchWalletTransactions({
141
+ addresses: { ethereum: { 0: '0xWallet' } },
142
+ accountIndex: 0,
143
+ tokenConfigs,
144
+ })
145
+
146
+ expect(list).toHaveLength(2)
147
+ expect(list[0].transactionHash).toBe('0x2')
148
+ expect(list[1].transactionHash).toBe('0x1')
149
+ })
150
+
151
+ it('resolveWalletTransactions returns empty when indexer not configured', async () => {
152
+ setIndexerConfig(null)
153
+ const list = await TransactionService.resolveWalletTransactions({
154
+ addresses: { ethereum: { 0: '0xWallet' } },
155
+ accountIndex: 0,
156
+ tokenConfigs: {
157
+ ethereum: {
158
+ native: { address: null, symbol: 'ETH', name: 'Ethereum', decimals: 18 },
159
+ tokens: [],
160
+ },
161
+ },
162
+ })
163
+ expect(list).toEqual([])
164
+ expect(mockFetch).not.toHaveBeenCalled()
165
+ })
166
+ })
@@ -55,6 +55,7 @@ import {
55
55
  DEFAULT_QUERY_STALE_TIME_MS,
56
56
  DEFAULT_QUERY_GC_TIME_MS,
57
57
  BALANCE_FETCH_STAGGER_MS,
58
+ BALANCE_FETCH_INTRA_NETWORK_STAGGER_MS,
58
59
  } from '../utils/constants'
59
60
  import { logError } from '../utils/logger'
60
61
  import { delay, withTransientRetry } from '../utils/retryUtils'
@@ -388,18 +389,21 @@ async function fetchBalancesForQueryKeys(
388
389
  }
389
390
  isFirstNetwork = false
390
391
 
391
- const networkResults = await Promise.all(
392
- keys.map(async (queryKey) => {
393
- const validated = validateQueryKeyStructure(queryKey)
394
- const result = await fetchBalance(
395
- validated.network,
396
- validated.accountIndex,
397
- validated.tokenAddress,
398
- walletId
399
- )
400
- return { id: balanceQueryKeyId(queryKey), result }
401
- })
402
- )
392
+ const networkResults: Array<{ id: string; result: BalanceFetchResult }> = []
393
+ for (let i = 0; i < keys.length; i++) {
394
+ if (i > 0) {
395
+ await delay(BALANCE_FETCH_INTRA_NETWORK_STAGGER_MS)
396
+ }
397
+ const queryKey = keys[i]
398
+ const validated = validateQueryKeyStructure(queryKey)
399
+ const result = await fetchBalance(
400
+ validated.network,
401
+ validated.accountIndex,
402
+ validated.tokenAddress,
403
+ walletId
404
+ )
405
+ networkResults.push({ id: balanceQueryKeyId(queryKey), result })
406
+ }
403
407
 
404
408
  for (const { id, result } of networkResults) {
405
409
  results.set(id, result)
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Transaction hooks with TanStack Query
3
+ *
4
+ * Fetches wallet activity from the WDK Indexer (token-transfers per network/token/address).
5
+ * Requires indexerConfig on WdkAppProvider (typically from EXPO_PUBLIC_WDK_INDEXER_* in the app).
6
+ */
7
+
8
+ import { useQuery } from '@tanstack/react-query'
9
+ import { useShallow } from 'zustand/react/shallow'
10
+
11
+ import { TransactionService } from '../services/transactionService'
12
+ import { getWalletStore } from '../store/walletStore'
13
+ import { getWorkletStore } from '../store/workletStore'
14
+ import { isIndexerConfigured } from '../store/indexerConfigStore'
15
+ import { resolveWalletId } from '../utils/storeHelpers'
16
+ import {
17
+ DEFAULT_QUERY_GC_TIME_MS,
18
+ DEFAULT_QUERY_STALE_TIME_MS,
19
+ } from '../utils/constants'
20
+ import type { TokenConfigProvider, WalletTransaction } from '../types'
21
+
22
+ export interface WalletTransactionsQueryOptions {
23
+ enabled?: boolean
24
+ staleTime?: number
25
+ refetchInterval?: number | false
26
+ walletId?: string
27
+ }
28
+
29
+ export const transactionQueryKeys = {
30
+ all: ['transactions'] as const,
31
+ byWallet: (walletId: string, accountIndex: number) =>
32
+ ['transactions', 'wallet', walletId, accountIndex] as const,
33
+ }
34
+
35
+ function resolveTokenConfigs(tokenConfigs: TokenConfigProvider) {
36
+ return typeof tokenConfigs === 'function' ? tokenConfigs() : tokenConfigs
37
+ }
38
+
39
+ function addressesFingerprint(
40
+ addresses: Record<string, Record<number, string>>,
41
+ accountIndex: number
42
+ ): string {
43
+ const parts: string[] = []
44
+ for (const network of Object.keys(addresses).sort()) {
45
+ const address = addresses[network]?.[accountIndex]
46
+ if (address) {
47
+ parts.push(`${network}:${address.toLowerCase()}`)
48
+ }
49
+ }
50
+ return parts.join('|')
51
+ }
52
+
53
+ async function fetchWalletTransactionsForAccount(
54
+ walletId: string,
55
+ accountIndex: number,
56
+ tokenConfigs: TokenConfigProvider
57
+ ): Promise<WalletTransaction[]> {
58
+ const walletStore = getWalletStore()
59
+ const addresses = walletStore.getState().addresses[walletId] ?? {}
60
+
61
+ return TransactionService.resolveWalletTransactions({
62
+ addresses,
63
+ accountIndex,
64
+ tokenConfigs: resolveTokenConfigs(tokenConfigs),
65
+ })
66
+ }
67
+
68
+ /**
69
+ * Fetch aggregated wallet transactions from the WDK Indexer.
70
+ */
71
+ export function useWalletTransactions(
72
+ accountIndex: number,
73
+ tokenConfigs: TokenConfigProvider,
74
+ options?: WalletTransactionsQueryOptions
75
+ ) {
76
+ const workletStore = getWorkletStore()
77
+ const walletStore = getWalletStore()
78
+ const isInitialized = workletStore.getState().isInitialized
79
+ const walletId = resolveWalletId(options?.walletId)
80
+ const addresses = walletStore(
81
+ useShallow((state) => state.addresses[walletId] ?? {})
82
+ )
83
+ const addressKey = addressesFingerprint(addresses, accountIndex)
84
+ const indexerReady = isIndexerConfigured()
85
+
86
+ return useQuery({
87
+ queryKey: [...transactionQueryKeys.byWallet(walletId, accountIndex), addressKey],
88
+ queryFn: () => fetchWalletTransactionsForAccount(walletId, accountIndex, tokenConfigs),
89
+ enabled:
90
+ (options?.enabled !== false) &&
91
+ isInitialized &&
92
+ indexerReady &&
93
+ addressKey.length > 0,
94
+ staleTime: options?.staleTime ?? DEFAULT_QUERY_STALE_TIME_MS,
95
+ gcTime: DEFAULT_QUERY_GC_TIME_MS,
96
+ refetchInterval: options?.refetchInterval,
97
+ })
98
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,9 @@ export type {
20
20
  TokenConfigProvider,
21
21
  TokenHelpers,
22
22
  WalletStore,
23
+ IndexerConfig,
24
+ WalletTransaction,
25
+ IndexerTokenTransfersResponse,
23
26
  } from './types'
24
27
 
25
28
  // HRPC Type Extensions (for extending HRPC functionality)
@@ -38,6 +41,8 @@ export { useWdkApp } from './hooks/useWdkApp'
38
41
  export { useWalletManager } from './hooks/useWalletManager'
39
42
  export type { UseWalletManagerResult, WalletInfo } from './hooks/useWalletManager'
40
43
  export { useBalance, useBalancesForWallet, useBalancesForWallets, useRefreshBalance, balanceQueryKeys } from './hooks/useBalance'
44
+ export { useWalletTransactions, transactionQueryKeys } from './hooks/useTransactions'
45
+ export type { WalletTransactionsQueryOptions } from './hooks/useTransactions'
41
46
  export type { AccountInfo } from './store/walletStore'
42
47
 
43
48
  // Validation Utilities (for validating configs before use)
@@ -84,6 +89,7 @@ export { WorkletLifecycleService } from './services/workletLifecycleService'
84
89
  export { AddressService } from './services/addressService'
85
90
  export { AccountService } from './services/accountService'
86
91
  export { BalanceService } from './services/balanceService'
92
+ export { TransactionService } from './services/transactionService'
87
93
  export { WalletSetupService } from './services/walletSetupService'
88
94
  export { WalletSwitchingService } from './services/walletSwitchingService'
89
95
 
@@ -48,7 +48,8 @@ import { log, logError } from '../utils/logger'
48
48
  import { validateNetworkConfigs, validateTokenConfigs } from '../utils/validation'
49
49
  import { DEFAULT_QUERY_STALE_TIME_MS, DEFAULT_QUERY_GC_TIME_MS } from '../utils/constants'
50
50
  import { InitializationStatus, AppStatus, isAppReadyStatus, isAppInProgressStatus, getCombinedStatus, getWorkletStatus } from '../utils/initializationState'
51
- import type { NetworkConfigs, TokenConfigs } from '../types'
51
+ import type { IndexerConfig, NetworkConfigs, TokenConfigs } from '../types'
52
+ import { setIndexerConfig } from '../store/indexerConfigStore'
52
53
 
53
54
 
54
55
 
@@ -134,6 +135,11 @@ export interface WdkAppProviderProps {
134
135
  networkConfigs: NetworkConfigs
135
136
  /** Token configurations for balance fetching */
136
137
  tokenConfigs: TokenConfigs
138
+ /**
139
+ * WDK Indexer configuration for transaction history.
140
+ * Apps typically pass EXPO_PUBLIC_WDK_INDEXER_BASE_URL and EXPO_PUBLIC_WDK_INDEXER_API_KEY.
141
+ */
142
+ indexerConfig?: IndexerConfig
137
143
  /** Enable automatic wallet initialization on app restart (default: true) */
138
144
  enableAutoInitialization?: boolean
139
145
  /**
@@ -190,6 +196,7 @@ const deepEqualityFn = (a: any, b: any) => {
190
196
  export function WdkAppProvider({
191
197
  networkConfigs,
192
198
  tokenConfigs,
199
+ indexerConfig,
193
200
  enableAutoInitialization = true,
194
201
  currentUserId,
195
202
  children,
@@ -267,6 +274,11 @@ export function WdkAppProvider({
267
274
  }
268
275
  }, [networkConfigs, tokenConfigs])
269
276
 
277
+ useEffect(() => {
278
+ setIndexerConfig(indexerConfig ?? null)
279
+ return () => setIndexerConfig(null)
280
+ }, [indexerConfig])
281
+
270
282
  // Worklet state - read from workletStore via hook
271
283
  const workletHookState = useWorklet()
272
284
  const {
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Transaction Service
3
+ *
4
+ * Fetches wallet transaction history from the WDK Indexer API.
5
+ * Endpoint: GET /api/v1/{network}/{token}/{address}/token-transfers
6
+ */
7
+
8
+ import { getIndexerConfig } from '../store/indexerConfigStore'
9
+ import { BALANCE_FETCH_INTRA_NETWORK_STAGGER_MS, BALANCE_FETCH_STAGGER_MS } from '../utils/constants'
10
+ import { logError, logWarn } from '../utils/logger'
11
+ import { delay, withTransientRetry } from '../utils/retryUtils'
12
+ import type { IndexerTokenTransfersResponse, TokenConfigs, WalletTransaction } from '../types'
13
+
14
+ export interface FetchWalletTransactionsParams {
15
+ addresses: Record<string, Record<number, string>>
16
+ accountIndex: number
17
+ tokenConfigs: TokenConfigs
18
+ }
19
+
20
+ function normalizeBaseUrl(baseUrl: string): string {
21
+ return baseUrl.replace(/\/$/, '')
22
+ }
23
+
24
+ function tokenSlugFromConfig(symbol: string): string {
25
+ return symbol.toLowerCase()
26
+ }
27
+
28
+ function transactionDedupeKey(tx: WalletTransaction): string {
29
+ return [
30
+ tx.blockchain,
31
+ tx.transactionHash,
32
+ tx.token,
33
+ tx.transferIndex ?? '',
34
+ tx.logIndex ?? '',
35
+ tx.from,
36
+ tx.to,
37
+ tx.amount,
38
+ ].join('|')
39
+ }
40
+
41
+ function parseIndexerTransfers(payload: unknown): WalletTransaction[] {
42
+ if (!payload || typeof payload !== 'object') {
43
+ return []
44
+ }
45
+ const transfers = (payload as IndexerTokenTransfersResponse).transfers
46
+ if (!Array.isArray(transfers)) {
47
+ return []
48
+ }
49
+
50
+ const parsed: WalletTransaction[] = []
51
+ for (const item of transfers) {
52
+ if (!item || typeof item !== 'object') continue
53
+ const tx = item as Partial<WalletTransaction>
54
+ if (
55
+ typeof tx.transactionHash !== 'string' ||
56
+ typeof tx.token !== 'string' ||
57
+ typeof tx.amount !== 'string' ||
58
+ typeof tx.timestamp !== 'number' ||
59
+ typeof tx.from !== 'string' ||
60
+ typeof tx.to !== 'string'
61
+ ) {
62
+ continue
63
+ }
64
+ parsed.push({
65
+ blockchain: typeof tx.blockchain === 'string' ? tx.blockchain : '',
66
+ blockNumber: typeof tx.blockNumber === 'number' ? tx.blockNumber : undefined,
67
+ transactionHash: tx.transactionHash,
68
+ transferIndex: typeof tx.transferIndex === 'number' ? tx.transferIndex : undefined,
69
+ token: tx.token.toLowerCase(),
70
+ amount: tx.amount,
71
+ timestamp: tx.timestamp,
72
+ transactionIndex: typeof tx.transactionIndex === 'number' ? tx.transactionIndex : undefined,
73
+ logIndex: typeof tx.logIndex === 'number' ? tx.logIndex : undefined,
74
+ from: tx.from,
75
+ to: tx.to,
76
+ label: typeof tx.label === 'string' ? tx.label : undefined,
77
+ })
78
+ }
79
+ return parsed
80
+ }
81
+
82
+ export class TransactionService {
83
+ /**
84
+ * Fetch token transfers for one network/token/address from the indexer.
85
+ */
86
+ static async fetchTokenTransfers(
87
+ network: string,
88
+ tokenSlug: string,
89
+ address: string
90
+ ): Promise<WalletTransaction[]> {
91
+ const config = getIndexerConfig()
92
+ if (!config?.baseUrl || !config.apiKey) {
93
+ throw new Error('WDK Indexer is not configured')
94
+ }
95
+
96
+ const url = `${normalizeBaseUrl(config.baseUrl)}/api/v1/${encodeURIComponent(network)}/${encodeURIComponent(tokenSlug)}/${encodeURIComponent(address)}/token-transfers`
97
+
98
+ const response = await withTransientRetry(
99
+ async () => {
100
+ const res = await fetch(url, {
101
+ method: 'GET',
102
+ headers: {
103
+ 'x-api-key': config.apiKey,
104
+ Accept: 'application/json',
105
+ },
106
+ })
107
+
108
+ if (res.status === 404) {
109
+ return { ok: true as const, transfers: [] as WalletTransaction[] }
110
+ }
111
+
112
+ if (!res.ok) {
113
+ const text = await res.text().catch(() => '')
114
+ throw new Error(
115
+ `Indexer token-transfers failed (${res.status}) for ${network}/${tokenSlug}: ${text.slice(0, 200)}`
116
+ )
117
+ }
118
+
119
+ const json: unknown = await res.json()
120
+ return { ok: true as const, transfers: parseIndexerTransfers(json) }
121
+ },
122
+ { label: `${network}/${tokenSlug} token-transfers` }
123
+ )
124
+
125
+ return response.transfers
126
+ }
127
+
128
+ /**
129
+ * Fetch transfers for all configured networks/tokens for one account index.
130
+ */
131
+ static async fetchWalletTransactions(
132
+ params: FetchWalletTransactionsParams
133
+ ): Promise<WalletTransaction[]> {
134
+ const { addresses, accountIndex, tokenConfigs } = params
135
+ const networks = Object.keys(tokenConfigs)
136
+ const deduped = new Map<string, WalletTransaction>()
137
+ let isFirstNetwork = true
138
+
139
+ for (const network of networks) {
140
+ const address = addresses[network]?.[accountIndex]
141
+ if (!address) {
142
+ continue
143
+ }
144
+
145
+ if (!isFirstNetwork) {
146
+ await delay(BALANCE_FETCH_STAGGER_MS)
147
+ }
148
+ isFirstNetwork = false
149
+
150
+ const networkTokens = tokenConfigs[network]
151
+ if (!networkTokens) continue
152
+
153
+ const tokens = [networkTokens.native, ...networkTokens.tokens]
154
+ for (let i = 0; i < tokens.length; i++) {
155
+ if (i > 0) {
156
+ await delay(BALANCE_FETCH_INTRA_NETWORK_STAGGER_MS)
157
+ }
158
+
159
+ const token = tokens[i]
160
+ const tokenSlug = tokenSlugFromConfig(token.symbol)
161
+ try {
162
+ const transfers = await this.fetchTokenTransfers(network, tokenSlug, address)
163
+ for (const tx of transfers) {
164
+ const withBlockchain = tx.blockchain
165
+ ? tx
166
+ : { ...tx, blockchain: network }
167
+ deduped.set(transactionDedupeKey(withBlockchain), withBlockchain)
168
+ }
169
+ } catch (error) {
170
+ logError(
171
+ `Failed to fetch token transfers for ${network}/${tokenSlug}:`,
172
+ error
173
+ )
174
+ }
175
+ }
176
+ }
177
+
178
+ return Array.from(deduped.values()).sort((a, b) => b.timestamp - a.timestamp)
179
+ }
180
+
181
+ /**
182
+ * Resolve wallet transactions (alias for fetchWalletTransactions).
183
+ * Matches the legacy resolveWalletTransactions name used by consuming apps.
184
+ */
185
+ static async resolveWalletTransactions(
186
+ params: FetchWalletTransactionsParams
187
+ ): Promise<WalletTransaction[]> {
188
+ if (!getIndexerConfig()) {
189
+ logWarn('[TransactionService] Indexer not configured — returning empty transaction list')
190
+ return []
191
+ }
192
+ return this.fetchWalletTransactions(params)
193
+ }
194
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Runtime WDK Indexer configuration (set by WdkAppProvider from app env).
3
+ */
4
+
5
+ import type { IndexerConfig } from '../types'
6
+
7
+ let indexerConfig: IndexerConfig | null = null
8
+
9
+ export function setIndexerConfig(config: IndexerConfig | null): void {
10
+ indexerConfig = config
11
+ }
12
+
13
+ export function getIndexerConfig(): IndexerConfig | null {
14
+ return indexerConfig
15
+ }
16
+
17
+ export function isIndexerConfigured(): boolean {
18
+ return Boolean(indexerConfig?.baseUrl && indexerConfig?.apiKey)
19
+ }
package/src/types.ts CHANGED
@@ -157,6 +157,42 @@ export interface BalanceFetchResult {
157
157
  */
158
158
  export type TokenConfigProvider = TokenConfigs | (() => TokenConfigs)
159
159
 
160
+ /**
161
+ * WDK Indexer API configuration (balance/transaction history).
162
+ */
163
+ export interface IndexerConfig {
164
+ /** Base URL, e.g. https://wdk-api.tether.io */
165
+ baseUrl: string
166
+ /** API key sent as x-api-key header */
167
+ apiKey: string
168
+ }
169
+
170
+ /**
171
+ * Token transfer from the WDK Indexer token-transfers endpoint.
172
+ */
173
+ export interface WalletTransaction {
174
+ blockchain: string
175
+ blockNumber?: number
176
+ transactionHash: string
177
+ transferIndex?: number
178
+ token: string
179
+ amount: string
180
+ /** Unix timestamp in seconds (indexer convention) */
181
+ timestamp: number
182
+ transactionIndex?: number
183
+ logIndex?: number
184
+ from: string
185
+ to: string
186
+ label?: string
187
+ }
188
+
189
+ /**
190
+ * Raw indexer response for token-transfers.
191
+ */
192
+ export interface IndexerTokenTransfersResponse {
193
+ transfers?: WalletTransaction[]
194
+ }
195
+
160
196
  /**
161
197
  * Token Helpers
162
198
  *
@@ -73,6 +73,12 @@ export const DEFAULT_QUERY_GC_TIME_MS = 5 * 60 * 1000
73
73
  */
74
74
  export const BALANCE_FETCH_STAGGER_MS = 400
75
75
 
76
+ /**
77
+ * Delay between token balance fetches within the same network (ms).
78
+ * TON jetton balances issue multiple RPC calls each; serializing avoids 429 bursts.
79
+ */
80
+ export const BALANCE_FETCH_INTRA_NETWORK_STAGGER_MS = 300
81
+
76
82
  /**
77
83
  * Max attempts for transient blockchain/RPC errors (429, 5xx, network timeouts).
78
84
  */