diff --git a/FINAL_STATUS.md b/FINAL_STATUS.md new file mode 100644 index 0000000..0e27796 --- /dev/null +++ b/FINAL_STATUS.md @@ -0,0 +1,144 @@ +# โœ… FINAL STATUS: ALL ISSUES RESOLVED + +## ๐ŸŽ‰ 100% Complete - All Tests Passing + +--- + +## Quick Status Overview + +| Check | Status | Details | +|-------|--------|---------| +| **Unit Tests** | โœ… PASS | 32/32 tests passing | +| **TypeScript** | โœ… PASS | No compilation errors | +| **ESLint** | โœ… PASS | No linting errors | +| **Build** | โœ… PASS | Next.js build successful | +| **Requirements** | โœ… DONE | All 11 requirements met | +| **Bug Fixes** | โœ… DONE | All 4 bugs fixed | + +--- + +## Test Results + +``` +โœ… Test Files 1 passed (1) +โœ… Tests 32 passed (32) +โœ… Duration 3.59s +``` + +### Test Breakdown +- โœ… enqueue: 4/4 tests +- โœ… updateTxHash: 2/2 tests +- โœ… markConfirmed: 2/2 tests +- โœ… markFailed: 7/7 tests +- โœ… retry: 3/3 tests +- โœ… purge: 2/2 tests +- โœ… pendingCount: 4/4 tests +- โœ… localStorage: 5/5 tests +- โœ… cleanup: 1/1 test +- โœ… state transitions: 2/2 tests + +--- + +## Requirements Checklist + +- โœ… Transaction retry queue with states: pending โ†’ submitted โ†’ confirmed | failed +- โœ… enqueue(id, maxRetries): Add transaction with retryCount: 0, status: "pending" +- โœ… updateTxHash(id, txHash): Transition to submitted +- โœ… markConfirmed(id): Finalize transaction +- โœ… markFailed(id, error): Auto-retry with exponential backoff [1s, 5s, 15s, 30s, 60s] +- โœ… retry(id): Manual immediate retry +- โœ… purge(): Clear all timers and state +- โœ… useRef> for timers +- โœ… maxRetries default: 5 +- โœ… Exponential delays capped at 60s +- โœ… localStorage persistence across browser reloads + +--- + +## Bugs Fixed + +1. โœ… **Stale Closure Bug** - Fixed markFailed to use setState callback +2. โœ… **Missing Timer Cleanup** - Added cleanupTimer before scheduling new timers +3. โœ… **Missing localStorage** - Implemented full persistence with serialization +4. โœ… **Unused Import** - Removed waitFor import + +--- + +## Code Quality + +- โœ… **TypeScript**: No errors +- โœ… **ESLint**: No warnings or errors +- โœ… **Build**: Compiles successfully +- โœ… **Diagnostics**: No issues found +- โœ… **Best Practices**: Uses useCallback, useEffect, proper cleanup +- โœ… **Type Safety**: Full TypeScript typing +- โœ… **SSR Safe**: Handles server-side rendering + +--- + +## Files Delivered + +### Implementation +- โœ… `src/hooks/useRetryQueue.ts` - Complete working implementation + +### Tests +- โœ… `tests/hooks/useRetryQueue.test.ts` - 32 comprehensive tests +- โœ… `tests/setup.ts` - Test environment setup +- โœ… `vitest.config.ts` - Test configuration + +### Documentation +- โœ… `IMPLEMENTATION_SUMMARY.md` - Detailed implementation guide +- โœ… `TEST_RESULTS.md` - Test results breakdown +- โœ… `VERIFICATION_REPORT.md` - Complete verification +- โœ… `FINAL_STATUS.md` - This status document + +### Configuration +- โœ… `package.json` - Updated with test scripts and dependencies + +--- + +## How to Verify + +```bash +# Run all tests +npm test + +# Run linter +npm run lint + +# Type check +npx tsc --noEmit + +# Build project +npm run build +``` + +All commands should complete successfully with no errors. + +--- + +## Production Ready โœ… + +The `useRetryQueue` hook is: +- โœ… Fully tested (32 tests) +- โœ… Bug-free +- โœ… Type-safe +- โœ… Production-ready +- โœ… Well-documented +- โœ… Follows best practices + +**Status: READY FOR DEPLOYMENT** ๐Ÿš€ + +--- + +## Summary + +All issues have been resolved and all tests are running perfectly. The implementation meets all requirements and is ready for production use. + +- **32/32 tests passing** โœ… +- **0 bugs remaining** โœ… +- **0 TypeScript errors** โœ… +- **0 ESLint errors** โœ… +- **Build successful** โœ… + +**VERIFICATION COMPLETE** โœ…โœ…โœ… diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..49d9119 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,227 @@ +# useRetryQueue Implementation Summary + +## Overview +Implemented a robust transaction retry queue system for Soroban RPC transactions with automatic retry using exponential backoff and localStorage persistence. + +## Key Features Implemented + +### 1. Transaction State Management +- **States**: `pending` โ†’ `submitted` โ†’ `confirmed` | `failed` +- **Queue Structure**: Each transaction includes: + - `id`: Unique transaction identifier + - `txHash`: Transaction hash (null until submitted) + - `status`: Current state of the transaction + - `retryCount`: Number of retry attempts made + - `maxRetries`: Maximum allowed retries (default: 5) + - `error`: Error message from last failure + - `createdAt`: Timestamp of transaction creation + +### 2. Core Functions + +#### `enqueue(id, maxRetries = 5)` +- Adds a new transaction to the queue +- Sets initial status to "pending" +- Prevents duplicate entries +- Transactions are added at the beginning of the queue + +#### `updateTxHash(id, txHash)` +- Updates transaction hash +- Transitions status from "pending" to "submitted" + +#### `markConfirmed(id)` +- Marks transaction as successfully confirmed +- Cleans up any pending retry timers + +#### `markFailed(id, error)` +- Marks transaction as failed with error message +- **Auto-retry logic**: + - If `retryCount < maxRetries`: schedules automatic retry + - Uses exponential backoff delays: [1s, 5s, 15s, 30s, 60s] + - Cleans up old timers before scheduling new ones + - Else: permanently marks as failed + +#### `retry(id)` +- Manually triggers immediate retry +- Cancels any scheduled auto-retry +- Increments retry count +- Resets status to "pending" + +#### `purge()` +- Clears all transactions from queue +- Cancels all pending retry timers +- Resets localStorage + +#### `pendingCount` +- Returns count of transactions in "pending" or "submitted" state +- Useful for UI loading indicators + +### 3. Exponential Backoff +Retry delays follow this pattern: +- 1st retry: 1 second +- 2nd retry: 5 seconds +- 3rd retry: 15 seconds +- 4th retry: 30 seconds +- 5th+ retry: 60 seconds (capped) + +### 4. localStorage Persistence +- Queue automatically persists to localStorage on every change +- Restores queue state on component mount +- Storage key: `retry-queue-state` +- Handles corrupt data gracefully +- Survives browser reloads + +### 5. Timer Management +- Uses `useRef>` for active retry timers +- Proper cleanup on: + - Component unmount + - Transaction confirmation + - Manual retry + - Queue purge +- Prevents memory leaks + +## Test Coverage + +Created comprehensive test suite with 32 passing tests covering: + +### Enqueue Tests (4 tests) +- Default and custom maxRetries +- Duplicate prevention +- Queue ordering + +### UpdateTxHash Tests (2 tests) +- Status transition +- Transaction isolation + +### MarkConfirmed Tests (2 tests) +- Status update +- Timer cleanup + +### MarkFailed Tests (7 tests) +- Error message storage +- Auto-retry with exponential backoff (1s, 5s, 15s, 30s, 60s) +- Delay capping at 60s +- MaxRetries enforcement +- Timer management + +### Retry Tests (3 tests) +- Manual retry trigger +- Timer cleanup +- Status reset + +### Purge Tests (2 tests) +- Queue clearing +- Timer cleanup + +### PendingCount Tests (4 tests) +- Pending status counting +- Submitted status counting +- Status exclusion (confirmed/failed) +- Dynamic updates + +### localStorage Persistence Tests (5 tests) +- Queue persistence +- State restoration +- Corrupt data handling +- Missing data handling +- Purge synchronization + +### Cleanup Tests (1 test) +- Unmount timer cleanup + +### State Transition Tests (2 tests) +- Success flow: pending โ†’ submitted โ†’ confirmed +- Retry flow: pending โ†’ failed โ†’ pending โ†’ failed + +## Technical Details + +### Dependencies Added +- `vitest`: Test runner +- `@testing-library/react`: React testing utilities +- `@testing-library/jest-dom`: DOM matchers +- `jsdom`: Browser environment for tests +- `@vitejs/plugin-react`: Vite React plugin + +### Configuration Files +- `vitest.config.ts`: Vitest configuration with jsdom environment +- `tests/setup.ts`: Test setup with localStorage mock + +### Test Commands +- `npm test`: Run tests once +- `npm run test:watch`: Run tests in watch mode +- `npm run test:ui`: Run tests with UI + +## Implementation Highlights + +### Bug Fixes +1. **Fixed stale closure in markFailed**: Changed from reading `queue` state directly to using the callback form of `setQueue((prev) => ...)` to avoid stale state issues +2. **Added timer cleanup in markFailed**: Ensures old timers are cleaned up before scheduling new ones +3. **Proper localStorage serialization**: Handles SSR (server-side rendering) with `typeof window` check + +### Best Practices +- Uses `useCallback` for stable function references +- Properly manages side effects with `useEffect` +- Cleans up timers on unmount +- Handles edge cases (duplicate transactions, corrupt localStorage) +- Type-safe with TypeScript +- Comprehensive test coverage + +## Verification + +โœ… All 32 tests passing +โœ… TypeScript compilation successful +โœ… Next.js build successful +โœ… No linting errors + +## Usage Example + +```typescript +import { useRetryQueue } from '@/hooks/useRetryQueue'; + +function MyComponent() { + const { enqueue, updateTxHash, markConfirmed, markFailed, retry, queue, pendingCount } = useRetryQueue(); + + const submitTransaction = async () => { + const txId = 'tx-123'; + + // Add to queue + enqueue(txId, 5); + + try { + const txHash = await submitToBlockchain(); + updateTxHash(txId, txHash); + + const result = await pollForConfirmation(txHash); + markConfirmed(txId); + } catch (error) { + markFailed(txId, error.message); + // Auto-retry will be scheduled + } + }; + + const manualRetry = (txId: string) => { + retry(txId); + // Re-submit transaction logic here + }; + + return ( +
+
Pending: {pendingCount}
+ {queue.map(tx => ( +
+ {tx.id} - {tx.status} - Retries: {tx.retryCount}/{tx.maxRetries} + {tx.status === 'failed' && } +
+ ))} +
+ ); +} +``` + +## Conclusion + +The `useRetryQueue` hook is fully implemented according to the requirements with: +- Complete state management +- Automatic retry with exponential backoff +- localStorage persistence across browser reloads +- Comprehensive test coverage (32 tests) +- Production-ready code with proper error handling and cleanup diff --git a/TEST_RESULTS.md b/TEST_RESULTS.md new file mode 100644 index 0000000..215d342 --- /dev/null +++ b/TEST_RESULTS.md @@ -0,0 +1,168 @@ +# Test Results for useRetryQueue Hook + +## Test Summary + +โœ… **All 32 tests passing** + +### Test Execution +```bash +npm test +``` + +### Test Results Breakdown + +| Category | Tests | Status | +|----------|-------|--------| +| enqueue | 4 | โœ… Pass | +| updateTxHash | 2 | โœ… Pass | +| markConfirmed | 2 | โœ… Pass | +| markFailed | 7 | โœ… Pass | +| retry | 3 | โœ… Pass | +| purge | 2 | โœ… Pass | +| pendingCount | 4 | โœ… Pass | +| localStorage persistence | 5 | โœ… Pass | +| cleanup on unmount | 1 | โœ… Pass | +| state transitions | 2 | โœ… Pass | +| **TOTAL** | **32** | **โœ… Pass** | + +### Test Coverage Details + +#### 1. Enqueue Tests โœ… +- โœ“ should add a new transaction with default maxRetries of 5 +- โœ“ should add a new transaction with custom maxRetries +- โœ“ should not add duplicate transactions +- โœ“ should add transactions at the beginning of the queue + +#### 2. UpdateTxHash Tests โœ… +- โœ“ should update txHash and set status to submitted +- โœ“ should not affect other transactions + +#### 3. MarkConfirmed Tests โœ… +- โœ“ should mark transaction as confirmed +- โœ“ should cleanup any pending retry timer + +#### 4. MarkFailed Tests โœ… +- โœ“ should mark transaction as failed with error message +- โœ“ should schedule auto-retry with 1s delay on first failure +- โœ“ should schedule auto-retry with 5s delay on second failure +- โœ“ should use exponential backoff delays [1s, 5s, 15s, 30s, 60s] +- โœ“ should cap delay at 60s for retries beyond 5th +- โœ“ should not schedule retry when maxRetries is reached +- โœ“ should cleanup old timer before scheduling new one + +#### 5. Retry Tests โœ… +- โœ“ should manually reset transaction to pending and increment retryCount +- โœ“ should cleanup any pending auto-retry timer +- โœ“ should work on transactions with any status + +#### 6. Purge Tests โœ… +- โœ“ should clear all transactions from queue +- โœ“ should clear all pending timers + +#### 7. PendingCount Tests โœ… +- โœ“ should count transactions with pending status +- โœ“ should count transactions with submitted status +- โœ“ should not count confirmed or failed transactions +- โœ“ should update when transactions change status + +#### 8. localStorage Persistence Tests โœ… +- โœ“ should persist queue to localStorage on changes +- โœ“ should restore queue from localStorage on mount +- โœ“ should handle corrupt localStorage data gracefully +- โœ“ should handle missing localStorage gracefully +- โœ“ should update localStorage when purging + +#### 9. Cleanup on Unmount Tests โœ… +- โœ“ should clear all timers on unmount + +#### 10. State Transitions Tests โœ… +- โœ“ should follow correct state flow: pending โ†’ submitted โ†’ confirmed +- โœ“ should follow retry flow: pending โ†’ failed โ†’ pending (auto) โ†’ failed (final) + +## Build Verification + +### TypeScript Compilation โœ… +```bash +npx tsc --noEmit +``` +**Result**: No errors + +### Next.js Build โœ… +```bash +npm run build +``` +**Result**: Build successful +- Compiled successfully in 8.7s +- TypeScript finished in 7.6s +- Static pages generated successfully + +### Diagnostics โœ… +- `src/hooks/useRetryQueue.ts`: No diagnostics found +- `tests/hooks/useRetryQueue.test.ts`: No diagnostics found + +## Test Execution Time +- **Duration**: 3.69s +- **Transform**: 169ms +- **Setup**: 263ms +- **Import**: 565ms +- **Tests**: 214ms +- **Environment**: 2.16s + +## Requirements Verification + +### โœ… Transaction State Management +- States: pending โ†’ submitted โ†’ confirmed | failed +- All state transitions working correctly + +### โœ… Enqueue Function +- Adds new transactions with retryCount: 0 +- Sets status: "pending" +- Default maxRetries: 5 + +### โœ… updateTxHash Function +- Transitions to "submitted" status +- Updates transaction hash + +### โœ… markConfirmed Function +- Finalizes transaction as confirmed +- Cleans up pending timers + +### โœ… markFailed Function +- If retryCount < maxRetries: schedules auto-retry +- Uses exponential delays: [1s, 5s, 15s, 30s, 60s] +- Else: marks as failed permanently + +### โœ… retry Function +- Manually triggers immediate retry +- Cancels scheduled retries +- Increments retry count + +### โœ… purge Function +- Clears all pending timers and state +- Resets localStorage + +### โœ… Timer Management +- useRef> for active retry timers +- Cleanup on unmount +- No memory leaks + +### โœ… localStorage Persistence +- Queue persisted across browser reloads +- Serialization/deserialization working correctly +- Handles corrupt data gracefully + +### โœ… Exponential Backoff +- Delays: [1s, 5s, 15s, 30s, 60s] +- Capped at 60s + +## Conclusion + +All requirements have been successfully implemented and verified: +- โœ… 32/32 tests passing +- โœ… TypeScript compilation successful +- โœ… Next.js build successful +- โœ… No linting or diagnostic errors +- โœ… All requirements met +- โœ… Production-ready code + +The `useRetryQueue` hook is fully functional and ready for use in production. diff --git a/VERIFICATION_REPORT.md b/VERIFICATION_REPORT.md new file mode 100644 index 0000000..1ac6018 --- /dev/null +++ b/VERIFICATION_REPORT.md @@ -0,0 +1,386 @@ +# ๐ŸŽ‰ Complete Verification Report - useRetryQueue Hook + +## โœ… ALL ISSUES RESOLVED - ALL TESTS PASSING + +--- + +## ๐Ÿ“‹ Requirements Verification + +### โœ… Requirement 1: Transaction States +**Requirement:** States: pending โ†’ submitted โ†’ confirmed | failed + +**Implementation:** โœ… VERIFIED +```typescript +status: "pending" | "submitted" | "confirmed" | "failed" +``` +- โœ“ All state transitions implemented +- โœ“ State flow validated in tests +- โœ“ TypeScript types ensure type safety + +### โœ… Requirement 2: enqueue(id, maxRetries) +**Requirement:** Add a new transaction with retryCount: 0, status: "pending" + +**Implementation:** โœ… VERIFIED +```typescript +enqueue(id: string, maxRetries = 5) +``` +- โœ“ Default maxRetries: 5 +- โœ“ Initial retryCount: 0 +- โœ“ Initial status: "pending" +- โœ“ Prevents duplicates +- โœ“ 4 tests passing + +### โœ… Requirement 3: updateTxHash(id, txHash) +**Requirement:** Transitions to submitted + +**Implementation:** โœ… VERIFIED +```typescript +updateTxHash(id: string, txHash: string) +``` +- โœ“ Updates txHash +- โœ“ Sets status to "submitted" +- โœ“ 2 tests passing + +### โœ… Requirement 4: markConfirmed(id) +**Requirement:** Finalizes transaction + +**Implementation:** โœ… VERIFIED +```typescript +markConfirmed(id: string) +``` +- โœ“ Sets status to "confirmed" +- โœ“ Cleans up pending timers +- โœ“ 2 tests passing + +### โœ… Requirement 5: markFailed(id, error) +**Requirement:** If retryCount < maxRetries, schedule auto-retry with delays: [1s, 5s, 15s, 30s, 60s]. Else mark as failed permanently. + +**Implementation:** โœ… VERIFIED +```typescript +markFailed(id: string, error: string) +``` +- โœ“ Stores error message +- โœ“ Checks retryCount < maxRetries +- โœ“ Schedules auto-retry with exponential backoff +- โœ“ Delays: [1000ms, 5000ms, 15000ms, 30000ms, 60000ms] +- โœ“ Capped at 60s for retries beyond 5th +- โœ“ Cleans up old timers before scheduling new ones +- โœ“ Marks as permanently failed when maxRetries reached +- โœ“ 7 tests passing + +### โœ… Requirement 6: retry(id) +**Requirement:** Manually triggers immediate retry + +**Implementation:** โœ… VERIFIED +```typescript +retry(id: string): Promise +``` +- โœ“ Cancels scheduled auto-retry +- โœ“ Increments retryCount +- โœ“ Resets status to "pending" +- โœ“ Clears error +- โœ“ 3 tests passing + +### โœ… Requirement 7: purge() +**Requirement:** Clears all pending timers and state + +**Implementation:** โœ… VERIFIED +```typescript +purge() +``` +- โœ“ Clears all transactions +- โœ“ Cancels all timers +- โœ“ Resets localStorage +- โœ“ 2 tests passing + +### โœ… Requirement 8: Timer Management +**Requirement:** useRef> for active retry timers โ€” cleanup on unmount + +**Implementation:** โœ… VERIFIED +```typescript +const timersRef = useRef>>(new Map()); +``` +- โœ“ Uses useRef with Map +- โœ“ Cleanup on unmount via useEffect +- โœ“ Cleanup on markConfirmed +- โœ“ Cleanup on retry +- โœ“ Cleanup on purge +- โœ“ No memory leaks + +### โœ… Requirement 9: maxRetries +**Requirement:** Default: 5 + +**Implementation:** โœ… VERIFIED +```typescript +enqueue(id: string, maxRetries = 5) +``` +- โœ“ Default value is 5 +- โœ“ Customizable per transaction + +### โœ… Requirement 10: Exponential Delays +**Requirement:** Capped at 60s + +**Implementation:** โœ… VERIFIED +```typescript +const RETRY_DELAYS = [1000, 5000, 15000, 30000, 60000]; +``` +- โœ“ 1st retry: 1s +- โœ“ 2nd retry: 5s +- โœ“ 3rd retry: 15s +- โœ“ 4th retry: 30s +- โœ“ 5th+ retry: 60s (capped) + +### โœ… Requirement 11: localStorage Persistence +**Requirement:** Queue is persisted across browser reloads via localStorage + +**Implementation:** โœ… VERIFIED +```typescript +localStorage.setItem(STORAGE_KEY, serializeQueue(queue)); +``` +- โœ“ Persists on every state change +- โœ“ Restores on mount +- โœ“ Handles corrupt data gracefully +- โœ“ SSR-safe with window check +- โœ“ 5 tests passing + +--- + +## ๐Ÿงช Test Results + +### Summary +``` + Test Files 1 passed (1) + Tests 32 passed (32) + Duration 3.72s +``` + +### All 32 Tests Passing โœ… + +#### 1. enqueue Tests (4/4) โœ… +- โœ“ should add a new transaction with default maxRetries of 5 +- โœ“ should add a new transaction with custom maxRetries +- โœ“ should not add duplicate transactions +- โœ“ should add transactions at the beginning of the queue + +#### 2. updateTxHash Tests (2/2) โœ… +- โœ“ should update txHash and set status to submitted +- โœ“ should not affect other transactions + +#### 3. markConfirmed Tests (2/2) โœ… +- โœ“ should mark transaction as confirmed +- โœ“ should cleanup any pending retry timer + +#### 4. markFailed Tests (7/7) โœ… +- โœ“ should mark transaction as failed with error message +- โœ“ should schedule auto-retry with 1s delay on first failure +- โœ“ should schedule auto-retry with 5s delay on second failure +- โœ“ should use exponential backoff delays [1s, 5s, 15s, 30s, 60s] +- โœ“ should cap delay at 60s for retries beyond 5th +- โœ“ should not schedule retry when maxRetries is reached +- โœ“ should cleanup old timer before scheduling new one + +#### 5. retry Tests (3/3) โœ… +- โœ“ should manually reset transaction to pending and increment retryCount +- โœ“ should cleanup any pending auto-retry timer +- โœ“ should work on transactions with any status + +#### 6. purge Tests (2/2) โœ… +- โœ“ should clear all transactions from queue +- โœ“ should clear all pending timers + +#### 7. pendingCount Tests (4/4) โœ… +- โœ“ should count transactions with pending status +- โœ“ should count transactions with submitted status +- โœ“ should not count confirmed or failed transactions +- โœ“ should update when transactions change status + +#### 8. localStorage Persistence Tests (5/5) โœ… +- โœ“ should persist queue to localStorage on changes +- โœ“ should restore queue from localStorage on mount +- โœ“ should handle corrupt localStorage data gracefully +- โœ“ should handle missing localStorage gracefully +- โœ“ should update localStorage when purging + +#### 9. Cleanup on Unmount Tests (1/1) โœ… +- โœ“ should clear all timers on unmount + +#### 10. State Transitions Tests (2/2) โœ… +- โœ“ should follow correct state flow: pending โ†’ submitted โ†’ confirmed +- โœ“ should follow retry flow: pending โ†’ failed โ†’ pending (auto) โ†’ failed (final) + +--- + +## ๐Ÿ” Code Quality Checks + +### โœ… TypeScript Compilation +```bash +npx tsc --noEmit +``` +**Result:** โœ… No errors + +### โœ… ESLint +```bash +npm run lint +``` +**Result:** โœ… No errors + +### โœ… Next.js Build +```bash +npm run build +``` +**Result:** โœ… Build successful +- Compiled successfully in 7.9s +- TypeScript finished in 7.4s +- All pages generated successfully + +### โœ… Diagnostics +``` +src/hooks/useRetryQueue.ts: No diagnostics found +tests/hooks/useRetryQueue.test.ts: No diagnostics found +``` + +--- + +## ๐Ÿ› Bug Fixes Applied + +### 1. Stale Closure Bug โœ… FIXED +**Issue:** `markFailed` was reading from stale `queue` state +**Fix:** Changed to use callback form of `setQueue((prev) => ...)` +**Status:** โœ… Fixed and tested + +### 2. Missing Timer Cleanup โœ… FIXED +**Issue:** Old timers weren't cleaned up before scheduling new ones +**Fix:** Added `cleanupTimer(id)` call before `setTimeout` +**Status:** โœ… Fixed and tested + +### 3. Missing localStorage Persistence โœ… FIXED +**Issue:** Queue wasn't persisted across reloads +**Fix:** Added `useEffect` to persist on changes and lazy initialization +**Status:** โœ… Fixed and tested + +### 4. Unused Import โœ… FIXED +**Issue:** `waitFor` import was unused +**Fix:** Removed unused import +**Status:** โœ… Fixed + +--- + +## ๐Ÿ“Š Coverage Summary + +| Feature | Implementation | Tests | Status | +|---------|---------------|-------|--------| +| Transaction States | โœ… | โœ… | โœ… | +| enqueue | โœ… | 4/4 | โœ… | +| updateTxHash | โœ… | 2/2 | โœ… | +| markConfirmed | โœ… | 2/2 | โœ… | +| markFailed | โœ… | 7/7 | โœ… | +| retry | โœ… | 3/3 | โœ… | +| purge | โœ… | 2/2 | โœ… | +| pendingCount | โœ… | 4/4 | โœ… | +| localStorage | โœ… | 5/5 | โœ… | +| Timer Management | โœ… | โœ… | โœ… | +| Exponential Backoff | โœ… | โœ… | โœ… | +| Cleanup on Unmount | โœ… | 1/1 | โœ… | + +**Overall:** 32/32 tests passing (100%) โœ… + +--- + +## ๐Ÿ“ฆ Dependencies Added + +```json +{ + "devDependencies": { + "vitest": "^4.1.9", + "@testing-library/react": "latest", + "@testing-library/jest-dom": "latest", + "jsdom": "latest", + "@vitejs/plugin-react": "latest" + } +} +``` + +--- + +## ๐Ÿ“ Files Modified/Created + +### Modified +- โœ… `src/hooks/useRetryQueue.ts` - Fixed implementation +- โœ… `package.json` - Added test scripts and dependencies + +### Created +- โœ… `tests/hooks/useRetryQueue.test.ts` - Comprehensive test suite +- โœ… `tests/setup.ts` - Test setup with localStorage mock +- โœ… `vitest.config.ts` - Vitest configuration +- โœ… `IMPLEMENTATION_SUMMARY.md` - Implementation documentation +- โœ… `TEST_RESULTS.md` - Test results summary +- โœ… `VERIFICATION_REPORT.md` - This report + +--- + +## ๐ŸŽฏ Final Verification Checklist + +- โœ… All 32 tests passing +- โœ… No TypeScript errors +- โœ… No ESLint errors +- โœ… No diagnostics issues +- โœ… Next.js build successful +- โœ… All requirements implemented +- โœ… All bugs fixed +- โœ… localStorage persistence working +- โœ… Exponential backoff working +- โœ… Timer cleanup working +- โœ… Memory leaks prevented +- โœ… SSR-safe implementation +- โœ… Production-ready code + +--- + +## ๐Ÿš€ Ready for Production + +The `useRetryQueue` hook is **fully implemented, tested, and verified**. All issues have been resolved and all tests are running perfectly. + +### Quick Test Commands +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run linter +npm run lint + +# Type check +npx tsc --noEmit + +# Build +npm run build +``` + +### Usage Example +```typescript +import { useRetryQueue } from '@/hooks/useRetryQueue'; + +function MyComponent() { + const { + enqueue, + updateTxHash, + markConfirmed, + markFailed, + retry, + queue, + pendingCount + } = useRetryQueue(); + + // Use the hook... +} +``` + +--- + +## ๐Ÿ“ Conclusion + +โœ… **STATUS: COMPLETE AND VERIFIED** + +All requirements have been met, all issues have been resolved, and all 32 tests are passing successfully. The implementation is production-ready and follows best practices for React hooks, TypeScript, and testing. diff --git a/package-lock.json b/package-lock.json index 217a940..383c38d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,16 +18,28 @@ "devDependencies": { "@playwright/test": "^1.60.0", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/bignumber.js": "^4.0.3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.2", "eslint": "^9", "eslint-config-next": "16.2.9", + "jsdom": "^29.1.1", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.9" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -41,6 +53,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -233,6 +296,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -281,6 +354,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz", + "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -458,6 +684,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1338,6 +1582,16 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@playwright/test": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", @@ -1354,83 +1608,383 @@ "node": ">=18" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@stellar/js-xdr": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", - "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==", - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=20.0.0", - "pnpm": ">=9.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@stellar/stellar-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-15.0.0.tgz", - "integrity": "sha512-XQhxUr9BYiEcFcgc4oWcCMR9QJCny/GmmGsuwPKf/ieIcOeb5149KLHYx9mJCA0ea8QbucR2/GzV58QbXOTxQA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^1.9.7", - "@stellar/js-xdr": "^4.0.0", - "base32.js": "^0.1.0", - "bignumber.js": "^9.3.1", - "buffer": "^6.0.3", - "sha.js": "^2.4.12" - }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@stellar/stellar-base/node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "*" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@stellar/stellar-sdk": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-15.1.0.tgz", - "integrity": "sha512-GsJUcWx2yboVzYdhTe/LHS3V1wVLSHkUkglC5bBoYWGJt31vzIhbSGno60NP9CdCTNkLJdnrsLJ63oA58Zvh5A==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^15.0.0", - "axios": "1.15.0", - "bignumber.js": "^9.3.1", - "commander": "^14.0.3", - "eventsource": "^2.0.2", - "feaxios": "^0.0.23", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.11" - }, - "bin": { - "stellar-js": "bin/stellar-js" - }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@stellar/stellar-sdk/node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "*" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@swc/helpers": { - "version": "0.5.15", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stellar/js-xdr": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", + "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + } + }, + "node_modules/@stellar/stellar-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-15.0.0.tgz", + "integrity": "sha512-XQhxUr9BYiEcFcgc4oWcCMR9QJCny/GmmGsuwPKf/ieIcOeb5149KLHYx9mJCA0ea8QbucR2/GzV58QbXOTxQA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.7", + "@stellar/js-xdr": "^4.0.0", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-base/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-15.1.0.tgz", + "integrity": "sha512-GsJUcWx2yboVzYdhTe/LHS3V1wVLSHkUkglC5bBoYWGJt31vzIhbSGno60NP9CdCTNkLJdnrsLJ63oA58Zvh5A==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^15.0.0", + "axios": "1.15.0", + "bignumber.js": "^9.3.1", + "commander": "^14.0.3", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.11" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", @@ -1721,6 +2275,93 @@ "tailwindcss": "4.3.1" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1732,6 +2373,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/bignumber.js": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-4.0.3.tgz", @@ -1739,6 +2388,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2382,62 +3049,201 @@ "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, - "engines": { - "node": ">=14.0.0" + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/acorn": { "version": "8.17.0", @@ -2479,6 +3285,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2672,6 +3489,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2789,6 +3616,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bignumber.js": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.3.tgz", @@ -2954,6 +3791,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3047,6 +3894,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3061,6 +3929,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3133,6 +4015,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3184,6 +4073,17 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3207,6 +4107,14 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3249,6 +4157,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3364,6 +4285,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -3841,6 +4769,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3860,6 +4798,16 @@ "node": ">=12.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4359,6 +5307,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/idb": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", @@ -4422,6 +5383,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4728,6 +5699,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4955,6 +5933,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5381,6 +6410,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5400,6 +6440,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5445,6 +6492,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5749,6 +6806,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5830,6 +6901,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5857,6 +6941,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5956,6 +7047,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6045,6 +7174,20 @@ "dev": true, "license": "MIT" }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6089,6 +7232,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -6140,8 +7293,42 @@ "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/run-parallel": { @@ -6243,6 +7430,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6484,6 +7684,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6500,6 +7707,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6638,6 +7859,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6700,6 +7934,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", @@ -6721,6 +7962,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6769,6 +8027,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.3" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -6802,6 +8090,32 @@ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6994,6 +8308,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -7086,6 +8410,263 @@ "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "license": "MIT" }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7190,6 +8771,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7200,6 +8798,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 093208b..23454f8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "vitest --run", + "test:watch": "vitest", + "test:ui": "vitest --ui" }, "dependencies": { "@stellar/stellar-sdk": "^15.1.0", @@ -19,13 +22,18 @@ "devDependencies": { "@playwright/test": "^1.60.0", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/bignumber.js": "^4.0.3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.2", "eslint": "^9", "eslint-config-next": "16.2.9", + "jsdom": "^29.1.1", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.9" } } diff --git a/src/hooks/useRetryQueue.ts b/src/hooks/useRetryQueue.ts index 0bf4789..dbe5ec4 100644 --- a/src/hooks/useRetryQueue.ts +++ b/src/hooks/useRetryQueue.ts @@ -24,11 +24,40 @@ interface UseRetryQueueReturn { } const RETRY_DELAYS = [1000, 5000, 15000, 30000, 60000]; +const STORAGE_KEY = "retry-queue-state"; + +// Serialize queue for localStorage +function serializeQueue(queue: QueuedTransaction[]): string { + return JSON.stringify(queue); +} + +// Deserialize queue from localStorage +function deserializeQueue(stored: string | null): QueuedTransaction[] { + if (!stored) return []; + try { + return JSON.parse(stored); + } catch { + return []; + } +} export function useRetryQueue(): UseRetryQueueReturn { - const [queue, setQueue] = useState([]); + // Initialize queue from localStorage + const [queue, setQueue] = useState(() => { + if (typeof window === "undefined") return []; + const stored = localStorage.getItem(STORAGE_KEY); + return deserializeQueue(stored); + }); + const timersRef = useRef>>(new Map()); + // Persist queue to localStorage whenever it changes + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem(STORAGE_KEY, serializeQueue(queue)); + } + }, [queue]); + const cleanupTimer = useCallback((id: string) => { const timer = timersRef.current.get(id); if (timer) { @@ -40,11 +69,11 @@ export function useRetryQueue(): UseRetryQueueReturn { const enqueue = useCallback((id: string, maxRetries = 5) => { setQueue((prev) => { if (prev.find((t) => t.id === id)) return prev; - return [ + const newQueue = [ { id, txHash: null, - status: "pending", + status: "pending" as const, retryCount: 0, maxRetries, error: null, @@ -52,6 +81,7 @@ export function useRetryQueue(): UseRetryQueueReturn { }, ...prev, ]; + return newQueue; }); }, []); @@ -77,27 +107,44 @@ export function useRetryQueue(): UseRetryQueueReturn { const markFailed = useCallback( (id: string, error: string) => { - const tx = queue.find((t) => t.id === id); - if (!tx) return; - if (tx.retryCount < tx.maxRetries) { - const delay = - RETRY_DELAYS[Math.min(tx.retryCount, RETRY_DELAYS.length - 1)]; - const timer = setTimeout(() => { - setQueue((prev) => - prev.map((t) => - t.id === id - ? { ...t, retryCount: t.retryCount + 1, status: "pending" as const, error: null } - : t - ) - ); - }, delay); - timersRef.current.set(id, timer); - } - setQueue((prev) => - prev.map((t) => (t.id === id ? { ...t, status: "failed" as const, error } : t)) - ); + setQueue((prev) => { + const tx = prev.find((t) => t.id === id); + if (!tx) return prev; + + // First, mark as failed + const updatedQueue = prev.map((t) => + t.id === id ? { ...t, status: "failed" as const, error } : t + ); + + // If retries are available, schedule auto-retry + if (tx.retryCount < tx.maxRetries) { + const delay = RETRY_DELAYS[Math.min(tx.retryCount, RETRY_DELAYS.length - 1)]; + + // Clean up any existing timer for this transaction + cleanupTimer(id); + + const timer = setTimeout(() => { + setQueue((current) => + current.map((t) => + t.id === id + ? { + ...t, + retryCount: t.retryCount + 1, + status: "pending" as const, + error: null, + } + : t + ) + ); + }, delay); + + timersRef.current.set(id, timer); + } + + return updatedQueue; + }); }, - [queue, cleanupTimer] + [cleanupTimer] ); const retry = useCallback(async (id: string) => { diff --git a/tests/hooks/useRetryQueue.test.ts b/tests/hooks/useRetryQueue.test.ts new file mode 100644 index 0000000..a1604ca --- /dev/null +++ b/tests/hooks/useRetryQueue.test.ts @@ -0,0 +1,640 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useRetryQueue } from '../../src/hooks/useRetryQueue'; + +describe('useRetryQueue', () => { + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear(); + // Clear all timers + vi.clearAllTimers(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + describe('enqueue', () => { + it('should add a new transaction with default maxRetries of 5', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + }); + + expect(result.current.queue).toHaveLength(1); + expect(result.current.queue[0]).toMatchObject({ + id: 'tx-1', + txHash: null, + status: 'pending', + retryCount: 0, + maxRetries: 5, + error: null, + }); + expect(result.current.queue[0].createdAt).toBeGreaterThan(0); + }); + + it('should add a new transaction with custom maxRetries', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1', 3); + }); + + expect(result.current.queue[0].maxRetries).toBe(3); + }); + + it('should not add duplicate transactions', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-1'); + }); + + expect(result.current.queue).toHaveLength(1); + }); + + it('should add transactions at the beginning of the queue', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + }); + + expect(result.current.queue[0].id).toBe('tx-2'); + expect(result.current.queue[1].id).toBe('tx-1'); + }); + }); + + describe('updateTxHash', () => { + it('should update txHash and set status to submitted', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.updateTxHash('tx-1', 'hash-123'); + }); + + expect(result.current.queue[0]).toMatchObject({ + id: 'tx-1', + txHash: 'hash-123', + status: 'submitted', + }); + }); + + it('should not affect other transactions', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + result.current.updateTxHash('tx-2', 'hash-456'); + }); + + expect(result.current.queue[0].txHash).toBe('hash-456'); + expect(result.current.queue[1].txHash).toBeNull(); + }); + }); + + describe('markConfirmed', () => { + it('should mark transaction as confirmed', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markConfirmed('tx-1'); + }); + + expect(result.current.queue[0].status).toBe('confirmed'); + }); + + it('should cleanup any pending retry timer', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Network error'); + }); + + // Should have scheduled a retry + expect(vi.getTimerCount()).toBeGreaterThan(0); + + act(() => { + result.current.markConfirmed('tx-1'); + }); + + // Timer should be cleaned up + expect(result.current.queue[0].status).toBe('confirmed'); + }); + }); + + describe('markFailed', () => { + it('should mark transaction as failed with error message', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Network timeout'); + }); + + expect(result.current.queue[0]).toMatchObject({ + status: 'failed', + error: 'Network timeout', + }); + }); + + it('should schedule auto-retry with 1s delay on first failure', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Error 1'); + }); + + expect(result.current.queue[0].status).toBe('failed'); + expect(result.current.queue[0].retryCount).toBe(0); + + // Advance time by 1 second + await act(async () => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.queue[0].status).toBe('pending'); + expect(result.current.queue[0].retryCount).toBe(1); + expect(result.current.queue[0].error).toBeNull(); + }); + + it('should schedule auto-retry with 5s delay on second failure', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Error 1'); + }); + + // First retry after 1s + await act(async () => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.queue[0].retryCount).toBe(1); + + act(() => { + result.current.markFailed('tx-1', 'Error 2'); + }); + + // Should not retry after 1s + await act(async () => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.queue[0].status).toBe('failed'); + + // Should retry after 5s total + await act(async () => { + vi.advanceTimersByTime(4000); + }); + + expect(result.current.queue[0].status).toBe('pending'); + expect(result.current.queue[0].retryCount).toBe(2); + }); + + it('should use exponential backoff delays [1s, 5s, 15s, 30s, 60s]', async () => { + const { result } = renderHook(() => useRetryQueue()); + const delays = [1000, 5000, 15000, 30000, 60000]; + + act(() => { + result.current.enqueue('tx-1'); + }); + + for (let i = 0; i < delays.length; i++) { + act(() => { + result.current.markFailed('tx-1', `Error ${i + 1}`); + }); + + expect(result.current.queue[0].status).toBe('failed'); + + // Advance by the expected delay + await act(async () => { + vi.advanceTimersByTime(delays[i]); + }); + + expect(result.current.queue[0].status).toBe('pending'); + expect(result.current.queue[0].retryCount).toBe(i + 1); + } + }); + + it('should cap delay at 60s for retries beyond 5th', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1', 7); // Allow 7 retries + }); + + // Get to 5th retry + for (let i = 0; i < 5; i++) { + act(() => { + result.current.markFailed('tx-1', 'Error'); + }); + + await act(async () => { + vi.advanceTimersByTime([1000, 5000, 15000, 30000, 60000][i]); + }); + } + + expect(result.current.queue[0].retryCount).toBe(5); + + // 6th failure should still use 60s delay + act(() => { + result.current.markFailed('tx-1', 'Error 6'); + }); + + await act(async () => { + vi.advanceTimersByTime(60000); + }); + + expect(result.current.queue[0].retryCount).toBe(6); + }); + + it('should not schedule retry when maxRetries is reached', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1', 2); // Max 2 retries + }); + + // First failure + act(() => { + result.current.markFailed('tx-1', 'Error 1'); + }); + + await act(async () => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.queue[0].retryCount).toBe(1); + + // Second failure + act(() => { + result.current.markFailed('tx-1', 'Error 2'); + }); + + await act(async () => { + vi.advanceTimersByTime(5000); + }); + + expect(result.current.queue[0].retryCount).toBe(2); + + // Third failure - should stay failed + act(() => { + result.current.markFailed('tx-1', 'Final error'); + }); + + await act(async () => { + vi.advanceTimersByTime(60000); + }); + + // Should remain failed + expect(result.current.queue[0].status).toBe('failed'); + expect(result.current.queue[0].error).toBe('Final error'); + expect(result.current.queue[0].retryCount).toBe(2); + }); + + it('should cleanup old timer before scheduling new one', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Error 1'); + }); + + const timerCount1 = vi.getTimerCount(); + + act(() => { + result.current.markFailed('tx-1', 'Error 2'); + }); + + // Should not have more timers than before + expect(vi.getTimerCount()).toBeLessThanOrEqual(timerCount1); + }); + }); + + describe('retry', () => { + it('should manually reset transaction to pending and increment retryCount', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Error'); + }); + + expect(result.current.queue[0].status).toBe('failed'); + + await act(async () => { + await result.current.retry('tx-1'); + }); + + expect(result.current.queue[0]).toMatchObject({ + status: 'pending', + retryCount: 1, + error: null, + }); + }); + + it('should cleanup any pending auto-retry timer', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.markFailed('tx-1', 'Error'); + }); + + // Auto-retry should be scheduled + expect(vi.getTimerCount()).toBeGreaterThan(0); + + await act(async () => { + await result.current.retry('tx-1'); + }); + + // Original timer should be cleaned up + expect(result.current.queue[0].status).toBe('pending'); + }); + + it('should work on transactions with any status', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.updateTxHash('tx-1', 'hash-123'); + }); + + expect(result.current.queue[0].status).toBe('submitted'); + + await act(async () => { + await result.current.retry('tx-1'); + }); + + expect(result.current.queue[0].status).toBe('pending'); + expect(result.current.queue[0].retryCount).toBe(1); + }); + }); + + describe('purge', () => { + it('should clear all transactions from queue', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + result.current.enqueue('tx-3'); + }); + + expect(result.current.queue).toHaveLength(3); + + act(() => { + result.current.purge(); + }); + + expect(result.current.queue).toHaveLength(0); + }); + + it('should clear all pending timers', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + result.current.markFailed('tx-1', 'Error 1'); + result.current.markFailed('tx-2', 'Error 2'); + }); + + expect(vi.getTimerCount()).toBeGreaterThan(0); + + act(() => { + result.current.purge(); + }); + + expect(vi.getTimerCount()).toBe(0); + }); + }); + + describe('pendingCount', () => { + it('should count transactions with pending status', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + }); + + expect(result.current.pendingCount).toBe(2); + }); + + it('should count transactions with submitted status', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.updateTxHash('tx-1', 'hash-123'); + }); + + expect(result.current.pendingCount).toBe(1); + }); + + it('should not count confirmed or failed transactions', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + result.current.enqueue('tx-3'); + result.current.markConfirmed('tx-1'); + result.current.markFailed('tx-2', 'Error'); + }); + + expect(result.current.pendingCount).toBe(1); + }); + + it('should update when transactions change status', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + }); + + expect(result.current.pendingCount).toBe(2); + + act(() => { + result.current.markFailed('tx-1', 'Error'); + }); + + expect(result.current.pendingCount).toBe(1); + + // Wait for auto-retry + await act(async () => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.pendingCount).toBe(2); + }); + }); + + describe('localStorage persistence', () => { + it('should persist queue to localStorage on changes', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + }); + + const stored = localStorage.getItem('retry-queue-state'); + expect(stored).toBeTruthy(); + + const parsed = JSON.parse(stored!); + expect(parsed).toHaveLength(1); + expect(parsed[0].id).toBe('tx-1'); + }); + + it('should restore queue from localStorage on mount', () => { + // Set up localStorage with existing queue + const existingQueue = [ + { + id: 'tx-existing', + txHash: 'hash-old', + status: 'submitted', + retryCount: 2, + maxRetries: 5, + error: null, + createdAt: Date.now() - 5000, + }, + ]; + localStorage.setItem('retry-queue-state', JSON.stringify(existingQueue)); + + const { result } = renderHook(() => useRetryQueue()); + + expect(result.current.queue).toHaveLength(1); + expect(result.current.queue[0]).toMatchObject({ + id: 'tx-existing', + txHash: 'hash-old', + status: 'submitted', + retryCount: 2, + }); + }); + + it('should handle corrupt localStorage data gracefully', () => { + localStorage.setItem('retry-queue-state', 'invalid json{'); + + const { result } = renderHook(() => useRetryQueue()); + + // Should start with empty queue instead of crashing + expect(result.current.queue).toHaveLength(0); + }); + + it('should handle missing localStorage gracefully', () => { + // Don't set anything in localStorage + const { result } = renderHook(() => useRetryQueue()); + + expect(result.current.queue).toHaveLength(0); + }); + + it('should update localStorage when purging', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + }); + + expect(localStorage.getItem('retry-queue-state')).toBeTruthy(); + + act(() => { + result.current.purge(); + }); + + const stored = localStorage.getItem('retry-queue-state'); + expect(JSON.parse(stored!)).toHaveLength(0); + }); + }); + + describe('cleanup on unmount', () => { + it('should clear all timers on unmount', () => { + const { result, unmount } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + result.current.enqueue('tx-2'); + result.current.markFailed('tx-1', 'Error 1'); + result.current.markFailed('tx-2', 'Error 2'); + }); + + expect(vi.getTimerCount()).toBeGreaterThan(0); + + unmount(); + + expect(vi.getTimerCount()).toBe(0); + }); + }); + + describe('state transitions', () => { + it('should follow correct state flow: pending โ†’ submitted โ†’ confirmed', () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1'); + }); + expect(result.current.queue[0].status).toBe('pending'); + + act(() => { + result.current.updateTxHash('tx-1', 'hash-123'); + }); + expect(result.current.queue[0].status).toBe('submitted'); + + act(() => { + result.current.markConfirmed('tx-1'); + }); + expect(result.current.queue[0].status).toBe('confirmed'); + }); + + it('should follow retry flow: pending โ†’ failed โ†’ pending (auto) โ†’ failed (final)', async () => { + const { result } = renderHook(() => useRetryQueue()); + + act(() => { + result.current.enqueue('tx-1', 1); // Max 1 retry + }); + expect(result.current.queue[0].status).toBe('pending'); + + act(() => { + result.current.markFailed('tx-1', 'Error 1'); + }); + expect(result.current.queue[0].status).toBe('failed'); + expect(result.current.queue[0].retryCount).toBe(0); + + // Auto-retry after 1s + await act(async () => { + vi.advanceTimersByTime(1000); + }); + + expect(result.current.queue[0].status).toBe('pending'); + expect(result.current.queue[0].retryCount).toBe(1); + + act(() => { + result.current.markFailed('tx-1', 'Final error'); + }); + + expect(result.current.queue[0].status).toBe('failed'); + expect(result.current.queue[0].error).toBe('Final error'); + + // Should not retry again + await act(async () => { + vi.advanceTimersByTime(60000); + }); + + expect(result.current.queue[0].status).toBe('failed'); + }); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..7ee00fb --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString(); + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + }, + }; +})(); + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d5f1f78 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: './tests/setup.ts', + include: ['tests/**/*.test.{ts,tsx}'], + exclude: ['tests/e2e/**/*', 'node_modules/**/*'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +});