938 lines
28 KiB
JavaScript
938 lines
28 KiB
JavaScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useReducer,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import {
|
|
BTC_ASSET_ADDRESS,
|
|
findBestPool,
|
|
USD_ASSET_ADDRESS,
|
|
swapBitcoinToToken,
|
|
getUserSwapHistory,
|
|
minFlashnetSwapAmounts,
|
|
checkClawbackEligibility,
|
|
requestManualClawback,
|
|
dollarsToSats,
|
|
currentPriceAinBToPriceDollars,
|
|
addActiveSwap,
|
|
removeActiveSwap,
|
|
savePendingSwapConfirmation,
|
|
completeSwapConfirmation,
|
|
retryPendingSwapConfirmations,
|
|
SEND_AMOUNT_INCREASE_BUFFER,
|
|
} from '../app/functions/spark/flashnet';
|
|
import { useAppStatus } from './appStatus';
|
|
import { useSparkWallet } from './sparkContext';
|
|
import { useActiveCustodyAccount } from './activeAccount';
|
|
import {
|
|
getSingleTxDetails,
|
|
getSparkPaymentStatus,
|
|
initializeFlashnet,
|
|
} from '../app/functions/spark';
|
|
import { USDB_TOKEN_ID } from '../app/constants';
|
|
import {
|
|
bulkUpdateSparkTransactions,
|
|
deleteUnpaidSparkLightningTransaction,
|
|
flashnetAutoSwapsEventListener,
|
|
getPendingAutoSwaps,
|
|
getSingleSparkLightningRequest,
|
|
HANDLE_FLASHNET_AUTO_SWAP,
|
|
updateSparkTransactionDetails,
|
|
} from '../app/functions/spark/transactions';
|
|
import {
|
|
isFlashnetTransfer,
|
|
loadSavedTransferIds,
|
|
setFlashnetTransfer,
|
|
} from '../app/functions/spark/handleFlashnetTransferIds';
|
|
import { useToast } from './toastManager';
|
|
import { useAuthContext } from './authContext';
|
|
import { listClawbackableTransfers } from '../app/functions/spark/flashnet';
|
|
import { getLocalStorageItem, setLocalStorageItem } from '../app/functions';
|
|
|
|
const FlashnetContext = createContext(null);
|
|
const MAX_SWAP_RETRIES = 10;
|
|
|
|
// Only these pool fields are consumed anywhere in the app; ignore the rest
|
|
// (updatedAt/reserves/tvl/volume change every poll and would force needless
|
|
// re-renders of all ~30 consumers).
|
|
const POOL_WATCHED_KEYS = ['lpPublicKey', 'currentPriceAInB', 'lpFeeBps'];
|
|
const poolInfoChanged = (next, prev) =>
|
|
POOL_WATCHED_KEYS.some(k => next?.[k] !== prev?.[k]);
|
|
|
|
export function FlashnetProvider({ children }) {
|
|
const { showToast } = useToast();
|
|
const { currentWalletMnemoinc } = useActiveCustodyAccount();
|
|
const { appState } = useAppStatus();
|
|
const { sparkInformation, sparkInfoRef, setSparkInformation } =
|
|
useSparkWallet();
|
|
const [poolInfo, setPoolInfo] = useState({});
|
|
const [swapLimits, setSwapLimits] = useState({ usd: 1.03, bitcoin: 1030 });
|
|
const swapLimitsRef = useRef(swapLimits);
|
|
const poolInfoRef = useRef({});
|
|
const poolIntervalRef = useRef(null);
|
|
const currentWalletMnemoincRef = useRef(currentWalletMnemoinc);
|
|
|
|
const triggeredSwapsRef = useRef(new Set());
|
|
|
|
const refundMonitorIntervalRef = useRef(null);
|
|
const swapMonitorIntervalRef = useRef(null);
|
|
const retryTimeoutRef = useRef(null);
|
|
|
|
const flashnetRetryIntervalRef = useRef(null);
|
|
const flashnetRetryDelayRef = useRef(5_000);
|
|
const { authResetkey } = useAuthContext();
|
|
const authResetKeyRef = useRef(authResetkey);
|
|
const isInitialLoad = useRef(true);
|
|
|
|
const REFUND_MONITOR_INTERVAL = 25_000;
|
|
const SWAP_MONITOR_INTERVAL = 30_000;
|
|
|
|
useEffect(() => {
|
|
authResetKeyRef.current = authResetkey;
|
|
}, [authResetkey]);
|
|
|
|
useEffect(() => {
|
|
currentWalletMnemoincRef.current = currentWalletMnemoinc;
|
|
}, [currentWalletMnemoinc]);
|
|
|
|
useEffect(() => {
|
|
poolInfoRef.current = poolInfo;
|
|
}, [poolInfo]);
|
|
|
|
useEffect(() => {
|
|
swapLimitsRef.current = swapLimits;
|
|
}, [swapLimits]);
|
|
|
|
const togglePoolInfo = poolInfo => {
|
|
setPoolInfo(poolInfo);
|
|
};
|
|
|
|
const refreshPool = async () => {
|
|
if (!sparkInformation.didConnectToFlashnet) return;
|
|
if (appState !== 'active') return;
|
|
|
|
const result = await findBestPool(
|
|
currentWalletMnemoincRef.current,
|
|
BTC_ASSET_ADDRESS,
|
|
USD_ASSET_ADDRESS,
|
|
);
|
|
|
|
if (
|
|
result?.didWork &&
|
|
result.pool &&
|
|
poolInfoChanged(result.pool, poolInfoRef.current)
|
|
) {
|
|
setLocalStorageItem('swapPoolInfo', JSON.stringify(result.pool));
|
|
setPoolInfo(result.pool);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
async function loadSavedPoolInfo() {
|
|
const savedPoolInfo = JSON.parse(
|
|
await getLocalStorageItem('swapPoolInfo'),
|
|
);
|
|
console.log('saved pool info', savedPoolInfo);
|
|
if (savedPoolInfo) {
|
|
setPoolInfo(savedPoolInfo);
|
|
}
|
|
}
|
|
if (Object.keys(poolInfo).length) return;
|
|
if (appState !== 'active') return;
|
|
loadSavedPoolInfo();
|
|
}, [appState]);
|
|
|
|
useEffect(() => {
|
|
if (retryTimeoutRef.current) {
|
|
clearTimeout(retryTimeoutRef.current);
|
|
retryTimeoutRef.current = null;
|
|
}
|
|
|
|
if (
|
|
appState === 'active' &&
|
|
sparkInformation.didConnectToFlashnet &&
|
|
currentWalletMnemoincRef.current &&
|
|
poolInfo?.lpPublicKey
|
|
) {
|
|
// Delay to ensure everything is initialized
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
retryTimeoutRef.current = setTimeout(() => {
|
|
// Check if auth has changed before executing
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Flashnet] Auth changed, skipping retry confirmations');
|
|
return;
|
|
}
|
|
|
|
retryPendingSwapConfirmations(
|
|
currentWalletMnemoincRef.current,
|
|
sparkInfoRef,
|
|
);
|
|
}, 3000);
|
|
}
|
|
|
|
return () => {
|
|
if (retryTimeoutRef.current) {
|
|
clearTimeout(retryTimeoutRef.current);
|
|
retryTimeoutRef.current = null;
|
|
}
|
|
};
|
|
}, [appState, sparkInformation.didConnectToFlashnet, poolInfo?.lpPublicKey]);
|
|
|
|
const handleAutoSwap = async (sparkRequestID, retryCount = 0) => {
|
|
let outboundTransferId;
|
|
try {
|
|
const MAX_RETRIES = 7;
|
|
const RETRY_DELAY = 2000;
|
|
console.log('Auto-swap triggered for sparkRequestID:', sparkRequestID);
|
|
if (triggeredSwapsRef.current.has(sparkRequestID)) {
|
|
console.warn(
|
|
'Auto-swap already triggered for sparkRequestID:',
|
|
sparkRequestID,
|
|
);
|
|
return;
|
|
} else {
|
|
triggeredSwapsRef.current.add(sparkRequestID);
|
|
}
|
|
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: true,
|
|
lastSwapAttempt: Date.now(),
|
|
});
|
|
|
|
// Get the lightning request
|
|
const lightningRequest = await getSingleSparkLightningRequest(
|
|
sparkRequestID,
|
|
);
|
|
|
|
console.log(
|
|
'found saved lightning requset invoice for',
|
|
lightningRequest,
|
|
);
|
|
|
|
if (!lightningRequest) {
|
|
console.error('Lightning request not found');
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Check if we have the finalSparkID
|
|
if (!lightningRequest.details?.finalSparkID) {
|
|
console.error('No finalSparkID found in lightning request');
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Get the transaction details
|
|
const txDetails = await getSingleTxDetails(
|
|
currentWalletMnemoincRef.current,
|
|
lightningRequest.details.finalSparkID,
|
|
);
|
|
|
|
if (!txDetails) {
|
|
console.error('Transaction details not found');
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const status = getSparkPaymentStatus(txDetails.status);
|
|
|
|
// Check if payment is settled
|
|
if (status !== 'completed') {
|
|
console.log('Payment not yet settled, status:', status);
|
|
|
|
// Retry logic
|
|
if (retryCount < MAX_RETRIES) {
|
|
setTimeout(() => {
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
handleAutoSwap(sparkRequestID, retryCount + 1);
|
|
}, RETRY_DELAY);
|
|
return;
|
|
} else {
|
|
console.error('Max retries reached, payment still not settled');
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const userRequest = txDetails.userRequest;
|
|
const invoice = userRequest ? userRequest.invoice?.encodedInvoice : '';
|
|
|
|
// Get the amount in sats
|
|
const amountSats = txDetails.totalValue;
|
|
|
|
if (
|
|
!amountSats ||
|
|
amountSats <= 0 ||
|
|
amountSats < swapLimitsRef.current.bitcoin
|
|
) {
|
|
console.error('Invalid amount for swap');
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
deleteUnpaidSparkLightningTransaction(sparkRequestID);
|
|
|
|
const tx = {
|
|
id: lightningRequest.details?.finalSparkID,
|
|
accountId: sparkInfoRef.current.identityPubKey,
|
|
paymentStatus: 'pending',
|
|
paymentType: 'lightning',
|
|
details: {
|
|
performSwaptoUSD: false,
|
|
},
|
|
};
|
|
await bulkUpdateSparkTransactions([tx]);
|
|
return;
|
|
}
|
|
|
|
// Check if we have pool info
|
|
const currentPoolInfo = poolInfoRef.current;
|
|
if (!currentPoolInfo || !currentPoolInfo.lpPublicKey) {
|
|
console.error('Pool info not available');
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
console.log('Executing auto-swap:', {
|
|
amount: amountSats,
|
|
poolId: currentPoolInfo.lpPublicKey,
|
|
});
|
|
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapInitiated: true,
|
|
swapAmount: amountSats,
|
|
});
|
|
|
|
// Execute the swap
|
|
const result = await swapBitcoinToToken(
|
|
currentWalletMnemoincRef.current,
|
|
{
|
|
tokenAddress: USD_ASSET_ADDRESS,
|
|
amountSats: amountSats,
|
|
poolId: currentPoolInfo.lpPublicKey,
|
|
},
|
|
);
|
|
|
|
if (result.didWork && result.swap) {
|
|
outboundTransferId = result.swap.outboundTransferId;
|
|
addActiveSwap(result.swap.outboundTransferId);
|
|
savePendingSwapConfirmation(sparkRequestID, outboundTransferId);
|
|
await updateSparkTransactionDetails(lightningRequest.sparkID, {
|
|
completedSwaptoUSD: true,
|
|
swapExecuting: false,
|
|
swapRetryCount: 0,
|
|
});
|
|
|
|
console.log('Auto-swap completed successfully:', {
|
|
amountOut: result.swap.amountOut,
|
|
executionPrice: result.swap.executionPrice,
|
|
});
|
|
|
|
const success = await completeSwapConfirmation(
|
|
sparkRequestID,
|
|
outboundTransferId,
|
|
lightningRequest,
|
|
txDetails,
|
|
result,
|
|
invoice,
|
|
currentWalletMnemoincRef.current,
|
|
sparkInfoRef,
|
|
);
|
|
|
|
if (!success) {
|
|
console.warn(
|
|
'[Auto-swap] Confirmation failed, will retry on next load',
|
|
);
|
|
}
|
|
} else {
|
|
console.error('Auto-swap failed:', result.error);
|
|
|
|
if (outboundTransferId) {
|
|
removeActiveSwap(sparkRequestID);
|
|
}
|
|
|
|
// Mark as failed but not executing, so it can be retried
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in auto-swap handler:', error);
|
|
if (outboundTransferId) {
|
|
removeActiveSwap(outboundTransferId);
|
|
}
|
|
// Mark as failed but not executing
|
|
try {
|
|
await updateSparkTransactionDetails(sparkRequestID, {
|
|
swapExecuting: false,
|
|
});
|
|
} catch (updateError) {
|
|
console.error('Failed to update swap status:', updateError);
|
|
}
|
|
|
|
triggeredSwapsRef.current.delete(sparkRequestID);
|
|
}
|
|
};
|
|
|
|
// Monitor for stuck/failed swaps that need retry
|
|
const runPendingSwapsMonitor = async () => {
|
|
try {
|
|
if (appState !== 'active') return;
|
|
if (!sparkInformation.didConnectToFlashnet) return;
|
|
if (!currentWalletMnemoincRef.current) return;
|
|
if (!poolInfoRef.current?.lpPublicKey) return;
|
|
|
|
console.log('[Pending Swaps Monitor] Checking for stuck swaps...');
|
|
|
|
const pendingSwaps = await getPendingAutoSwaps();
|
|
|
|
if (!pendingSwaps || pendingSwaps.length === 0) {
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
`[Pending Swaps Monitor] Found ${pendingSwaps.length} pending swap(s)`,
|
|
);
|
|
|
|
for (const swapRequest of pendingSwaps) {
|
|
const sparkID = swapRequest.sparkID;
|
|
const details = swapRequest.details || {};
|
|
const finalSparkID = details.finalSparkID;
|
|
|
|
// Skip if already in our triggered set (currently processing)
|
|
if (triggeredSwapsRef.current.has(sparkID)) {
|
|
console.log(
|
|
`[Pending Swaps Monitor] Swap ${sparkID} already processing`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Handle payments already swapped but have not been removed from db yet
|
|
if (details.completedSwaptoUSD || isFlashnetTransfer(finalSparkID)) {
|
|
console.warn(
|
|
`[Pending Swaps Monitor] Blocking already completed swaps`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const currentRetries = details.swapRetryCount || 0;
|
|
if (currentRetries >= MAX_SWAP_RETRIES) {
|
|
console.error(
|
|
`[Pending Swaps Monitor] Swap ${sparkID} exceeded max retries (${MAX_SWAP_RETRIES}), cleaning up`,
|
|
);
|
|
|
|
// Delete the lightning request
|
|
await deleteUnpaidSparkLightningTransaction(sparkID);
|
|
|
|
// Update the final transaction to remove swap flags if it exists
|
|
if (finalSparkID) {
|
|
const tx = {
|
|
id: finalSparkID,
|
|
accountId: sparkInfoRef.current.identityPubKey,
|
|
paymentStatus: 'completed',
|
|
paymentType: 'lightning',
|
|
details: {
|
|
performSwaptoUSD: false,
|
|
},
|
|
};
|
|
await bulkUpdateSparkTransactions([tx]);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Handle swaps marked as executing (potential orphaned state from app closure)
|
|
if (details.swapExecuting) {
|
|
const lastAttempt = details.lastSwapAttempt || 0;
|
|
const timeSinceAttempt = Date.now() - lastAttempt;
|
|
const EXECUTION_TIMEOUT = 2 * 60 * 1000; // 2 minutes
|
|
|
|
console.log(
|
|
`[Pending Swaps Monitor] Swap ${sparkID} marked as executing`,
|
|
);
|
|
|
|
if (isFlashnetTransfer(finalSparkID)) {
|
|
console.log(
|
|
`[Pending Swaps Monitor] Swap ${sparkID} was already completed, marking as such`,
|
|
);
|
|
await updateSparkTransactionDetails(sparkID, {
|
|
completedSwaptoUSD: true,
|
|
swapExecuting: false,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// If it's been executing for too long, it's orphaned - reset and retry
|
|
if (timeSinceAttempt > EXECUTION_TIMEOUT) {
|
|
console.log(
|
|
`[Pending Swaps Monitor] Swap ${sparkID} execution timeout, resetting and retrying`,
|
|
);
|
|
await updateSparkTransactionDetails(sparkID, {
|
|
swapExecuting: false,
|
|
swapRetryCount: currentRetries + 1,
|
|
});
|
|
} else {
|
|
// Still within execution window, skip for now
|
|
console.log(
|
|
`[Pending Swaps Monitor] Swap ${sparkID} still within execution window`,
|
|
);
|
|
continue;
|
|
}
|
|
} else {
|
|
await updateSparkTransactionDetails(sparkID, {
|
|
swapRetryCount: currentRetries + 1,
|
|
});
|
|
}
|
|
|
|
console.log(`[Pending Swaps Monitor] Retrying swap ${sparkID}`);
|
|
|
|
// Trigger the swap
|
|
handleAutoSwap(sparkID);
|
|
|
|
// Add delay between retries to avoid overwhelming the system
|
|
await new Promise(res => setTimeout(res, 1000));
|
|
}
|
|
} catch (err) {
|
|
console.error('[Pending Swaps Monitor] error:', err);
|
|
}
|
|
};
|
|
|
|
const startPendingSwapsMonitor = () => {
|
|
if (swapMonitorIntervalRef.current) return;
|
|
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
runPendingSwapsMonitor(); // immediate pass
|
|
|
|
swapMonitorIntervalRef.current = setInterval(async () => {
|
|
// Check if auth has changed
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Pending Swaps Monitor] Auth changed, stopping monitor');
|
|
stopPendingSwapsMonitor();
|
|
return;
|
|
}
|
|
|
|
await runPendingSwapsMonitor();
|
|
}, SWAP_MONITOR_INTERVAL);
|
|
};
|
|
|
|
const stopPendingSwapsMonitor = () => {
|
|
if (!swapMonitorIntervalRef.current) return;
|
|
|
|
clearInterval(swapMonitorIntervalRef.current);
|
|
swapMonitorIntervalRef.current = null;
|
|
};
|
|
|
|
const runRefundMonitor = async () => {
|
|
try {
|
|
// Hard guards — never trust effects alone
|
|
if (appState !== 'active') return;
|
|
if (!sparkInformation.didConnectToFlashnet) return;
|
|
if (!currentWalletMnemoincRef.current) return;
|
|
|
|
const refundableTransfers = await listClawbackableTransfers(
|
|
currentWalletMnemoincRef.current,
|
|
50,
|
|
);
|
|
|
|
if (
|
|
!refundableTransfers?.didWork ||
|
|
!refundableTransfers.resposne?.transfers.length
|
|
)
|
|
return;
|
|
|
|
const uniqueTransfers = Array.from(
|
|
new Map(
|
|
refundableTransfers.resposne.transfers.map(t => [t.id, t]),
|
|
).values(),
|
|
);
|
|
|
|
for (const transfer of uniqueTransfers) {
|
|
const transferId = transfer.id;
|
|
|
|
const eligibility = await checkClawbackEligibility(
|
|
currentWalletMnemoincRef.current,
|
|
transferId,
|
|
);
|
|
|
|
if (eligibility.didWork && eligibility.response) {
|
|
console.warn('[Flashnet Refund Monitor] Refundable', transferId);
|
|
|
|
const response = await requestManualClawback(
|
|
currentWalletMnemoincRef.current,
|
|
transferId,
|
|
transfer.lpIdentityPublicKey,
|
|
);
|
|
|
|
if (response.didWork && response.accepted) {
|
|
await new Promise(res => setTimeout(res, 500));
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[Flashnet Refund Monitor] error:', err);
|
|
}
|
|
};
|
|
|
|
const startRefundMonitor = () => {
|
|
if (refundMonitorIntervalRef.current) return;
|
|
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
refundMonitorIntervalRef.current = setInterval(async () => {
|
|
// Check if auth has changed
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Refund Monitor] Auth changed, stopping monitor');
|
|
stopRefundMonitor();
|
|
return;
|
|
}
|
|
|
|
await runRefundMonitor();
|
|
}, REFUND_MONITOR_INTERVAL);
|
|
};
|
|
|
|
const stopRefundMonitor = () => {
|
|
if (!refundMonitorIntervalRef.current) return;
|
|
|
|
clearInterval(refundMonitorIntervalRef.current);
|
|
refundMonitorIntervalRef.current = null;
|
|
};
|
|
|
|
// Set up the auto-swap event listener
|
|
useEffect(() => {
|
|
flashnetAutoSwapsEventListener.on(
|
|
HANDLE_FLASHNET_AUTO_SWAP,
|
|
handleAutoSwap,
|
|
);
|
|
|
|
loadSavedTransferIds();
|
|
|
|
return () => {
|
|
flashnetAutoSwapsEventListener.off(
|
|
HANDLE_FLASHNET_AUTO_SWAP,
|
|
handleAutoSwap,
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
appState === 'active' &&
|
|
sparkInformation.didConnectToFlashnet &&
|
|
poolInfo?.lpPublicKey
|
|
) {
|
|
startPendingSwapsMonitor();
|
|
} else {
|
|
stopPendingSwapsMonitor();
|
|
}
|
|
|
|
return stopPendingSwapsMonitor;
|
|
}, [appState, sparkInformation.didConnectToFlashnet, poolInfo?.lpPublicKey]);
|
|
|
|
useEffect(() => {
|
|
let refundMonitorTimeout;
|
|
if (appState === 'active' && sparkInformation.didConnectToFlashnet) {
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
refundMonitorTimeout = setTimeout(() => {
|
|
// Check if auth has changed before executing
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Refund Monitor] Auth changed, skipping initial run');
|
|
return;
|
|
}
|
|
runRefundMonitor();
|
|
}, 10_000);
|
|
|
|
startRefundMonitor();
|
|
} else {
|
|
stopRefundMonitor();
|
|
}
|
|
|
|
return () => {
|
|
if (refundMonitorTimeout) {
|
|
clearTimeout(refundMonitorTimeout);
|
|
}
|
|
stopRefundMonitor();
|
|
};
|
|
}, [appState, sparkInformation.didConnectToFlashnet]);
|
|
|
|
useEffect(() => {
|
|
const INITIAL_RETRY_DELAY = 5_000; // 5 seconds
|
|
const MAX_RETRY_DELAY = 120_000; // 2 minutes
|
|
|
|
const attemptFlashnetConnection = async () => {
|
|
try {
|
|
if (
|
|
sparkInformation.didConnect === true &&
|
|
!sparkInformation.didConnectToFlashnet &&
|
|
appState === 'active' &&
|
|
currentWalletMnemoincRef.current
|
|
) {
|
|
console.log(
|
|
`[Flashnet Retry] Attempting to initialize Flashnet... (delay: ${flashnetRetryDelayRef.current}ms)`,
|
|
);
|
|
|
|
const result = await initializeFlashnet(
|
|
currentWalletMnemoincRef.current,
|
|
);
|
|
|
|
if (result === true) {
|
|
console.log('[Flashnet Retry] Successfully initialized Flashnet');
|
|
setSparkInformation(prev => ({
|
|
...prev,
|
|
didConnectToFlashnet: true,
|
|
}));
|
|
|
|
// Reset delay on success
|
|
flashnetRetryDelayRef.current = INITIAL_RETRY_DELAY;
|
|
|
|
if (flashnetRetryIntervalRef.current) {
|
|
clearTimeout(flashnetRetryIntervalRef.current);
|
|
flashnetRetryIntervalRef.current = null;
|
|
}
|
|
} else {
|
|
console.warn(
|
|
`[Flashnet Retry] Failed to initialize, will retry in ${
|
|
flashnetRetryDelayRef.current / 1000
|
|
}s...`,
|
|
);
|
|
|
|
// Schedule next retry with exponential backoff
|
|
if (flashnetRetryIntervalRef.current) {
|
|
clearTimeout(flashnetRetryIntervalRef.current);
|
|
}
|
|
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
flashnetRetryIntervalRef.current = setTimeout(() => {
|
|
// Check if auth has changed
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Flashnet Retry] Auth changed, aborting retry');
|
|
return;
|
|
}
|
|
attemptFlashnetConnection();
|
|
}, flashnetRetryDelayRef.current);
|
|
|
|
// Double the delay for next time, capped at MAX_RETRY_DELAY
|
|
flashnetRetryDelayRef.current = Math.min(
|
|
flashnetRetryDelayRef.current * 2,
|
|
MAX_RETRY_DELAY,
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[Flashnet Retry] Error during initialization:', error);
|
|
|
|
// Schedule retry on error as well
|
|
if (flashnetRetryIntervalRef.current) {
|
|
clearTimeout(flashnetRetryIntervalRef.current);
|
|
}
|
|
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
flashnetRetryIntervalRef.current = setTimeout(() => {
|
|
// Check if auth has changed
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log(
|
|
'[Flashnet Retry] Auth changed, aborting retry after error',
|
|
);
|
|
return;
|
|
}
|
|
attemptFlashnetConnection();
|
|
}, flashnetRetryDelayRef.current);
|
|
|
|
flashnetRetryDelayRef.current = Math.min(
|
|
flashnetRetryDelayRef.current * 2,
|
|
MAX_RETRY_DELAY,
|
|
);
|
|
}
|
|
};
|
|
|
|
if (
|
|
sparkInformation.didConnect === true &&
|
|
!sparkInformation.didConnectToFlashnet &&
|
|
appState === 'active'
|
|
) {
|
|
// Reset delay when starting fresh
|
|
flashnetRetryDelayRef.current = INITIAL_RETRY_DELAY;
|
|
|
|
// Immediate first attempt
|
|
attemptFlashnetConnection();
|
|
} else {
|
|
// Clean up timeout if conditions no longer met
|
|
if (flashnetRetryIntervalRef.current) {
|
|
clearTimeout(flashnetRetryIntervalRef.current);
|
|
flashnetRetryIntervalRef.current = null;
|
|
}
|
|
|
|
// Reset delay when stopping
|
|
flashnetRetryDelayRef.current = INITIAL_RETRY_DELAY;
|
|
}
|
|
|
|
return () => {
|
|
if (flashnetRetryIntervalRef.current) {
|
|
clearTimeout(flashnetRetryIntervalRef.current);
|
|
flashnetRetryIntervalRef.current = null;
|
|
}
|
|
};
|
|
}, [
|
|
sparkInformation.didConnect,
|
|
sparkInformation.didConnectToFlashnet,
|
|
appState,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
// Stop any existing interval first
|
|
if (poolIntervalRef.current) {
|
|
clearInterval(poolIntervalRef.current);
|
|
poolIntervalRef.current = null;
|
|
}
|
|
|
|
// Only start polling when conditions are correct
|
|
if (!sparkInformation.didConnectToFlashnet) return;
|
|
if (appState !== 'active') return;
|
|
|
|
// Start interval
|
|
const capturedAuthKey = authResetKeyRef.current;
|
|
|
|
poolIntervalRef.current = setInterval(() => {
|
|
// Check if auth has changed
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Pool Refresh] Auth changed, stopping refresh');
|
|
if (poolIntervalRef.current) {
|
|
clearInterval(poolIntervalRef.current);
|
|
poolIntervalRef.current = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
refreshPool();
|
|
}, 30_000);
|
|
|
|
// Run immediately on activation
|
|
const refreshPoolTimeout = setTimeout(() => {
|
|
// Check if auth has changed
|
|
if (capturedAuthKey !== authResetKeyRef.current) {
|
|
console.log('[Pool Refresh] Auth changed, skipping initial refresh');
|
|
return;
|
|
}
|
|
refreshPool();
|
|
}, 2000);
|
|
|
|
return () => {
|
|
if (refreshPoolTimeout) {
|
|
clearTimeout(refreshPoolTimeout);
|
|
}
|
|
|
|
if (poolIntervalRef.current) {
|
|
clearInterval(poolIntervalRef.current);
|
|
poolIntervalRef.current = null;
|
|
}
|
|
};
|
|
}, [appState, sparkInformation.didConnectToFlashnet]);
|
|
|
|
useEffect(() => {
|
|
if (!sparkInformation.didConnectToFlashnet) return;
|
|
async function getLimits() {
|
|
const [usdLimits, bitconLimits] = await Promise.all([
|
|
minFlashnetSwapAmounts(
|
|
currentWalletMnemoincRef.current,
|
|
USD_ASSET_ADDRESS,
|
|
),
|
|
minFlashnetSwapAmounts(
|
|
currentWalletMnemoincRef.current,
|
|
BTC_ASSET_ADDRESS,
|
|
),
|
|
]);
|
|
if (usdLimits.didWork && bitconLimits.didWork) {
|
|
setSwapLimits({
|
|
usd: parseFloat(
|
|
(
|
|
(Number(usdLimits.assetData) / 1000000) *
|
|
SEND_AMOUNT_INCREASE_BUFFER
|
|
).toFixed(2),
|
|
),
|
|
bitcoin: Math.round(
|
|
Number(bitconLimits.assetData) * SEND_AMOUNT_INCREASE_BUFFER,
|
|
),
|
|
});
|
|
}
|
|
}
|
|
getLimits();
|
|
}, [sparkInformation.didConnectToFlashnet]);
|
|
|
|
const swapUSDPriceDollars = useMemo(() => {
|
|
return currentPriceAinBToPriceDollars(poolInfo?.currentPriceAInB);
|
|
}, [poolInfo?.currentPriceAInB]);
|
|
|
|
// Clean up all intervals and timeouts when auth resets
|
|
useEffect(() => {
|
|
if (isInitialLoad.current) {
|
|
isInitialLoad.current = false;
|
|
return;
|
|
}
|
|
console.log('[Flashnet] Auth reset detected, cleaning up all processes');
|
|
|
|
stopRefundMonitor();
|
|
stopPendingSwapsMonitor();
|
|
|
|
if (poolIntervalRef.current) {
|
|
clearInterval(poolIntervalRef.current);
|
|
poolIntervalRef.current = null;
|
|
}
|
|
|
|
if (flashnetRetryIntervalRef.current) {
|
|
clearTimeout(flashnetRetryIntervalRef.current);
|
|
flashnetRetryIntervalRef.current = null;
|
|
}
|
|
|
|
if (retryTimeoutRef.current) {
|
|
clearTimeout(retryTimeoutRef.current);
|
|
retryTimeoutRef.current = null;
|
|
}
|
|
|
|
// Clear triggered swaps set
|
|
triggeredSwapsRef.current.clear();
|
|
|
|
// Reset retry delay
|
|
flashnetRetryDelayRef.current = 5_000;
|
|
}, [authResetkey]);
|
|
|
|
const contextValue = useMemo(() => {
|
|
return {
|
|
poolInfo,
|
|
togglePoolInfo,
|
|
poolInfoRef: poolInfoRef.current,
|
|
swapLimits,
|
|
swapUSDPriceDollars,
|
|
};
|
|
}, [poolInfo, togglePoolInfo, swapLimits, swapUSDPriceDollars]);
|
|
|
|
return (
|
|
<FlashnetContext.Provider value={contextValue}>
|
|
{children}
|
|
</FlashnetContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const useFlashnet = () => useContext(FlashnetContext);
|