* get fresh balance on load * improve balance version matchup + unbounded debounce * bump to 50 * using sdk balance event * bumping sdk * fixing minor bugs * remove uneeded log * fixing border radius * lower to 50 txs
285 lines
8.2 KiB
JavaScript
285 lines
8.2 KiB
JavaScript
import { getSparkBalance } from './spark';
|
|
import { fullRestoreSparkState } from './spark/restore';
|
|
|
|
/**
|
|
* Reads the balance with a hard timeout. getSparkBalance can hang indefinitely
|
|
* when the underlying WebView request's timeout is neutered (it returns early
|
|
* without settling while the app is backgrounded). An unsettled read parks the
|
|
* supervisor's await forever and wedges the whole balance lane, so we race the
|
|
* read against a timer that resolves to a benign {didWork:false}.
|
|
* @param {string} mnemonic
|
|
* @param {number} timeoutMs
|
|
* @returns {Promise<{didWork: boolean, balance?: number, tokensObj?: object}>}
|
|
*/
|
|
export const getBalanceWithTimeout = async (mnemonic, timeoutMs = 15000) => {
|
|
let timer = null;
|
|
console.log('getting balance with timeout');
|
|
try {
|
|
return await Promise.race([
|
|
getSparkBalance(mnemonic),
|
|
new Promise(resolve => {
|
|
timer = setTimeout(() => resolve({ didWork: false }), timeoutMs);
|
|
}),
|
|
]);
|
|
} catch (err) {
|
|
console.log('err', err);
|
|
return { didWork: false };
|
|
} finally {
|
|
if (timer) clearTimeout(timer);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generic polling utility with exponential backoff
|
|
* @param {Object} config - Configuration object
|
|
* @param {Function} config.pollFn - Async function to poll (receives delayIndex)
|
|
* @param {Function} config.shouldContinue - Function that returns boolean to continue polling
|
|
* @param {Function} config.onUpdate - Callback when poll succeeds with new data
|
|
* @param {Array<number>} config.delays - Array of delay intervals in ms
|
|
* @param {AbortController} config.abortController - Optional abort controller
|
|
* @param {Function} config.validateResult - Function to validate if result should stop polling
|
|
* @returns {Object} - Returns cleanup function and current state
|
|
*/
|
|
export const createPollingManager = ({
|
|
pollFn,
|
|
shouldContinue,
|
|
onUpdate,
|
|
delays = [1000, 2000, 5000, 15000],
|
|
abortController,
|
|
validateResult = () => true,
|
|
initialBalance,
|
|
}) => {
|
|
let timeoutRef = null;
|
|
let previousResult = initialBalance || null;
|
|
|
|
const cleanup = () => {
|
|
if (timeoutRef) {
|
|
clearTimeout(timeoutRef);
|
|
timeoutRef = null;
|
|
}
|
|
};
|
|
|
|
const poll = async (delayIndex = 0, resolve, reject) => {
|
|
try {
|
|
// Check abort conditions
|
|
if (abortController?.signal?.aborted || !shouldContinue()) {
|
|
console.log('Polling stopped:', {
|
|
aborted: abortController?.signal?.aborted,
|
|
shouldContinue: shouldContinue(),
|
|
});
|
|
cleanup();
|
|
resolve({ success: false, reason: 'aborted' });
|
|
return;
|
|
}
|
|
|
|
// Check if we've exhausted all delays
|
|
if (delayIndex >= delays.length) {
|
|
console.log('Polling completed after all retries');
|
|
cleanup();
|
|
resolve({
|
|
success: false,
|
|
reason: 'max_retries',
|
|
result: previousResult,
|
|
});
|
|
return;
|
|
}
|
|
|
|
timeoutRef = setTimeout(async () => {
|
|
let globalResult;
|
|
try {
|
|
// Re-check abort conditions before executing
|
|
if (abortController?.signal?.aborted || !shouldContinue()) {
|
|
cleanup();
|
|
resolve({
|
|
success: false,
|
|
reason: 'aborted',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Execute the poll function
|
|
globalResult = await pollFn(delayIndex);
|
|
|
|
// Check abort AFTER async operation
|
|
if (abortController?.signal?.aborted || !shouldContinue()) {
|
|
cleanup();
|
|
resolve({
|
|
success: false,
|
|
reason: 'aborted',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate if we should continue
|
|
if (validateResult(globalResult, previousResult)) {
|
|
// Call update callback
|
|
if (onUpdate) {
|
|
await onUpdate(globalResult, delayIndex);
|
|
}
|
|
|
|
// Success - stop polling
|
|
cleanup();
|
|
resolve({ success: true, result: globalResult });
|
|
return;
|
|
}
|
|
|
|
// Continue to next delay
|
|
poll(delayIndex + 1, resolve, reject);
|
|
} catch (err) {
|
|
console.log('Error in polling iteration, continuing:', err);
|
|
poll(delayIndex + 1, resolve, reject);
|
|
} finally {
|
|
if (
|
|
globalResult != null &&
|
|
(previousResult == null || globalResult !== previousResult)
|
|
) {
|
|
// Only update if balance changes
|
|
previousResult = globalResult;
|
|
}
|
|
}
|
|
}, delays[delayIndex]);
|
|
} catch (err) {
|
|
console.log('Error in poll setup:', err);
|
|
cleanup();
|
|
reject(err);
|
|
}
|
|
};
|
|
|
|
return {
|
|
start: () => {
|
|
if (abortController?.signal?.aborted) {
|
|
console.log('Poller: Already aborted before start');
|
|
return Promise.resolve({
|
|
success: false,
|
|
reason: 'aborted',
|
|
result: previousResult,
|
|
});
|
|
}
|
|
|
|
return new Promise((resolve, reject) => poll(0, resolve, reject));
|
|
},
|
|
cleanup,
|
|
};
|
|
};
|
|
|
|
export const createBalancePoller = (
|
|
mnemonic,
|
|
currentMnemonicRef,
|
|
abortController,
|
|
onBalanceUpdate,
|
|
initialBalance,
|
|
customConfims = 2,
|
|
) => {
|
|
let hasIncreasedAtLeastOnce = false;
|
|
let sameValueIndex = 0;
|
|
|
|
// Wrap initialBalance in the expected format
|
|
const wrappedInitialBalance =
|
|
typeof initialBalance === 'number'
|
|
? { didWork: true, balance: initialBalance, tokensObj: {} }
|
|
: initialBalance;
|
|
|
|
return createPollingManager({
|
|
pollFn: async () => {
|
|
return await getBalanceWithTimeout(mnemonic);
|
|
},
|
|
shouldContinue: () => mnemonic === currentMnemonicRef.current,
|
|
validateResult: (newResult, previousResult) => {
|
|
console.log(
|
|
'Validate:',
|
|
{
|
|
newBalance: newResult?.balance,
|
|
prevBalance: previousResult?.balance,
|
|
},
|
|
{ hasIncreasedAtLeastOnce, sameValueIndex },
|
|
);
|
|
|
|
if (!newResult?.didWork) {
|
|
console.log('New result invalid');
|
|
return false;
|
|
}
|
|
|
|
// previousResult might be null on first run
|
|
if (!previousResult?.didWork) {
|
|
console.log('Previous result invalid, continuing');
|
|
return false;
|
|
}
|
|
|
|
const newBalance = Number(newResult.balance);
|
|
const previousBalance = Number(previousResult.balance);
|
|
|
|
if (Number.isNaN(newBalance) || Number.isNaN(previousBalance)) {
|
|
return false;
|
|
}
|
|
|
|
if (newBalance !== previousBalance) {
|
|
console.log('Balance changed — resetting');
|
|
hasIncreasedAtLeastOnce = true;
|
|
sameValueIndex = 0;
|
|
return false;
|
|
}
|
|
|
|
sameValueIndex++;
|
|
|
|
// if (
|
|
// (hasIncreasedAtLeastOnce && sameValueIndex >= customConfims + 1) ||
|
|
// (!hasIncreasedAtLeastOnce && sameValueIndex >= customConfims)
|
|
// ) {
|
|
// return true;
|
|
// }
|
|
if (sameValueIndex >= customConfims) return true;
|
|
|
|
return false;
|
|
},
|
|
onUpdate: async (balanceResult, delayIndex) => {
|
|
console.log(
|
|
`Balance updated to ${balanceResult.balance} after ${delayIndex} attempts`,
|
|
);
|
|
await onBalanceUpdate(balanceResult);
|
|
},
|
|
abortController,
|
|
delays: [
|
|
1000, 1500, 1500, 1500, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000,
|
|
2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000,
|
|
],
|
|
initialBalance: wrappedInitialBalance,
|
|
});
|
|
};
|
|
|
|
export const createRestorePoller = (
|
|
mnemonic,
|
|
isSendingPayment,
|
|
currentMnemonicRef,
|
|
abortController,
|
|
onRestoreComplete,
|
|
sparkInfo,
|
|
sendWebViewRequest,
|
|
) => {
|
|
return createPollingManager({
|
|
pollFn: async delayIndex => {
|
|
const result = await fullRestoreSparkState({
|
|
sparkAddress: sparkInfo.sparkAddress,
|
|
batchSize: 2,
|
|
isSendingPayment: isSendingPayment,
|
|
mnemonic,
|
|
identityPubKey: sparkInfo.identityPubKey,
|
|
sendWebViewRequest,
|
|
isInitialRestore: false,
|
|
});
|
|
return result;
|
|
},
|
|
shouldContinue: () => mnemonic === currentMnemonicRef.current,
|
|
validateResult: result => {
|
|
return typeof result === 'number' && result > 0;
|
|
},
|
|
onUpdate: (result, delayIndex) => {
|
|
console.log(
|
|
`Restore completed after ${delayIndex + 1} attempts with ${result} txs`,
|
|
);
|
|
onRestoreComplete(result);
|
|
},
|
|
abortController,
|
|
delays: [0, 2000, 5000],
|
|
});
|
|
};
|