@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.
- package/README.md +54 -14
- package/dist/index.d.mts +1224 -6
- package/dist/index.d.ts +1224 -6
- package/dist/index.js +5783 -10
- package/dist/index.mjs +5777 -9
- package/package.json +9 -8
- package/src/components/ethereum/index.ts +55 -0
- package/src/components/ethereum/privacy-toggle.tsx +822 -0
- package/src/components/ethereum/stealth-address-display.tsx +1050 -0
- package/src/components/ethereum/transaction-history.tsx +1187 -0
- package/src/components/ethereum/transaction-tracker.tsx +302 -0
- package/src/components/ethereum/viewing-key-manager.tsx +228 -0
- package/src/components/index.ts +107 -0
- package/src/components/privacy-toggle.tsx +548 -0
- package/src/components/stealth-address-display.tsx +770 -0
- package/src/components/transaction-history.tsx +651 -0
- package/src/components/transaction-tracker.tsx +1079 -0
- package/src/components/viewing-key-manager.tsx +1576 -0
- package/src/hooks/index.ts +61 -0
- package/src/hooks/use-privacy-advisor.ts +371 -0
- package/src/hooks/use-private-swap.ts +5 -5
- package/src/hooks/use-proof-composition.ts +654 -0
- package/src/hooks/use-scan-payments.ts +504 -0
- package/src/hooks/use-stealth-address.ts +23 -7
- package/src/hooks/use-stealth-transfer.ts +284 -0
- package/src/hooks/use-transaction-history.ts +435 -0
- package/src/index.ts +75 -0
|
@@ -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
|
+
}
|