@sip-protocol/react 0.1.0 → 0.1.1

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.
@@ -0,0 +1,284 @@
1
+ import { useState, useCallback } from 'react'
2
+ import {
3
+ sendPrivateSPLTransfer,
4
+ estimatePrivateTransferFee,
5
+ hasTokenAccount,
6
+ } from '@sip-protocol/sdk'
7
+ import type {
8
+ SolanaPrivateTransferParams,
9
+ SolanaPrivateTransferResult,
10
+ } from '@sip-protocol/sdk'
11
+ import type { StealthMetaAddress } from '@sip-protocol/types'
12
+
13
+ /**
14
+ * Transfer status states
15
+ */
16
+ export type TransferStatus = 'idle' | 'estimating' | 'signing' | 'sending' | 'confirming' | 'success' | 'error'
17
+
18
+ /**
19
+ * Parameters for useStealthTransfer hook
20
+ *
21
+ * Uses generic types for Solana objects to avoid direct @solana/web3.js dependency.
22
+ * Pass actual Connection, PublicKey, and Transaction objects from your wallet adapter.
23
+ */
24
+ export interface UseStealthTransferParams {
25
+ /** Solana RPC connection (pass actual Connection from @solana/web3.js) */
26
+ connection: SolanaPrivateTransferParams['connection']
27
+ /** Sender's public key (pass actual PublicKey from @solana/web3.js) */
28
+ sender: SolanaPrivateTransferParams['sender'] | null
29
+ /** Function to sign transactions (from wallet adapter) */
30
+ signTransaction?: SolanaPrivateTransferParams['signTransaction']
31
+ }
32
+
33
+ /**
34
+ * Parameters for initiating a transfer
35
+ */
36
+ export interface TransferParams {
37
+ /** Recipient's stealth meta-address (sip:solana:...) or StealthMetaAddress object */
38
+ recipientMetaAddress: StealthMetaAddress | string
39
+ /** SPL token mint address (PublicKey) */
40
+ mint: SolanaPrivateTransferParams['mint']
41
+ /** Sender's token account ATA (PublicKey) */
42
+ senderTokenAccount: SolanaPrivateTransferParams['senderTokenAccount']
43
+ /** Amount to transfer (in token's smallest unit) */
44
+ amount: bigint
45
+ }
46
+
47
+ /**
48
+ * Return type for useStealthTransfer hook
49
+ */
50
+ export interface UseStealthTransferReturn {
51
+ /** Current transfer status */
52
+ status: TransferStatus
53
+ /** Whether a transfer is in progress */
54
+ isLoading: boolean
55
+ /** Error message if transfer failed */
56
+ error: Error | null
57
+ /** Result of the last successful transfer */
58
+ result: SolanaPrivateTransferResult | null
59
+ /** Estimated fee for the transfer (in lamports) */
60
+ estimatedFee: bigint | null
61
+ /** Initiate a stealth transfer */
62
+ transfer: (params: TransferParams) => Promise<SolanaPrivateTransferResult | null>
63
+ /** Estimate the transfer fee */
64
+ estimateFee: (mint: SolanaPrivateTransferParams['mint'], stealthAddress?: string) => Promise<bigint>
65
+ /** Reset state */
66
+ reset: () => void
67
+ /** Clear error */
68
+ clearError: () => void
69
+ }
70
+
71
+ /**
72
+ * Parse meta-address from string or object
73
+ */
74
+ function parseMetaAddress(input: StealthMetaAddress | string): StealthMetaAddress {
75
+ if (typeof input === 'object') {
76
+ return input
77
+ }
78
+
79
+ // Parse string format: sip:solana:<spendingKey>:<viewingKey>
80
+ const parts = input.split(':')
81
+ if (parts.length < 4 || parts[0] !== 'sip' || parts[1] !== 'solana') {
82
+ throw new Error('Invalid stealth meta-address format. Expected: sip:solana:<spendingKey>:<viewingKey>')
83
+ }
84
+
85
+ return {
86
+ chain: 'solana',
87
+ spendingKey: (parts[2].startsWith('0x') ? parts[2] : `0x${parts[2]}`) as `0x${string}`,
88
+ viewingKey: (parts[3].startsWith('0x') ? parts[3] : `0x${parts[3]}`) as `0x${string}`,
89
+ }
90
+ }
91
+
92
+ /**
93
+ * useStealthTransfer - Send SPL tokens to stealth addresses on Solana
94
+ *
95
+ * @remarks
96
+ * This hook provides a React-friendly interface for sending private SPL token
97
+ * transfers using stealth addresses. It handles transaction building, signing,
98
+ * and confirmation with status tracking and error handling.
99
+ *
100
+ * Features:
101
+ * - Send tokens to stealth meta-addresses
102
+ * - Automatic ATA creation for stealth addresses
103
+ * - Fee estimation
104
+ * - Transaction status tracking
105
+ * - Error handling and recovery
106
+ *
107
+ * @param params - Hook configuration parameters
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * import { useStealthTransfer } from '@sip-protocol/react'
112
+ * import { useConnection, useWallet } from '@solana/wallet-adapter-react'
113
+ * import { PublicKey } from '@solana/web3.js'
114
+ *
115
+ * function SendPrivate() {
116
+ * const { connection } = useConnection()
117
+ * const { publicKey, signTransaction } = useWallet()
118
+ *
119
+ * const {
120
+ * transfer,
121
+ * status,
122
+ * isLoading,
123
+ * error,
124
+ * result,
125
+ * } = useStealthTransfer({
126
+ * connection,
127
+ * sender: publicKey,
128
+ * signTransaction,
129
+ * })
130
+ *
131
+ * const handleSend = async () => {
132
+ * await transfer({
133
+ * recipientMetaAddress: 'sip:solana:0x...:0x...',
134
+ * mint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'), // USDC
135
+ * senderTokenAccount: myUSDCAccount,
136
+ * amount: 5_000_000n, // 5 USDC
137
+ * })
138
+ * }
139
+ *
140
+ * return (
141
+ * <div>
142
+ * <button onClick={handleSend} disabled={isLoading}>
143
+ * {isLoading ? `${status}...` : 'Send Private'}
144
+ * </button>
145
+ * {error && <p>Error: {error.message}</p>}
146
+ * {result && (
147
+ * <a href={result.explorerUrl} target="_blank" rel="noreferrer">
148
+ * View on Solscan
149
+ * </a>
150
+ * )}
151
+ * </div>
152
+ * )
153
+ * }
154
+ * ```
155
+ */
156
+ export function useStealthTransfer(params: UseStealthTransferParams): UseStealthTransferReturn {
157
+ const { connection, sender, signTransaction } = params
158
+
159
+ const [status, setStatus] = useState<TransferStatus>('idle')
160
+ const [error, setError] = useState<Error | null>(null)
161
+ const [result, setResult] = useState<SolanaPrivateTransferResult | null>(null)
162
+ const [estimatedFee, setEstimatedFee] = useState<bigint | null>(null)
163
+
164
+ const isLoading = status !== 'idle' && status !== 'success' && status !== 'error'
165
+
166
+ /**
167
+ * Estimate transfer fee
168
+ */
169
+ const estimateFee = useCallback(
170
+ async (mint: SolanaPrivateTransferParams['mint'], stealthAddress?: string): Promise<bigint> => {
171
+ setStatus('estimating')
172
+ try {
173
+ // Check if ATA needs to be created
174
+ let needsATA = true
175
+ if (stealthAddress) {
176
+ needsATA = !(await hasTokenAccount(connection, stealthAddress, mint))
177
+ }
178
+
179
+ const fee = await estimatePrivateTransferFee(connection, needsATA)
180
+ setEstimatedFee(fee)
181
+ setStatus('idle')
182
+ return fee
183
+ } catch (err) {
184
+ const error = err instanceof Error ? err : new Error('Failed to estimate fee')
185
+ setError(error)
186
+ setStatus('error')
187
+ throw error
188
+ }
189
+ },
190
+ [connection]
191
+ )
192
+
193
+ /**
194
+ * Execute stealth transfer
195
+ */
196
+ const transfer = useCallback(
197
+ async (transferParams: TransferParams): Promise<SolanaPrivateTransferResult | null> => {
198
+ // Validate prerequisites
199
+ if (!sender) {
200
+ const err = new Error('Wallet not connected: sender is null')
201
+ setError(err)
202
+ setStatus('error')
203
+ return null
204
+ }
205
+
206
+ if (!signTransaction) {
207
+ const err = new Error('Wallet does not support signing transactions')
208
+ setError(err)
209
+ setStatus('error')
210
+ return null
211
+ }
212
+
213
+ setError(null)
214
+ setResult(null)
215
+
216
+ try {
217
+ // Parse meta-address
218
+ setStatus('estimating')
219
+ const recipientMetaAddress = parseMetaAddress(transferParams.recipientMetaAddress)
220
+
221
+ // Build SDK params
222
+ setStatus('signing')
223
+ const sdkParams: SolanaPrivateTransferParams = {
224
+ connection,
225
+ sender,
226
+ senderTokenAccount: transferParams.senderTokenAccount,
227
+ recipientMetaAddress,
228
+ mint: transferParams.mint,
229
+ amount: transferParams.amount,
230
+ signTransaction,
231
+ }
232
+
233
+ // Execute transfer
234
+ setStatus('sending')
235
+ const transferResult = await sendPrivateSPLTransfer(sdkParams)
236
+
237
+ setStatus('confirming')
238
+ // Transaction is already confirmed by sendPrivateSPLTransfer
239
+
240
+ setResult(transferResult)
241
+ setStatus('success')
242
+ return transferResult
243
+ } catch (err) {
244
+ const error = err instanceof Error ? err : new Error('Transfer failed')
245
+ setError(error)
246
+ setStatus('error')
247
+ return null
248
+ }
249
+ },
250
+ [connection, sender, signTransaction]
251
+ )
252
+
253
+ /**
254
+ * Reset all state
255
+ */
256
+ const reset = useCallback(() => {
257
+ setStatus('idle')
258
+ setError(null)
259
+ setResult(null)
260
+ setEstimatedFee(null)
261
+ }, [])
262
+
263
+ /**
264
+ * Clear error state
265
+ */
266
+ const clearError = useCallback(() => {
267
+ setError(null)
268
+ if (status === 'error') {
269
+ setStatus('idle')
270
+ }
271
+ }, [status])
272
+
273
+ return {
274
+ status,
275
+ isLoading,
276
+ error,
277
+ result,
278
+ estimatedFee,
279
+ transfer,
280
+ estimateFee,
281
+ reset,
282
+ clearError,
283
+ }
284
+ }
@@ -0,0 +1,435 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react'
2
+ import {
3
+ getTransactionHistory,
4
+ exportTransactions,
5
+ getTransactionSummary,
6
+ } from '@sip-protocol/sdk'
7
+ import type {
8
+ NEARHistoricalTransaction,
9
+ NEARTransactionHistoryParams,
10
+ NEARTransactionHistoryResult,
11
+ NEARTransactionType,
12
+ NEARExportFormat,
13
+ } from '@sip-protocol/sdk'
14
+ import type { HexString } from '@sip-protocol/types'
15
+
16
+ /**
17
+ * History fetch status states
18
+ */
19
+ export type HistoryStatus = 'idle' | 'loading' | 'success' | 'error' | 'refreshing'
20
+
21
+ /**
22
+ * Parameters for useTransactionHistory hook
23
+ */
24
+ export interface UseTransactionHistoryParams {
25
+ /** NEAR RPC URL */
26
+ rpcUrl: string
27
+ /** Recipient's viewing private key (hex) */
28
+ viewingPrivateKey: HexString
29
+ /** Recipient's spending private key (hex) */
30
+ spendingPrivateKey: HexString
31
+ /** Network type */
32
+ network?: 'mainnet' | 'testnet'
33
+ /** Number of transactions per page */
34
+ pageSize?: number
35
+ /** Auto-refresh interval in milliseconds (0 = disabled) */
36
+ refreshInterval?: number
37
+ /** Initial type filter */
38
+ typeFilter?: NEARTransactionType[]
39
+ /** Initial token filter */
40
+ tokenFilter?: (string | null)[]
41
+ }
42
+
43
+ /**
44
+ * Filter state for transaction history
45
+ */
46
+ export interface HistoryFilters {
47
+ /** Filter by transaction types */
48
+ typeFilter?: NEARTransactionType[]
49
+ /** Filter by token contracts */
50
+ tokenFilter?: (string | null)[]
51
+ /** Filter by date range start (ms) */
52
+ fromTimestamp?: number
53
+ /** Filter by date range end (ms) */
54
+ toTimestamp?: number
55
+ /** Search query (hash or address) */
56
+ searchQuery?: string
57
+ }
58
+
59
+ /**
60
+ * Transaction summary statistics
61
+ */
62
+ export interface TransactionSummary {
63
+ /** Total received amounts by token */
64
+ totalReceived: Record<string, bigint>
65
+ /** Total sent amounts by token */
66
+ totalSent: Record<string, bigint>
67
+ /** Total transaction count */
68
+ transactionCount: number
69
+ /** Unique addresses involved */
70
+ uniqueAddresses: number
71
+ /** Date range of transactions */
72
+ dateRange: { from: number; to: number } | null
73
+ }
74
+
75
+ /**
76
+ * Return type for useTransactionHistory hook
77
+ */
78
+ export interface UseTransactionHistoryReturn {
79
+ /** Current fetch status */
80
+ status: HistoryStatus
81
+ /** Whether initial load is in progress */
82
+ isLoading: boolean
83
+ /** Whether refresh is in progress */
84
+ isRefreshing: boolean
85
+ /** Error if fetch failed */
86
+ error: Error | null
87
+ /** Transaction history */
88
+ transactions: NEARHistoricalTransaction[]
89
+ /** Whether more transactions are available */
90
+ hasMore: boolean
91
+ /** Current page number (0-indexed) */
92
+ page: number
93
+ /** Total transactions found */
94
+ totalCount: number
95
+ /** Last refresh timestamp */
96
+ lastRefreshedAt: Date | null
97
+ /** Time taken for last fetch (ms) */
98
+ fetchTimeMs: number
99
+ /** Transaction summary statistics */
100
+ summary: TransactionSummary | null
101
+ /** Current filters */
102
+ filters: HistoryFilters
103
+ /** Fetch initial page or refresh */
104
+ refresh: () => Promise<void>
105
+ /** Load next page */
106
+ loadMore: () => Promise<void>
107
+ /** Update filters */
108
+ setFilters: (filters: HistoryFilters) => void
109
+ /** Clear filters */
110
+ clearFilters: () => void
111
+ /** Export transactions */
112
+ exportData: (format: NEARExportFormat, options?: { prettyPrint?: boolean }) => string
113
+ /** Search transactions */
114
+ search: (query: string) => void
115
+ /** Clear error */
116
+ clearError: () => void
117
+ /** Reset to initial state */
118
+ reset: () => void
119
+ }
120
+
121
+ /**
122
+ * useTransactionHistory - View NEAR privacy transaction history
123
+ *
124
+ * @remarks
125
+ * This hook provides a React-friendly interface for viewing transaction history
126
+ * of NEAR privacy operations. It supports pagination, filtering, and export.
127
+ *
128
+ * Features:
129
+ * - Paginated transaction history
130
+ * - Filter by type, token, date range
131
+ * - Search by hash or address
132
+ * - Export to CSV/JSON
133
+ * - Auto-refresh with configurable interval
134
+ * - Transaction summary statistics
135
+ *
136
+ * @example Basic usage
137
+ * ```tsx
138
+ * function TransactionHistory() {
139
+ * const {
140
+ * transactions,
141
+ * isLoading,
142
+ * hasMore,
143
+ * loadMore,
144
+ * refresh,
145
+ * } = useTransactionHistory({
146
+ * rpcUrl: 'https://rpc.mainnet.near.org',
147
+ * viewingPrivateKey: '0x...',
148
+ * spendingPrivateKey: '0x...',
149
+ * })
150
+ *
151
+ * if (isLoading) return <div>Loading...</div>
152
+ *
153
+ * return (
154
+ * <div>
155
+ * {transactions.map(tx => (
156
+ * <div key={tx.hash}>
157
+ * {tx.type}: {tx.amountFormatted} {tx.token}
158
+ * </div>
159
+ * ))}
160
+ * {hasMore && <button onClick={loadMore}>Load More</button>}
161
+ * </div>
162
+ * )
163
+ * }
164
+ * ```
165
+ *
166
+ * @example With filtering
167
+ * ```tsx
168
+ * const { transactions, setFilters, filters } = useTransactionHistory(params)
169
+ *
170
+ * // Filter by type
171
+ * setFilters({ ...filters, typeFilter: ['receive'] })
172
+ *
173
+ * // Filter by date range
174
+ * setFilters({
175
+ * ...filters,
176
+ * fromTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000, // last 7 days
177
+ * })
178
+ * ```
179
+ *
180
+ * @param params - Hook configuration parameters
181
+ * @returns Transaction history state and actions
182
+ */
183
+ export function useTransactionHistory(
184
+ params: UseTransactionHistoryParams
185
+ ): UseTransactionHistoryReturn {
186
+ const {
187
+ rpcUrl,
188
+ viewingPrivateKey,
189
+ spendingPrivateKey,
190
+ network = 'mainnet',
191
+ pageSize = 20,
192
+ refreshInterval = 0,
193
+ typeFilter: initialTypeFilter,
194
+ tokenFilter: initialTokenFilter,
195
+ } = params
196
+
197
+ // State
198
+ const [status, setStatus] = useState<HistoryStatus>('idle')
199
+ const [error, setError] = useState<Error | null>(null)
200
+ const [transactions, setTransactions] = useState<NEARHistoricalTransaction[]>([])
201
+ const [hasMore, setHasMore] = useState(false)
202
+ const [page, setPage] = useState(0)
203
+ const [totalCount, setTotalCount] = useState(0)
204
+ const [cursor, setCursor] = useState<string | undefined>(undefined)
205
+ const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null)
206
+ const [fetchTimeMs, setFetchTimeMs] = useState(0)
207
+ const [summary, setSummary] = useState<TransactionSummary | null>(null)
208
+ const [filters, setFiltersState] = useState<HistoryFilters>({
209
+ typeFilter: initialTypeFilter,
210
+ tokenFilter: initialTokenFilter,
211
+ })
212
+
213
+ // Refs
214
+ const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
215
+ const abortControllerRef = useRef<AbortController | null>(null)
216
+
217
+ // Derived state
218
+ const isLoading = status === 'loading'
219
+ const isRefreshing = status === 'refreshing'
220
+
221
+ /**
222
+ * Fetch transaction history
223
+ */
224
+ const fetchHistory = useCallback(async (
225
+ isRefresh: boolean = false,
226
+ loadMoreCursor?: string
227
+ ): Promise<NEARTransactionHistoryResult | null> => {
228
+ // Cancel any in-flight request
229
+ if (abortControllerRef.current) {
230
+ abortControllerRef.current.abort()
231
+ }
232
+ abortControllerRef.current = new AbortController()
233
+
234
+ setStatus(isRefresh ? 'refreshing' : 'loading')
235
+ setError(null)
236
+
237
+ try {
238
+ const fetchParams: NEARTransactionHistoryParams = {
239
+ rpcUrl,
240
+ viewingPrivateKey,
241
+ spendingPrivateKey,
242
+ network,
243
+ limit: pageSize,
244
+ cursor: loadMoreCursor,
245
+ typeFilter: filters.typeFilter,
246
+ tokenFilter: filters.tokenFilter,
247
+ fromTimestamp: filters.fromTimestamp,
248
+ toTimestamp: filters.toTimestamp,
249
+ searchQuery: filters.searchQuery,
250
+ }
251
+
252
+ const result = await getTransactionHistory(fetchParams)
253
+
254
+ if (loadMoreCursor) {
255
+ // Append to existing
256
+ setTransactions(prev => [...prev, ...result.transactions])
257
+ setPage(prev => prev + 1)
258
+ } else {
259
+ // Replace
260
+ setTransactions(result.transactions)
261
+ setPage(0)
262
+ // Calculate summary
263
+ const newSummary = getTransactionSummary(result.transactions)
264
+ setSummary(newSummary)
265
+ }
266
+
267
+ setHasMore(result.hasMore)
268
+ setCursor(result.nextCursor)
269
+ setTotalCount(result.totalCount)
270
+ setFetchTimeMs(result.scanTimeMs)
271
+ setLastRefreshedAt(new Date())
272
+ setStatus('success')
273
+
274
+ return result
275
+ } catch (err) {
276
+ const error = err instanceof Error ? err : new Error(String(err))
277
+ setError(error)
278
+ setStatus('error')
279
+ return null
280
+ }
281
+ }, [
282
+ rpcUrl,
283
+ viewingPrivateKey,
284
+ spendingPrivateKey,
285
+ network,
286
+ pageSize,
287
+ filters,
288
+ ])
289
+
290
+ /**
291
+ * Refresh (fetch first page)
292
+ */
293
+ const refresh = useCallback(async () => {
294
+ setCursor(undefined)
295
+ await fetchHistory(true)
296
+ }, [fetchHistory])
297
+
298
+ /**
299
+ * Load next page
300
+ */
301
+ const loadMore = useCallback(async () => {
302
+ if (!hasMore || isLoading || isRefreshing) return
303
+ await fetchHistory(false, cursor)
304
+ }, [hasMore, isLoading, isRefreshing, cursor, fetchHistory])
305
+
306
+ /**
307
+ * Update filters
308
+ */
309
+ const setFilters = useCallback((newFilters: HistoryFilters) => {
310
+ setFiltersState(newFilters)
311
+ // Reset pagination when filters change
312
+ setCursor(undefined)
313
+ setPage(0)
314
+ }, [])
315
+
316
+ /**
317
+ * Clear all filters
318
+ */
319
+ const clearFilters = useCallback(() => {
320
+ setFiltersState({})
321
+ setCursor(undefined)
322
+ setPage(0)
323
+ }, [])
324
+
325
+ /**
326
+ * Search transactions
327
+ */
328
+ const search = useCallback((query: string) => {
329
+ setFiltersState(prev => ({
330
+ ...prev,
331
+ searchQuery: query || undefined,
332
+ }))
333
+ setCursor(undefined)
334
+ setPage(0)
335
+ }, [])
336
+
337
+ /**
338
+ * Export transactions to CSV or JSON
339
+ */
340
+ const exportData = useCallback((
341
+ format: NEARExportFormat,
342
+ options?: { prettyPrint?: boolean }
343
+ ): string => {
344
+ return exportTransactions(transactions, {
345
+ format,
346
+ prettyPrint: options?.prettyPrint,
347
+ })
348
+ }, [transactions])
349
+
350
+ /**
351
+ * Clear error
352
+ */
353
+ const clearError = useCallback(() => {
354
+ setError(null)
355
+ if (status === 'error') {
356
+ setStatus('idle')
357
+ }
358
+ }, [status])
359
+
360
+ /**
361
+ * Reset to initial state
362
+ */
363
+ const reset = useCallback(() => {
364
+ setStatus('idle')
365
+ setError(null)
366
+ setTransactions([])
367
+ setHasMore(false)
368
+ setPage(0)
369
+ setTotalCount(0)
370
+ setCursor(undefined)
371
+ setLastRefreshedAt(null)
372
+ setFetchTimeMs(0)
373
+ setSummary(null)
374
+ setFiltersState({
375
+ typeFilter: initialTypeFilter,
376
+ tokenFilter: initialTokenFilter,
377
+ })
378
+ }, [initialTypeFilter, initialTokenFilter])
379
+
380
+ // Auto-refresh effect
381
+ useEffect(() => {
382
+ if (refreshInterval > 0) {
383
+ refreshIntervalRef.current = setInterval(() => {
384
+ refresh()
385
+ }, refreshInterval)
386
+ }
387
+
388
+ return () => {
389
+ if (refreshIntervalRef.current) {
390
+ clearInterval(refreshIntervalRef.current)
391
+ refreshIntervalRef.current = null
392
+ }
393
+ }
394
+ }, [refreshInterval, refresh])
395
+
396
+ // Initial fetch when filters change
397
+ useEffect(() => {
398
+ fetchHistory(false)
399
+ }, [filters])
400
+
401
+ // Cleanup on unmount
402
+ useEffect(() => {
403
+ return () => {
404
+ if (abortControllerRef.current) {
405
+ abortControllerRef.current.abort()
406
+ }
407
+ if (refreshIntervalRef.current) {
408
+ clearInterval(refreshIntervalRef.current)
409
+ }
410
+ }
411
+ }, [])
412
+
413
+ return {
414
+ status,
415
+ isLoading,
416
+ isRefreshing,
417
+ error,
418
+ transactions,
419
+ hasMore,
420
+ page,
421
+ totalCount,
422
+ lastRefreshedAt,
423
+ fetchTimeMs,
424
+ summary,
425
+ filters,
426
+ refresh,
427
+ loadMore,
428
+ setFilters,
429
+ clearFilters,
430
+ exportData,
431
+ search,
432
+ clearError,
433
+ reset,
434
+ }
435
+ }