Files
com.blitzwallet/context-store/rootstockSwapContext.js
T
Blake KaufmanandGitHub b1a58e23b2 Reload wallet if backgrounded (#290)
* creating centralized location to handle stale background state

* adding global background threashold

* creating auth context

* disconnecting from liquid node during force auth

* reset did get to homepage variable

* fix crash if globalContactsInformation.myProfile is not set

* resetting custody account settings

* clearing keys information on reset

* remove roostock swap listener on reset

* resetting webview context on stale auth

* adding auth reset to spark context

* only load user info once per session

* only open tables once per session

* making sure to recheck login status to fix issue with initial load

* adding large heap flag

* fixing sql table open bug

* making sure didLoadUserSettings runs on app open

* fixing unqiueName being null bug

* fixing race condition causing switch to naive spark version

* misc changes
2025-12-05 09:35:51 -05:00

416 lines
12 KiB
JavaScript

import React, {
createContext,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
deleteSwapById,
getSwapById,
loadSwaps,
updateSwap,
} from '../app/functions/boltz/rootstock/swapDb';
import {
claimRootstockReverseSwap,
refundRootstockSubmarineSwap,
} from '../app/functions/boltz/rootstock/claims';
import {
executeSubmarineSwap,
lockSubmarineSwap,
} from '../app/functions/boltz/rootstock/submarineSwap';
import { useKeysContext } from './keys';
import { useAppStatus } from './appStatus';
import {
getRoostockProviderEndpoint,
rootstockEnvironment,
} from '../app/functions/boltz/rootstock';
import { JsonRpcProvider, Wallet } from 'ethers';
import { getBoltzWsUrl } from '../app/functions/boltz/boltzEndpoitns';
import { useSparkWallet } from './sparkContext';
import { useWebView } from './webViewContext';
import { useAuthContext } from './authContext';
export const RootstockSwapContext = createContext();
export const RootstockSwapProvider = ({ children }) => {
const { sendWebViewRequest } = useWebView();
const { authResetkey } = useAuthContext();
const { sparkInformation } = useSparkWallet();
const { accountMnemoinc } = useKeysContext();
const { didGetToHomepage, minMaxLiquidSwapAmounts } = useAppStatus();
const subscribedIdsRef = useRef(new Set());
const activeSwapIdsRef = useRef(new Set()); // Track active swaps
const [signer, setSigner] = useState(null);
const [pendingNavigation, setPendingNavigation] = useState(null);
const isInitialRender = useRef(true);
const wsRef = useRef(null);
const intervalRef = useRef(null);
const timeoutRef = useRef(null);
const cleanupDebounceRef = useRef(null);
const didRunSignerCreation = useRef(null);
// Helper function to check if a swap is in a terminal state
const isSwapCompleted = status => {
return [
'transaction.claimed',
'transaction.claim.pending',
'swap.expired',
'invoice.failedToPay',
'transaction.lockupFailed',
].includes(status);
};
// Smart cleanup function that only clears if no active swaps
const cleanupRootstockListener = useCallback(() => {
console.log('Running event listeners cleanup', {
activeSwaps: Array.from(activeSwapIdsRef.current),
});
// If there are active swaps, don't cleanup
if (activeSwapIdsRef.current.size > 0) {
console.log('Skipping cleanup - active swaps in progress');
return false;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (cleanupDebounceRef.current) {
clearTimeout(cleanupDebounceRef.current);
cleanupDebounceRef.current = null;
}
if (wsRef.current) {
// Remove event listeners to prevent the assertion error
wsRef.current.onopen = null;
wsRef.current.onmessage = null;
wsRef.current.onerror = null;
wsRef.current.onclose = null;
// Only close if not already closed/closing
if (
wsRef.current.readyState === WebSocket.OPEN ||
wsRef.current.readyState === WebSocket.CONNECTING ||
wsRef.current.readyState === WebSocket.CLOSING
) {
wsRef.current.close();
}
wsRef.current = null;
}
subscribedIdsRef.current.clear();
activeSwapIdsRef.current.clear();
return true;
}, []);
// Debounced cleanup function to prevent rapid fire cleanup attempts
const debouncedCleanup = useCallback(() => {
if (cleanupDebounceRef.current) {
clearTimeout(cleanupDebounceRef.current);
}
cleanupDebounceRef.current = setTimeout(() => {
cleanupRootstockListener();
cleanupDebounceRef.current = null;
}, 1000); // 1s debounce
}, [cleanupRootstockListener]);
const provider = useMemo(() => {
return new JsonRpcProvider(
getRoostockProviderEndpoint(rootstockEnvironment),
);
}, []);
const createSigner = useCallback(() => {
try {
console.log('PROCESSS WALLET', new Date().getTime());
const wallet = Wallet.fromPhrase(accountMnemoinc);
console.log('PROCESSS WALLET 2', new Date().getTime());
const connectedSigner = wallet.connect(provider);
console.log('PROCESSS WALLET 3', new Date().getTime());
setSigner(connectedSigner);
return true;
} catch (error) {
console.error('Error creating wallet:', error);
setSigner(null);
return false;
}
}, [accountMnemoinc, provider]);
const subscribeToIds = ids => {
if (wsRef.current && wsRef.current.readyState === 1 && ids.length > 0) {
console.log('sending ids to websocket', ids);
ids.forEach(id => {
wsRef.current.send(
JSON.stringify({
op: 'subscribe',
channel: 'swap.update',
args: [id],
}),
);
});
return true;
}
return false;
};
const loadRootstockSwaps = async () => {
let swaps = await loadSwaps();
if (!swaps.length) {
const swap = await executeSubmarineSwap(
accountMnemoinc,
minMaxLiquidSwapAmounts,
provider,
signer,
sendWebViewRequest,
);
if (swap) swaps.push(swap);
}
console.log('saved swaps', swaps);
return swaps || [];
};
// Runs after every interval or ws update
const scheduleCleanup = durationMs => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
debouncedCleanup();
// If still active, schedule again
if (activeSwapIdsRef.current.size > 0) {
scheduleCleanup(durationMs);
}
}, durationMs);
};
// Load swaps from DB and subscribe
const loadAndSubscribeSwaps = async (force = false, swaps) => {
const newSwaps = swaps;
if (!newSwaps || !newSwaps.length) {
// No new swaps to process, but existing active swaps may still be running
// Don't clear anything here - let the interval and message handlers manage state
return [];
}
// Update active swaps tracking
newSwaps.forEach(swap => {
if (!isSwapCompleted(swap.status)) {
activeSwapIdsRef.current.add(swap.id);
}
});
const newIds = newSwaps
.map(s => s.id)
.filter(id => !subscribedIdsRef.current.has(id));
if (newIds.length > 0 || force) {
const response = subscribeToIds(newIds);
if (response) newIds.forEach(id => subscribedIdsRef.current.add(id));
}
};
useEffect(() => {
if (!sparkInformation.identityPubKey) return;
if (didRunSignerCreation.current) return;
didRunSignerCreation.current = true;
createSigner();
}, [sparkInformation.identityPubKey]);
useEffect(() => {
if (!signer) return;
// Open websocket once
startRootstockEventListener({ durationMs: 30000, intervalMs: 15000 });
}, [signer]);
const startRootstockEventListener = useCallback(
async ({ durationMs = 60000, intervalMs = 20000 } = {}) => {
let swaps = await loadRootstockSwaps();
// Check if we should open a new WebSocket or use existing one
const hasActiveSwaps = activeSwapIdsRef.current.size > 0;
if (hasActiveSwaps && wsRef.current) {
console.log(
'WebSocket already active, adding new swaps to existing connection',
);
// Just subscribe to new swaps on existing connection
loadAndSubscribeSwaps(false, swaps);
return;
}
// Clean up any existing connections before opening new one
cleanupRootstockListener();
// Initialize active swaps tracking
swaps.forEach(swap => {
if (!isSwapCompleted(swap.status)) {
activeSwapIdsRef.current.add(swap.id);
}
});
if (swaps.length) {
// Open websocket
const ws = new WebSocket(
`${getBoltzWsUrl(rootstockEnvironment)}/v2/ws`,
);
wsRef.current = ws;
ws.onopen = event => {
console.log('WebSocket opened');
loadAndSubscribeSwaps(true, swaps);
};
ws.onmessage = async event => {
try {
const msg = JSON.parse(event.data);
if (msg.event !== 'update') return;
const swapId = msg.args[0].id;
const status = msg.args[0].status;
console.log(`Swap ${swapId} updated: ${status}`);
// Find swap type from state
const swapResponse = await getSwapById(swapId);
console.log(swapResponse, 'saved swap in history');
if (!swapResponse) return;
const [swap] = swapResponse;
console.log(swap, 'DESTRUCTED SWAP');
// Perform actions based on type + status
if (swap.type === 'reverse' && status === 'transaction.confirmed') {
await claimRootstockReverseSwap(swap, signer);
}
if (swap.type === 'submarine') {
if (status == 'invoice.set') {
await lockSubmarineSwap(swap, signer);
setPendingNavigation(true);
}
if (
[
'invoice.failedToPay',
'swap.expired',
'transaction.lockupFailed',
].includes(status)
) {
await updateSwap(swapId, { didSwapFail: true });
await refundRootstockSubmarineSwap(swap, signer);
// Remove from active swaps when it fails/expires
activeSwapIdsRef.current.delete(swapId);
}
if (
status == 'transaction.claimed' ||
status === 'transaction.claim.pending'
) {
await deleteSwapById(swapId);
// Remove from active swaps when completed
activeSwapIdsRef.current.delete(swapId);
}
}
debouncedCleanup();
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
ws.onerror = event => {
console.error('WebSocket error:', event);
// Don't try to access 'this' or other properties that might cause issues
};
ws.onclose = event => {
console.log('WebSocket closed:', event.code, event.reason);
// Clean up subscribed IDs when connection closes
subscribedIdsRef.current.clear();
// Clear active swaps since we can't monitor them without WebSocket
activeSwapIdsRef.current.clear();
};
}
intervalRef.current = setInterval(async () => {
const freshSwaps = await loadRootstockSwaps();
if (freshSwaps && freshSwaps.length > 0) {
// Update active swaps based on current status
const currentActiveSwaps = freshSwaps.filter(
swap => !isSwapCompleted(swap.status),
);
// Update the active swaps ref
activeSwapIdsRef.current.clear();
currentActiveSwaps.forEach(swap => {
activeSwapIdsRef.current.add(swap.id);
});
loadAndSubscribeSwaps(false, freshSwaps);
} else {
console.log('No swaps found in interval check');
// Clear active swaps since no swaps exist
activeSwapIdsRef.current.clear();
}
}, intervalMs);
scheduleCleanup(durationMs);
},
[
cleanupRootstockListener,
debouncedCleanup,
signer,
accountMnemoinc,
minMaxLiquidSwapAmounts,
provider,
],
);
useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
cleanupRootstockListener();
didRunSignerCreation.current = false;
setSigner(null);
}, [authResetkey]);
const contextValue = useMemo(() => {
return {
provider,
signer,
createSigner,
startRootstockEventListener,
pendingNavigation,
setPendingNavigation,
};
}, [
provider,
signer,
createSigner,
startRootstockEventListener,
pendingNavigation,
setPendingNavigation,
]);
return (
<RootstockSwapContext.Provider value={contextValue}>
{children}
</RootstockSwapContext.Provider>
);
};
export const useRootstockProvider = () => {
return React.useContext(RootstockSwapContext);
};