* 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
416 lines
12 KiB
JavaScript
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);
|
|
};
|