Remove txs from memory (#849)
* memorizing context values * reduce stored txs in memory * improve db lookup * dismiss keyboard before going back
This commit is contained in:
@@ -187,3 +187,110 @@ describe('Spark transaction bulk update guards', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPaidSparkLightningInvoice', () => {
|
||||
it('asks SQLite for a single matching lightning invoice instead of loading transactions', async () => {
|
||||
const mockDb = createMockDb();
|
||||
mockDb.getAllAsync.mockResolvedValue([{ found: 1 }]);
|
||||
const { hasPaidSparkLightningInvoice } = loadTransactionsModule(mockDb);
|
||||
|
||||
const result = await hasPaidSparkLightningInvoice(' lnbc123 ');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockDb.getAllAsync).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [sql, params] = mockDb.getAllAsync.mock.calls[0];
|
||||
expect(sql).toContain('SELECT 1 as found');
|
||||
expect(sql).toContain("paymentType = 'lightning'");
|
||||
expect(sql).toContain("TRIM(json_extract(details, '$.address')) = ?");
|
||||
expect(sql).toContain('LIMIT 1');
|
||||
expect(sql).not.toContain('SELECT *');
|
||||
expect(params).toEqual(['lnbc123']);
|
||||
});
|
||||
|
||||
it('returns false without opening the database for empty invoice addresses', async () => {
|
||||
const mockDb = createMockDb();
|
||||
const { hasPaidSparkLightningInvoice } = loadTransactionsModule(mockDb);
|
||||
|
||||
await expect(hasPaidSparkLightningInvoice(' ')).resolves.toBe(false);
|
||||
|
||||
expect(mockOpenDatabaseAsync).not.toHaveBeenCalled();
|
||||
expect(mockDb.getAllAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSparkTransactionBySparkId', () => {
|
||||
it('returns the raw row for a single sparkID/accountId lookup', async () => {
|
||||
const mockDb = createMockDb();
|
||||
const row = {
|
||||
sparkID: 'btc-txid',
|
||||
accountId: 'identity-pubkey',
|
||||
paymentStatus: 'pending',
|
||||
paymentType: 'bitcoin',
|
||||
details: JSON.stringify({ amount: 2500 }),
|
||||
};
|
||||
mockDb.getAllAsync.mockResolvedValue([row]);
|
||||
const { getSparkTransactionBySparkId } = loadTransactionsModule(mockDb);
|
||||
|
||||
const result = await getSparkTransactionBySparkId(
|
||||
' btc-txid ',
|
||||
'identity-pubkey',
|
||||
);
|
||||
|
||||
expect(result).toBe(row);
|
||||
expect(mockDb.getAllAsync).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [sql, params] = mockDb.getAllAsync.mock.calls[0];
|
||||
expect(sql).toContain('SELECT *');
|
||||
expect(sql).toContain('WHERE sparkID = ? AND accountId = ?');
|
||||
expect(sql).toContain('LIMIT 1');
|
||||
expect(params).toEqual(['btc-txid', 'identity-pubkey']);
|
||||
});
|
||||
|
||||
it('returns null without opening the database for missing lookup input', async () => {
|
||||
const mockDb = createMockDb();
|
||||
const { getSparkTransactionBySparkId } = loadTransactionsModule(mockDb);
|
||||
|
||||
await expect(
|
||||
getSparkTransactionBySparkId('', 'identity-pubkey'),
|
||||
).resolves.toBe(null);
|
||||
await expect(getSparkTransactionBySparkId('btc-txid')).resolves.toBe(null);
|
||||
|
||||
expect(mockOpenDatabaseAsync).not.toHaveBeenCalled();
|
||||
expect(mockDb.getAllAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatestSavedLRC20TransactionId', () => {
|
||||
it('returns the latest saved token transaction id from SQLite', async () => {
|
||||
const mockDb = createMockDb();
|
||||
mockDb.getAllAsync.mockResolvedValue([{ sparkID: 'token-tx-hash' }]);
|
||||
const { getLatestSavedLRC20TransactionId } =
|
||||
loadTransactionsModule(mockDb);
|
||||
|
||||
const result = await getLatestSavedLRC20TransactionId('identity-pubkey');
|
||||
|
||||
expect(result).toBe('token-tx-hash');
|
||||
expect(mockDb.getAllAsync).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [sql, params] = mockDb.getAllAsync.mock.calls[0];
|
||||
expect(sql).toContain('SELECT sparkID');
|
||||
expect(sql).toContain('accountId = ?');
|
||||
expect(sql).toContain("paymentType = 'spark'");
|
||||
expect(sql).toContain('LENGTH(sparkID) >= 40');
|
||||
expect(sql).toContain("ORDER BY json_extract(details, '$.time') DESC");
|
||||
expect(sql).toContain('LIMIT 1');
|
||||
expect(params).toEqual(['identity-pubkey']);
|
||||
});
|
||||
|
||||
it('returns null without opening the database for a missing account id', async () => {
|
||||
const mockDb = createMockDb();
|
||||
const { getLatestSavedLRC20TransactionId } =
|
||||
loadTransactionsModule(mockDb);
|
||||
|
||||
await expect(getLatestSavedLRC20TransactionId()).resolves.toBe(null);
|
||||
|
||||
expect(mockOpenDatabaseAsync).not.toHaveBeenCalled();
|
||||
expect(mockDb.getAllAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
+17
-8
@@ -35,6 +35,7 @@ import {
|
||||
SIZES,
|
||||
} from '../../../../../constants/theme';
|
||||
import CheckMarkCircle from '../../../../../functions/CustomElements/checkMarkCircle';
|
||||
import { KeyboardController } from 'react-native-keyboard-controller';
|
||||
|
||||
const DEFAULT_FILTER = {
|
||||
categories: [],
|
||||
@@ -210,20 +211,28 @@ export default function OnlineListingsFilterHalfModal({
|
||||
}, []);
|
||||
|
||||
const handleCountryPress = useCallback(
|
||||
countryCode => {
|
||||
setDraft(prev => ({ ...prev, countryCode }));
|
||||
setCountrySearch('');
|
||||
setShowCountryPicker(false);
|
||||
setIsKeyboardActive?.(false);
|
||||
async countryCode => {
|
||||
await KeyboardController.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
setDraft(prev => ({ ...prev, countryCode }));
|
||||
setCountrySearch('');
|
||||
setShowCountryPicker(false);
|
||||
setIsKeyboardActive?.(false);
|
||||
});
|
||||
},
|
||||
[setIsKeyboardActive],
|
||||
);
|
||||
|
||||
const handleCountryBackPress = useCallback(() => {
|
||||
if (!showCountryPicker) return false;
|
||||
setShowCountryPicker(false);
|
||||
setCountrySearch('');
|
||||
setIsKeyboardActive?.(false);
|
||||
KeyboardController.dismiss().then(resp => {
|
||||
requestAnimationFrame(() => {
|
||||
setShowCountryPicker(false);
|
||||
setCountrySearch('');
|
||||
setIsKeyboardActive?.(false);
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}, [setIsKeyboardActive, showCountryPicker]);
|
||||
|
||||
|
||||
+10
-5
@@ -1,3 +1,4 @@
|
||||
import { uses24HourClock } from 'react-native-localize';
|
||||
import { formatLocalTimeNumeric } from '../../../../../functions/timeFormatter';
|
||||
|
||||
// Utility functions
|
||||
@@ -46,11 +47,15 @@ export function createFormattedDate(time, currentTime, t) {
|
||||
formattedTime = t('constants.yesterday');
|
||||
} else if (isSameDay(date, currentDate)) {
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
|
||||
const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
|
||||
formattedTime = `${formattedHours}:${formattedMinutes} ${ampm}`;
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
if (uses24HourClock()) {
|
||||
formattedTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
|
||||
formattedTime = `${formattedHours}:${minutes} ${ampm}`;
|
||||
}
|
||||
} else {
|
||||
const daysDiff = getDaysDifference(currentDate, date);
|
||||
if (daysDiff === 1) {
|
||||
|
||||
@@ -157,6 +157,7 @@ export default function HomeLightning({ navigation }) {
|
||||
sparkInformation,
|
||||
showTokensInformation,
|
||||
isSendingPaymentRef,
|
||||
updateHomepageScrollPosition,
|
||||
// numberOfCachedTxs
|
||||
} = useSparkWallet();
|
||||
const { poolInfoRef } = useFlashnet();
|
||||
@@ -190,13 +191,15 @@ export default function HomeLightning({ navigation }) {
|
||||
const prevBg = useSharedValue(false);
|
||||
const [navbarHeight, setNavbarHeight] = useState(0);
|
||||
const [scrollPosition, setScrollPosition] = useState('total');
|
||||
const scrollPositionRef = useRef('total');
|
||||
|
||||
const updateScrollPosition = useCallback(page => {
|
||||
const pos = page === 0 ? 'total' : page === 1 ? 'sats' : 'usd';
|
||||
scrollPositionRef.current = pos;
|
||||
setScrollPosition(pos);
|
||||
}, []);
|
||||
const updateScrollPosition = useCallback(
|
||||
page => {
|
||||
const pos = page === 0 ? 'total' : page === 1 ? 'sats' : 'usd';
|
||||
setScrollPosition(pos);
|
||||
updateHomepageScrollPosition(pos);
|
||||
},
|
||||
[updateHomepageScrollPosition],
|
||||
);
|
||||
|
||||
const onBalancePageScroll = usePagerScrollHandler({
|
||||
onPageScroll: e => {
|
||||
@@ -349,7 +352,6 @@ export default function HomeLightning({ navigation }) {
|
||||
darkModeType,
|
||||
t,
|
||||
showTokensInformation,
|
||||
scrollPosition,
|
||||
]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
@@ -613,7 +615,7 @@ export default function HomeLightning({ navigation }) {
|
||||
theme={theme}
|
||||
darkModeType={darkModeType}
|
||||
isConnectedToTheInternet={isConnectedToTheInternet}
|
||||
scrollPositionRef={scrollPositionRef}
|
||||
scrollPosition={scrollPosition}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function SendRecieveBTNs({
|
||||
darkModeType,
|
||||
isConnectedToTheInternet,
|
||||
isNWCWallet = false,
|
||||
scrollPositionRef,
|
||||
scrollPosition,
|
||||
}) {
|
||||
const navigate = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
@@ -60,11 +60,11 @@ export function SendRecieveBTNs({
|
||||
return;
|
||||
}
|
||||
navigate.navigate('CustomHalfModal', {
|
||||
scrollPosition: scrollPositionRef?.current === 'usd' ? 'USD' : 'BTC',
|
||||
scrollPosition: scrollPosition === 'usd' ? 'USD' : 'BTC',
|
||||
wantedContent: 'receiveOptions',
|
||||
sliderHight: 0.8,
|
||||
});
|
||||
}, [handleSettingsCheck, navigate, scrollPositionRef, t]);
|
||||
}, [handleSettingsCheck, navigate, scrollPosition, t]);
|
||||
|
||||
const handleCamera = useCallback(() => {
|
||||
const areSettingsSet = handleSettingsCheck();
|
||||
|
||||
@@ -19,7 +19,7 @@ import { INSET_WINDOW_WIDTH } from '../../../../constants/theme';
|
||||
|
||||
export default function SparkErrorScreen(props) {
|
||||
const { accountMnemoinc } = useKeysContext();
|
||||
const { setSparkInformation } = useSparkWallet();
|
||||
const { setSparkInformation, filterAndSetTransactions } = useSparkWallet();
|
||||
const { showToast } = useToast();
|
||||
const { backgroundColor, backgroundOffset, transparentOveraly } =
|
||||
GetThemeColors();
|
||||
@@ -37,6 +37,7 @@ export default function SparkErrorScreen(props) {
|
||||
if (retryCount < 1) {
|
||||
const { didWork, error } = await initWallet({
|
||||
setSparkInformation,
|
||||
filterAndSetTransactions,
|
||||
// toggleGlobalContactsInformation,
|
||||
// globalContactsInformation,
|
||||
mnemonic: accountMnemoinc,
|
||||
|
||||
@@ -2,22 +2,13 @@ import {
|
||||
crashlyticsLogReport,
|
||||
crashlyticsRecordErrorReport,
|
||||
} from '../../../../../functions/crashlyticsLogs';
|
||||
import { getAllSparkTransactions } from '../../../../../functions/spark/transactions';
|
||||
import { hasPaidSparkLightningInvoice } from '../../../../../functions/spark/transactions';
|
||||
|
||||
export default async function hasAlredyPaidInvoice({ scannedAddress }) {
|
||||
try {
|
||||
crashlyticsLogReport('Begining already paid invoice function');
|
||||
|
||||
const allTransactions = await getAllSparkTransactions();
|
||||
|
||||
const didPayWithSpark = allTransactions.find(tx => {
|
||||
return (
|
||||
tx.paymentType === 'lightning' &&
|
||||
JSON.parse(tx.details).address?.trim() === scannedAddress?.trim()
|
||||
);
|
||||
});
|
||||
|
||||
return !!didPayWithSpark;
|
||||
return await hasPaidSparkLightningInvoice(scannedAddress);
|
||||
} catch (err) {
|
||||
console.log('already paid invoice error', err);
|
||||
crashlyticsRecordErrorReport(err.message);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import CheckMarkCircle from '../../../../functions/CustomElements/checkMarkCircle';
|
||||
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
|
||||
import DropdownMenu from '../../../../functions/CustomElements/dropdownMenu';
|
||||
import { useSparkWallet } from '../../../../../context-store/sparkContext';
|
||||
|
||||
// Settings Section Component
|
||||
const SettingsSection = ({ title, children, style }) => {
|
||||
@@ -88,6 +89,7 @@ export default function DisplayOptions() {
|
||||
const initialValueRef = useRef(masterInfoObject.userBalanceDenomination);
|
||||
const saveTimeoutRef = useRef(null);
|
||||
const navigate = useNavigation();
|
||||
const { updateHomepageTxPreferance } = useSparkWallet();
|
||||
|
||||
const dropdownOptions = [15, 20, 25, 30, 35, 40].map(value => ({
|
||||
label: t('settings.displayOptions.transactionsLabel', { context: value }),
|
||||
@@ -268,9 +270,10 @@ export default function DisplayOptions() {
|
||||
selectedValue={t('settings.displayOptions.transactionsLabel', {
|
||||
context: masterInfoObject.homepageTxPreferance,
|
||||
})}
|
||||
onSelect={item =>
|
||||
toggleMasterInfoObject({ homepageTxPreferance: item.value })
|
||||
}
|
||||
onSelect={item => {
|
||||
toggleMasterInfoObject({ homepageTxPreferance: item.value });
|
||||
updateHomepageTxPreferance(item.value);
|
||||
}}
|
||||
customButtonStyles={{
|
||||
backgroundColor: theme ? backgroundColor : COLORS.darkModeText,
|
||||
}}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { KeyboardController } from 'react-native-keyboard-controller';
|
||||
|
||||
export async function keyboardGoBack(navigate) {
|
||||
await KeyboardController.dismiss();
|
||||
setTimeout(navigate.goBack, KeyboardController.isVisible() ? 60 : 0);
|
||||
setTimeout(navigate.goBack, KeyboardController.isVisible() ? 30 : 0);
|
||||
}
|
||||
|
||||
export async function keyboardNavigate(navigatorFunction) {
|
||||
await KeyboardController.dismiss();
|
||||
setTimeout(navigatorFunction, KeyboardController.isVisible() ? 60 : 0);
|
||||
setTimeout(navigatorFunction, KeyboardController.isVisible() ? 30 : 0);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as ImageManipulator from 'expo-image-manipulator';
|
||||
import {deleteAsync} from 'expo-file-system/legacy';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
|
||||
export async function detectQRCode(uri) {
|
||||
let temporaryImageUri;
|
||||
|
||||
try {
|
||||
const resized = ImageManipulator.ImageManipulator.manipulate(uri).resize({
|
||||
width: 400,
|
||||
@@ -12,9 +15,10 @@ export async function detectQRCode(uri) {
|
||||
compress: 0.5,
|
||||
format: ImageManipulator.SaveFormat.WEBP,
|
||||
});
|
||||
temporaryImageUri = savedImage.uri;
|
||||
|
||||
const response = await RNQRGenerator.detect({
|
||||
uri: savedImage.uri,
|
||||
uri: temporaryImageUri,
|
||||
});
|
||||
|
||||
return response;
|
||||
@@ -25,5 +29,13 @@ export async function detectQRCode(uri) {
|
||||
console.error('QR detection failed:', error);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
if (temporaryImageUri) {
|
||||
try {
|
||||
await deleteAsync(temporaryImageUri, {idempotent: true});
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to delete temporary QR scan image:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getAccountBalanceSnapshot } from './spark/balanceSnapshots';
|
||||
|
||||
export async function initWallet({
|
||||
setSparkInformation,
|
||||
filterAndSetTransactions,
|
||||
// toggleGlobalContactsInformation,
|
||||
// globalContactsInformation,
|
||||
mnemonic,
|
||||
@@ -32,6 +33,7 @@ export async function initWallet({
|
||||
}));
|
||||
const didSetSpark = await initializeSparkSession({
|
||||
setSparkInformation,
|
||||
filterAndSetTransactions,
|
||||
// globalContactsInformation,
|
||||
// toggleGlobalContactsInformation,
|
||||
mnemonic,
|
||||
@@ -59,6 +61,7 @@ export async function initWallet({
|
||||
|
||||
export async function initializeSparkSession({
|
||||
setSparkInformation,
|
||||
filterAndSetTransactions,
|
||||
mnemonic,
|
||||
sendWebViewRequest,
|
||||
hasRestoreCompleted,
|
||||
@@ -116,8 +119,10 @@ export async function initializeSparkSession({
|
||||
setSparkInformation(prev => ({
|
||||
...prev,
|
||||
...storageObject,
|
||||
transactions: transactions ?? prev.transactions,
|
||||
}));
|
||||
const txToUse = transactions ?? [];
|
||||
if (txToUse.length && filterAndSetTransactions)
|
||||
filterAndSetTransactions(txToUse);
|
||||
return storageObject;
|
||||
}
|
||||
|
||||
@@ -130,15 +135,15 @@ export async function initializeSparkSession({
|
||||
initialBalance: Number(balance.balance),
|
||||
};
|
||||
|
||||
setSparkInformation(prev => {
|
||||
const txToUse =
|
||||
!hasRestoreCompleted ||
|
||||
(prev.identityPubKey && prev.identityPubKey !== identityPubKey)
|
||||
? transactions ?? prev.transactions
|
||||
: prev.transactions;
|
||||
const txToUse =
|
||||
!hasRestoreCompleted ||
|
||||
(cachedIdentityPubKey && cachedIdentityPubKey !== identityPubKey)
|
||||
? transactions
|
||||
: null;
|
||||
|
||||
return { ...prev, ...storageObject, transactions: txToUse };
|
||||
});
|
||||
setSparkInformation(prev => ({...prev, ...storageObject}));
|
||||
if (txToUse && filterAndSetTransactions)
|
||||
filterAndSetTransactions(txToUse);
|
||||
return storageObject;
|
||||
} catch (err) {
|
||||
console.log('Set spark error', err);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { AppState } from 'react-native';
|
||||
import { IS_SPARK_ID, USDB_TOKEN_ID } from '../../constants';
|
||||
import {
|
||||
getCachedSparkTransactions,
|
||||
getSparkTokenTransactions,
|
||||
} from '../spark';
|
||||
import { getSparkTokenTransactions } from '../spark';
|
||||
import { getActiveSwapTransferIds, isSwapActive } from '../spark/flashnet';
|
||||
import {
|
||||
bulkUpdateSparkTransactions,
|
||||
deleteSparkContactTransaction,
|
||||
getAllSparkContactInvoices,
|
||||
getLatestSavedLRC20TransactionId,
|
||||
getSparkTransactionBySparkId,
|
||||
} from '../spark/transactions';
|
||||
import { convertToBech32m } from './bech32';
|
||||
import tokenBufferAmountToDecimal from './bufferToDecimal';
|
||||
@@ -24,24 +23,10 @@ export async function getLRC20Transactions({
|
||||
if (isRunning) throw new Error('process is already running');
|
||||
isRunning = true;
|
||||
if (AppState.currentState !== 'active') return;
|
||||
const savedTxs = await getCachedSparkTransactions(null, ownerPublicKeys[0]);
|
||||
|
||||
// Find last saved token transaction (any status, including failed flashnet pairs)
|
||||
let lastSavedTransactionId = null;
|
||||
if (savedTxs) {
|
||||
for (const tx of savedTxs) {
|
||||
if (
|
||||
tx.paymentType !== 'spark' ||
|
||||
IS_SPARK_ID.test(tx.sparkID) ||
|
||||
tx.sparkID.length < 40
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lastSavedTransactionId = tx.sparkID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const ownerPubKey = ownerPublicKeys[0];
|
||||
const lastSavedTransactionId = await getLatestSavedLRC20TransactionId(
|
||||
ownerPubKey,
|
||||
);
|
||||
|
||||
const tokenTxs = await getSparkTokenTransactions({
|
||||
ownerPublicKeys,
|
||||
@@ -53,23 +38,24 @@ export async function getLRC20Transactions({
|
||||
if (!tokenTxs?.tokenTransactionsWithStatus) return;
|
||||
const tokenTransactions = tokenTxs.tokenTransactionsWithStatus;
|
||||
|
||||
// Build savedIds set for all saved LRC20 token txs (any status) to avoid reprocessing
|
||||
const savedIds = new Set();
|
||||
if (savedTxs) {
|
||||
for (const tx of savedTxs) {
|
||||
if (tx.paymentType !== 'spark' || IS_SPARK_ID.test(tx.sparkID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
savedIds.add(tx.sparkID);
|
||||
}
|
||||
}
|
||||
|
||||
const newTxs = [];
|
||||
const ownerPubKey = ownerPublicKeys[0];
|
||||
const isSwapInProgress = isSwapActive();
|
||||
const activeSwaps = getActiveSwapTransferIds();
|
||||
const unpaidContactInvoices = await getAllSparkContactInvoices();
|
||||
const savedTxCache = new Map();
|
||||
const getSavedLRC20Tx = async txHash => {
|
||||
if (!txHash) return null;
|
||||
if (savedTxCache.has(txHash)) return savedTxCache.get(txHash);
|
||||
|
||||
const savedTx = await getSparkTransactionBySparkId(txHash, ownerPubKey);
|
||||
const savedLRC20Tx =
|
||||
savedTx?.paymentType === 'spark' && !IS_SPARK_ID.test(savedTx.sparkID)
|
||||
? savedTx
|
||||
: null;
|
||||
|
||||
savedTxCache.set(txHash, savedLRC20Tx);
|
||||
return savedLRC20Tx;
|
||||
};
|
||||
|
||||
for (const tokenTx of tokenTransactions) {
|
||||
const tokenOutput = tokenTx.tokenTransaction.tokenOutputs[0];
|
||||
@@ -89,7 +75,7 @@ export async function getLRC20Transactions({
|
||||
).toString('hex');
|
||||
|
||||
// Skip if already saved
|
||||
if (savedIds.has(txHash)) continue;
|
||||
if (await getSavedLRC20Tx(txHash)) continue;
|
||||
|
||||
const tokenOutputs = tokenTx.tokenTransaction.tokenOutputs;
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import {USDB_TOKEN_ID} from '../../constants';
|
||||
import {isFlashnetTransfer} from './handleFlashnetTransferIds';
|
||||
|
||||
export function filterDisplayableTransactions({
|
||||
transactions,
|
||||
scrollPosition,
|
||||
enabledLRC20,
|
||||
tokens,
|
||||
forcedPendingMap,
|
||||
appliedEpoch = 0,
|
||||
limit = 25,
|
||||
}) {
|
||||
if (!transactions?.length) return [];
|
||||
|
||||
const shownTxs = new Set();
|
||||
const lnFundingTxIds = new Set();
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < transactions.length && result.length < limit; i++) {
|
||||
const tx = transactions[i];
|
||||
|
||||
let paymentDetails;
|
||||
try {
|
||||
paymentDetails = JSON.parse(tx.details);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const paymentType = tx.paymentType;
|
||||
const paymentStatus = tx.paymentStatus;
|
||||
const isLRC20Payment = paymentDetails.isLRC20Payment;
|
||||
const hasSavedTokenData = tokens?.[paymentDetails.LRC20Token];
|
||||
|
||||
if (paymentDetails?.ln_funding_id) {
|
||||
lnFundingTxIds.add(paymentDetails.ln_funding_id);
|
||||
}
|
||||
|
||||
const showSwapConversion =
|
||||
paymentDetails.performSwaptoUSD &&
|
||||
(!paymentDetails.completedSwaptoUSD ||
|
||||
!lnFundingTxIds.has(tx.sparkID));
|
||||
|
||||
// Static filters
|
||||
if (
|
||||
!enabledLRC20 &&
|
||||
isLRC20Payment &&
|
||||
paymentDetails.LRC20Token !== USDB_TOKEN_ID
|
||||
)
|
||||
continue;
|
||||
if (paymentType === 'unknown') continue;
|
||||
if (
|
||||
paymentDetails.senderIdentityPublicKey ===
|
||||
process.env.SPARK_IDENTITY_PUBKEY
|
||||
)
|
||||
continue;
|
||||
if (shownTxs.has(tx.sparkID)) continue;
|
||||
if (isLRC20Payment && !hasSavedTokenData) continue;
|
||||
if (paymentStatus === 'failed') continue;
|
||||
if (
|
||||
paymentType === 'lightning' &&
|
||||
tx.status === 'LIGHTNING_PAYMENT_INITIATED'
|
||||
)
|
||||
continue;
|
||||
if (isFlashnetTransfer(tx.sparkID)) continue;
|
||||
|
||||
// Scroll position filters
|
||||
if (
|
||||
scrollPosition === 'total' &&
|
||||
paymentDetails.showSwapLabel &&
|
||||
paymentDetails.direction === 'OUTGOING'
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
(scrollPosition === 'sats' && isLRC20Payment) ||
|
||||
(scrollPosition === 'sats' && showSwapConversion)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
(scrollPosition === 'usd' &&
|
||||
isLRC20Payment &&
|
||||
paymentDetails.LRC20Token !== USDB_TOKEN_ID) ||
|
||||
(scrollPosition === 'usd' && !isLRC20Payment && !showSwapConversion)
|
||||
)
|
||||
continue;
|
||||
|
||||
shownTxs.add(tx.sparkID);
|
||||
|
||||
// Apply pending flag
|
||||
const pendingMeta = forcedPendingMap?.get(tx.sparkID);
|
||||
if (pendingMeta && pendingMeta.epoch > appliedEpoch && !tx.isBalancePending) {
|
||||
result.push({...tx, isBalancePending: true});
|
||||
} else {
|
||||
result.push(tx);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
getCachedSparkTransactions,
|
||||
getSingleTxDetails,
|
||||
getSparkBitcoinPaymentRequest,
|
||||
getSparkLightningPaymentStatus,
|
||||
|
||||
@@ -189,6 +189,23 @@ export const initializeSparkDatabase = async () => {
|
||||
tokens TEXT NOT NULL DEFAULT '{}',
|
||||
updatedAt INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_spark_tx_lightning_invoice_address
|
||||
ON ${SPARK_TRANSACTIONS_TABLE_NAME} (
|
||||
paymentType,
|
||||
TRIM(json_extract(details, '$.address'))
|
||||
)
|
||||
WHERE paymentType = 'lightning' AND json_valid(details);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_spark_tx_spark_id_account_id
|
||||
ON ${SPARK_TRANSACTIONS_TABLE_NAME} (sparkID, accountId);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_spark_tx_lrc20_latest
|
||||
ON ${SPARK_TRANSACTIONS_TABLE_NAME} (
|
||||
accountId,
|
||||
paymentType,
|
||||
json_extract(details, '$.time')
|
||||
);
|
||||
`);
|
||||
|
||||
console.log('Opened spark transaction and contacts tables');
|
||||
@@ -298,6 +315,84 @@ export const getAllSparkTransactions = async (options = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const hasPaidSparkLightningInvoice = async invoiceAddress => {
|
||||
const trimmedInvoiceAddress =
|
||||
typeof invoiceAddress === 'string' ? invoiceAddress.trim() : '';
|
||||
|
||||
if (!trimmedInvoiceAddress) return false;
|
||||
|
||||
try {
|
||||
await ensureSparkDatabaseReady();
|
||||
|
||||
const result = await sqlLiteDB.getAllAsync(
|
||||
`SELECT 1 as found
|
||||
FROM ${SPARK_TRANSACTIONS_TABLE_NAME}
|
||||
WHERE paymentType = 'lightning'
|
||||
AND json_valid(details)
|
||||
AND TRIM(json_extract(details, '$.address')) = ?
|
||||
LIMIT 1`,
|
||||
[trimmedInvoiceAddress],
|
||||
);
|
||||
|
||||
return result?.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking paid spark lightning invoice:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSparkTransactionBySparkId = async (sparkID, accountId) => {
|
||||
const normalizedSparkID = typeof sparkID === 'string' ? sparkID.trim() : '';
|
||||
const normalizedAccountId =
|
||||
accountId !== undefined && accountId !== null ? String(accountId) : '';
|
||||
|
||||
if (!normalizedSparkID || !normalizedAccountId) return null;
|
||||
|
||||
try {
|
||||
await ensureSparkDatabaseReady();
|
||||
|
||||
const rows = await sqlLiteDB.getAllAsync(
|
||||
`SELECT *
|
||||
FROM ${SPARK_TRANSACTIONS_TABLE_NAME}
|
||||
WHERE sparkID = ? AND accountId = ?
|
||||
LIMIT 1`,
|
||||
[normalizedSparkID, normalizedAccountId],
|
||||
);
|
||||
|
||||
return rows?.[0] ?? null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching spark transaction by sparkID:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestSavedLRC20TransactionId = async accountId => {
|
||||
const normalizedAccountId =
|
||||
accountId !== undefined && accountId !== null ? String(accountId) : '';
|
||||
|
||||
if (!normalizedAccountId) return null;
|
||||
|
||||
try {
|
||||
await ensureSparkDatabaseReady();
|
||||
|
||||
const rows = await sqlLiteDB.getAllAsync(
|
||||
`SELECT sparkID
|
||||
FROM ${SPARK_TRANSACTIONS_TABLE_NAME}
|
||||
WHERE accountId = ?
|
||||
AND paymentType = 'spark'
|
||||
AND LENGTH(sparkID) >= 40
|
||||
ORDER BY json_extract(details, '$.time') DESC
|
||||
LIMIT 1`,
|
||||
[normalizedAccountId],
|
||||
);
|
||||
|
||||
return rows?.[0]?.sparkID ?? null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching latest saved LRC20 transaction ID:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const DATE_OFFSETS = {
|
||||
'7d': 7 * 24 * 60 * 60 * 1000,
|
||||
'30d': 30 * 24 * 60 * 60 * 1000,
|
||||
|
||||
@@ -11,7 +11,8 @@ import { navigationRef } from '../../navigation/navigationService';
|
||||
|
||||
export default function useAccountSwitcher() {
|
||||
const navigate = useNavigation();
|
||||
const { setSparkInformation, sparkInformation } = useSparkWallet();
|
||||
const { setSparkInformation, sparkInformation, filterAndSetTransactions } =
|
||||
useSparkWallet();
|
||||
const {
|
||||
currentWalletMnemoinc,
|
||||
selectedAltAccount,
|
||||
@@ -60,6 +61,7 @@ export default function useAccountSwitcher() {
|
||||
|
||||
const initResponse = await initWallet({
|
||||
setSparkInformation,
|
||||
filterAndSetTransactions,
|
||||
mnemonic: accountMnemonic,
|
||||
hasRestoreCompleted: false,
|
||||
});
|
||||
|
||||
@@ -65,7 +65,10 @@ export default function ViewAllTxPage() {
|
||||
|
||||
let transactions;
|
||||
if (!hasActiveFilters) {
|
||||
const txs = await getAllSparkTransactions({ limit: null });
|
||||
const txs = await getAllSparkTransactions({
|
||||
limit: null,
|
||||
accountId: sparkInformation.identityPubKey,
|
||||
});
|
||||
transactions = txs;
|
||||
} else {
|
||||
transactions = await getFilteredTransactions(
|
||||
|
||||
@@ -550,29 +550,50 @@ export const ActiveCustodyAccountProvider = ({ children }) => {
|
||||
});
|
||||
}, [custodyAccountsList, isUsingNostr, selectedAltAccount]);
|
||||
|
||||
const accountValues = useMemo(() => {
|
||||
return {
|
||||
custodyAccounts,
|
||||
removeAccount,
|
||||
createAccount,
|
||||
updateAccount,
|
||||
updateAccountCacheOnly,
|
||||
createDerivedAccount,
|
||||
createImportedAccount,
|
||||
restoreDerivedAccount,
|
||||
getAccountMnemonic,
|
||||
restoreDerivedAccountsFromCloud,
|
||||
selectedAltAccount,
|
||||
isUsingAltAccount,
|
||||
currentWalletMnemoinc,
|
||||
toggleIsUsingNostr,
|
||||
isUsingNostr,
|
||||
nostrSeed,
|
||||
activeAccount,
|
||||
custodyAccountsList,
|
||||
};
|
||||
}, [
|
||||
custodyAccounts,
|
||||
removeAccount,
|
||||
createAccount,
|
||||
updateAccount,
|
||||
updateAccountCacheOnly,
|
||||
createDerivedAccount,
|
||||
createImportedAccount,
|
||||
restoreDerivedAccount,
|
||||
getAccountMnemonic,
|
||||
restoreDerivedAccountsFromCloud,
|
||||
selectedAltAccount,
|
||||
isUsingAltAccount,
|
||||
currentWalletMnemoinc,
|
||||
toggleIsUsingNostr,
|
||||
isUsingNostr,
|
||||
nostrSeed,
|
||||
activeAccount,
|
||||
custodyAccountsList,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ActiveCustodyAccount.Provider
|
||||
value={{
|
||||
custodyAccounts,
|
||||
removeAccount,
|
||||
createAccount,
|
||||
updateAccount,
|
||||
updateAccountCacheOnly,
|
||||
createDerivedAccount,
|
||||
createImportedAccount,
|
||||
restoreDerivedAccount,
|
||||
getAccountMnemonic,
|
||||
restoreDerivedAccountsFromCloud,
|
||||
selectedAltAccount,
|
||||
isUsingAltAccount,
|
||||
currentWalletMnemoinc,
|
||||
toggleIsUsingNostr,
|
||||
isUsingNostr,
|
||||
nostrSeed,
|
||||
activeAccount,
|
||||
custodyAccountsList,
|
||||
}}
|
||||
>
|
||||
<ActiveCustodyAccount.Provider value={accountValues}>
|
||||
{children}
|
||||
</ActiveCustodyAccount.Provider>
|
||||
);
|
||||
|
||||
@@ -223,30 +223,48 @@ export function AnalyticsProvider({ children }) {
|
||||
}
|
||||
}, [spentTotalBTC, spentTotalUSD]);
|
||||
|
||||
const accountsValues = useMemo(() => {
|
||||
return {
|
||||
spentTotal,
|
||||
inTxsBTC,
|
||||
outTxsBTC,
|
||||
inTxsUSD,
|
||||
outTxsUSD,
|
||||
incomeTotalBTC,
|
||||
incomeTotalUSD,
|
||||
spentTotalBTC,
|
||||
spentTotalUSD,
|
||||
incomeTxCountBTC: inTxsBTC.length,
|
||||
spentTxCountBTC: outTxsBTC.length,
|
||||
incomeTxCountUSD: inTxsUSD.length,
|
||||
spentTxCountUSD: outTxsUSD.length,
|
||||
cumulativeIncomeDataBTC,
|
||||
cumulativeSpentDataBTC,
|
||||
cumulativeIncomeDataUSD,
|
||||
cumulativeSpentDataUSD,
|
||||
isLoading,
|
||||
isReloading,
|
||||
};
|
||||
}, [
|
||||
spentTotal,
|
||||
inTxsBTC,
|
||||
outTxsBTC,
|
||||
inTxsUSD,
|
||||
outTxsUSD,
|
||||
incomeTotalBTC,
|
||||
incomeTotalUSD,
|
||||
spentTotalBTC,
|
||||
spentTotalUSD,
|
||||
cumulativeIncomeDataBTC,
|
||||
cumulativeSpentDataBTC,
|
||||
cumulativeIncomeDataUSD,
|
||||
cumulativeSpentDataUSD,
|
||||
isLoading,
|
||||
isReloading,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AnalyticsContext.Provider
|
||||
value={{
|
||||
spentTotal,
|
||||
inTxsBTC,
|
||||
outTxsBTC,
|
||||
inTxsUSD,
|
||||
outTxsUSD,
|
||||
incomeTotalBTC,
|
||||
incomeTotalUSD,
|
||||
spentTotalBTC,
|
||||
spentTotalUSD,
|
||||
incomeTxCountBTC: inTxsBTC.length,
|
||||
spentTxCountBTC: outTxsBTC.length,
|
||||
incomeTxCountUSD: inTxsUSD.length,
|
||||
spentTxCountUSD: outTxsUSD.length,
|
||||
cumulativeIncomeDataBTC,
|
||||
cumulativeSpentDataBTC,
|
||||
cumulativeIncomeDataUSD,
|
||||
cumulativeSpentDataUSD,
|
||||
isLoading,
|
||||
isReloading,
|
||||
}}
|
||||
>
|
||||
<AnalyticsContext.Provider value={accountsValues}>
|
||||
{children}
|
||||
</AnalyticsContext.Provider>
|
||||
);
|
||||
|
||||
+123
-30
@@ -25,6 +25,7 @@ import {
|
||||
insertSparkTransactionPlaceholders,
|
||||
getAllSparkTransactions,
|
||||
getAllSparkContactInvoices,
|
||||
getSparkTransactionBySparkId,
|
||||
getAllUnpaidSparkLightningInvoices,
|
||||
SPARK_TX_UPDATE_ENVENT_NAME,
|
||||
sparkTransactionsEventEmitter,
|
||||
@@ -36,10 +37,7 @@ import {
|
||||
updateSparkTxStatus,
|
||||
} from '../app/functions/spark/restore';
|
||||
import { useGlobalContactsInfo } from './globalContacts';
|
||||
import {
|
||||
initializeSparkSession,
|
||||
initWallet,
|
||||
} from '../app/functions/initiateWalletConnection';
|
||||
import { initWallet } from '../app/functions/initiateWalletConnection';
|
||||
// import { useNodeContext } from './nodeContext';
|
||||
import { AppState } from 'react-native';
|
||||
import getDepositAddressTxIds from '../app/functions/spark/getDepositAdressTxIds';
|
||||
@@ -73,6 +71,7 @@ import {
|
||||
runTokenOptimization,
|
||||
} from '../app/functions/spark/optimization';
|
||||
import { isFlashnetTransfer } from '../app/functions/spark/handleFlashnetTransferIds';
|
||||
import { filterDisplayableTransactions } from '../app/functions/spark/filterTransactions';
|
||||
|
||||
export const isSendingPayingEventEmiiter = new EventEmitter();
|
||||
export const SENDING_PAYMENT_EVENT_NAME = 'SENDING_PAYMENT_EVENT';
|
||||
@@ -197,12 +196,14 @@ const SparkWalletProvider = ({ children }) => {
|
||||
const balanceSupervisorRunIdRef = useRef(0);
|
||||
const forcedPendingBySparkIdRef = useRef(new Map());
|
||||
const lastConfirmedTxBoundaryRef = useRef(null);
|
||||
const scrollPositionRef = useRef('total');
|
||||
|
||||
const isBalancePollerRunningRef = useRef(false);
|
||||
const lastBalancePollEventRef = useRef({
|
||||
updateType: null,
|
||||
timestamp: 0,
|
||||
});
|
||||
const homepageTxPreferance = masterInfoObject.homepageTxPreferance;
|
||||
|
||||
const showTokensInformation =
|
||||
masterInfoObject.enabledBTKNTokens === null
|
||||
@@ -256,18 +257,17 @@ const SparkWalletProvider = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
sparkInfoRef.current = {
|
||||
...sparkInfoRef.current,
|
||||
balance: sparkInformation.balance,
|
||||
tokens: sparkInformation.tokens,
|
||||
identityPubKey: sparkInformation.identityPubKey,
|
||||
sparkAddress: sparkInformation.sparkAddress,
|
||||
transactions: sparkInformation.transactions?.slice(0, 50),
|
||||
};
|
||||
}, [
|
||||
sparkInformation.balance,
|
||||
sparkInformation.tokens,
|
||||
sparkInformation.identityPubKey,
|
||||
sparkInformation.sparkAddress,
|
||||
sparkInformation.transactions,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -621,6 +621,65 @@ const SparkWalletProvider = ({ children }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const filterAndSetTransactions = useCallback(
|
||||
freshTxs => {
|
||||
sparkInfoRef.current.transactions = freshTxs.slice(0, 50);
|
||||
const filtered = filterDisplayableTransactions({
|
||||
transactions: freshTxs,
|
||||
scrollPosition: scrollPositionRef.current,
|
||||
enabledLRC20: showTokensInformation,
|
||||
tokens: sparkInfoRef.current.tokens,
|
||||
forcedPendingMap: forcedPendingBySparkIdRef.current,
|
||||
appliedEpoch: balanceEpochRef.current.applied,
|
||||
limit: homepageTxPreferance,
|
||||
});
|
||||
setSparkInformation(prev => ({ ...prev, transactions: filtered }));
|
||||
},
|
||||
[showTokensInformation, homepageTxPreferance],
|
||||
);
|
||||
|
||||
const updateHomepageScrollPosition = useCallback(
|
||||
async pos => {
|
||||
scrollPositionRef.current = pos;
|
||||
const allTxs = await getAllSparkTransactions({
|
||||
limit: null,
|
||||
accountId: sparkInfoRef.current.identityPubKey,
|
||||
});
|
||||
const filtered = filterDisplayableTransactions({
|
||||
transactions: allTxs,
|
||||
scrollPosition: pos,
|
||||
enabledLRC20: showTokensInformation,
|
||||
tokens: sparkInfoRef.current.tokens,
|
||||
forcedPendingMap: forcedPendingBySparkIdRef.current,
|
||||
appliedEpoch: balanceEpochRef.current.applied,
|
||||
limit: homepageTxPreferance,
|
||||
});
|
||||
if (scrollPositionRef.current !== pos) return;
|
||||
setSparkInformation(prev => ({ ...prev, transactions: filtered }));
|
||||
},
|
||||
[showTokensInformation, homepageTxPreferance],
|
||||
);
|
||||
|
||||
const updateHomepageTxPreferance = useCallback(
|
||||
async num => {
|
||||
const allTxs = await getAllSparkTransactions({
|
||||
limit: null,
|
||||
accountId: sparkInfoRef.current.identityPubKey,
|
||||
});
|
||||
const filtered = filterDisplayableTransactions({
|
||||
transactions: allTxs,
|
||||
scrollPosition: scrollPositionRef.current,
|
||||
enabledLRC20: showTokensInformation,
|
||||
tokens: sparkInfoRef.current.tokens,
|
||||
forcedPendingMap: forcedPendingBySparkIdRef.current,
|
||||
appliedEpoch: balanceEpochRef.current.applied,
|
||||
limit: num,
|
||||
});
|
||||
setSparkInformation(prev => ({ ...prev, transactions: filtered }));
|
||||
},
|
||||
[showTokensInformation],
|
||||
);
|
||||
|
||||
const enqueueTxLane = useCallback((updateType, task) => {
|
||||
queueDepthRef.current += 1;
|
||||
console.log(
|
||||
@@ -856,10 +915,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
|
||||
const txListWithPendingFlags = applyForcedPendingFlags(txs);
|
||||
|
||||
setSparkInformation(prev => ({
|
||||
...prev,
|
||||
transactions: txListWithPendingFlags,
|
||||
}));
|
||||
filterAndSetTransactions(txs);
|
||||
|
||||
enqueueUiLane(event.updateType, () =>
|
||||
maybeHandleConfirmNavigation(
|
||||
@@ -869,7 +925,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
),
|
||||
);
|
||||
},
|
||||
[enqueueUiLane, maybeHandleConfirmNavigation],
|
||||
[enqueueUiLane, maybeHandleConfirmNavigation, filterAndSetTransactions],
|
||||
);
|
||||
|
||||
const applyConfirmedBalanceSnapshot = useCallback(
|
||||
@@ -907,6 +963,8 @@ const SparkWalletProvider = ({ children }) => {
|
||||
'apply confimred balance snapshot',
|
||||
);
|
||||
|
||||
filterAndSetTransactions(freshTxs);
|
||||
|
||||
const myVersion = ++balanceVersionRef.current;
|
||||
setSparkInformation(prev => {
|
||||
if (myVersion < balanceVersionRef.current) return prev;
|
||||
@@ -916,11 +974,15 @@ const SparkWalletProvider = ({ children }) => {
|
||||
? numericBalance
|
||||
: prev.balance,
|
||||
tokens: result?.didWork ? result.tokensObj : prev.tokens,
|
||||
transactions: projectedTxs || prev.transactions,
|
||||
};
|
||||
});
|
||||
},
|
||||
[maybeHandleConfirmNavigation, contactsPrivateKey, publicKey],
|
||||
[
|
||||
maybeHandleConfirmNavigation,
|
||||
contactsPrivateKey,
|
||||
publicKey,
|
||||
filterAndSetTransactions,
|
||||
],
|
||||
);
|
||||
|
||||
const runBalanceSupervisor = useCallback(async () => {
|
||||
@@ -1011,16 +1073,13 @@ const SparkWalletProvider = ({ children }) => {
|
||||
if (shouldForcePending) {
|
||||
const currentTxs = sparkInfoRef.current.transactions || [];
|
||||
registerForcedPendingForEpoch(currentTxs, epoch);
|
||||
setSparkInformation(prev => ({
|
||||
...prev,
|
||||
transactions: applyForcedPendingFlags(prev.transactions || []),
|
||||
}));
|
||||
filterAndSetTransactions(sparkInfoRef.current.transactions || []);
|
||||
}
|
||||
|
||||
runBalanceSupervisor();
|
||||
return epoch;
|
||||
},
|
||||
[runBalanceSupervisor],
|
||||
[runBalanceSupervisor, filterAndSetTransactions],
|
||||
);
|
||||
|
||||
const applyIncomingPaymentSnapshot = useCallback(
|
||||
@@ -1067,12 +1126,13 @@ const SparkWalletProvider = ({ children }) => {
|
||||
getBoundaryFromTxs(freshTxs),
|
||||
);
|
||||
|
||||
filterAndSetTransactions(freshTxs);
|
||||
|
||||
const myVersion = ++balanceVersionRef.current;
|
||||
setSparkInformation(prev => {
|
||||
if (myVersion < balanceVersionRef.current) return prev;
|
||||
return {
|
||||
...prev,
|
||||
transactions: applyForcedPendingFlags(freshTxs || prev.transactions),
|
||||
balance: Number.isFinite(numericPassedBalance)
|
||||
? numericPassedBalance
|
||||
: prev.balance,
|
||||
@@ -1082,7 +1142,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
};
|
||||
});
|
||||
},
|
||||
[contactsPrivateKey, publicKey],
|
||||
[contactsPrivateKey, publicKey, filterAndSetTransactions],
|
||||
);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
@@ -1447,6 +1507,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
tokens: {},
|
||||
identityPubKey: '',
|
||||
sparkAddress: '',
|
||||
transactions: [],
|
||||
};
|
||||
handledTransfers.current = new Set();
|
||||
prevListenerType.current = null;
|
||||
@@ -1573,10 +1634,18 @@ const SparkWalletProvider = ({ children }) => {
|
||||
if (AppState.currentState !== 'active') return;
|
||||
if (isSendingPaymentRef.current) return;
|
||||
if (!currentMnemonicRef.current) return;
|
||||
const allTxs = await getAllSparkTransactions({
|
||||
accountId: sparkInfoRef.current.identityPubKey,
|
||||
});
|
||||
const savedTxMap = new Map(allTxs.map(tx => [tx.sparkID, tx]));
|
||||
const savedTxCache = new Map();
|
||||
const getSavedTxByTxid = async txid => {
|
||||
if (!txid) return null;
|
||||
if (savedTxCache.has(txid)) return savedTxCache.get(txid);
|
||||
|
||||
const savedTx = await getSparkTransactionBySparkId(
|
||||
txid,
|
||||
sparkInfoRef.current.identityPubKey,
|
||||
);
|
||||
savedTxCache.set(txid, savedTx);
|
||||
return savedTx;
|
||||
};
|
||||
const depositAddresses = await queryAllStaticDepositAddresses(
|
||||
currentMnemonicRef.current,
|
||||
);
|
||||
@@ -1610,7 +1679,8 @@ const SparkWalletProvider = ({ children }) => {
|
||||
for (const tx of exploraData) {
|
||||
if (claimableByTxid.has(tx.txid)) continue; // Spark has it, Phase 2 handles it
|
||||
if (allKnownByTxid.has(tx.txid)) continue; // Already claimed by Spark
|
||||
if (savedTxMap.has(tx.txid)) continue; // Already in our DB
|
||||
const savedTx = await getSavedTxByTxid(tx.txid);
|
||||
if (savedTx) continue; // Already in our DB marked as pending
|
||||
|
||||
console.log(
|
||||
'Adding pending deposit tx (not yet claimable):',
|
||||
@@ -1628,7 +1698,11 @@ const SparkWalletProvider = ({ children }) => {
|
||||
address,
|
||||
sparkInfoRef.current,
|
||||
);
|
||||
savedTxMap.set(tx.txid, true);
|
||||
savedTxCache.set(tx.txid, {
|
||||
sparkID: tx.txid,
|
||||
accountId: sparkInfoRef.current.identityPubKey,
|
||||
details: JSON.stringify({ amount: tx.amount }),
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Unclaimed UTXOs for address:', address, unclaimedUtxos);
|
||||
@@ -1638,7 +1712,8 @@ const SparkWalletProvider = ({ children }) => {
|
||||
for (const utxo of unclaimedUtxos.utxos) {
|
||||
const { txid, vout } = utxo;
|
||||
const exploraTx = exploraData?.find(t => t.txid === txid);
|
||||
const hasAlreadySaved = savedTxMap.has(txid);
|
||||
let savedTx = await getSavedTxByTxid(txid);
|
||||
const hasAlreadySaved = !!savedTx;
|
||||
|
||||
// Get quote for this specific UTXO
|
||||
const {
|
||||
@@ -1670,7 +1745,17 @@ const SparkWalletProvider = ({ children }) => {
|
||||
|
||||
// Add pending transaction if not already saved
|
||||
if (!hasAlreadySaved) {
|
||||
await addPendingTransaction(quote, address, sparkInfoRef.current);
|
||||
const pendingTx = await addPendingTransaction(
|
||||
quote,
|
||||
address,
|
||||
sparkInfoRef.current,
|
||||
);
|
||||
savedTx = {
|
||||
sparkID: pendingTx.id,
|
||||
accountId: pendingTx.accountId,
|
||||
details: JSON.stringify(pendingTx.details),
|
||||
};
|
||||
savedTxCache.set(txid, savedTx);
|
||||
}
|
||||
|
||||
if (!claimTx || !didWork) {
|
||||
@@ -1701,7 +1786,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
} else {
|
||||
const savedTxDetails = (() => {
|
||||
try {
|
||||
return JSON.parse(savedTxMap.get(txid)?.details ?? 'null');
|
||||
return JSON.parse(savedTx?.details ?? 'null');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -1780,6 +1865,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
},
|
||||
};
|
||||
await bulkUpdateSparkTransactions([pendingTx], 'transactions');
|
||||
return pendingTx;
|
||||
};
|
||||
|
||||
clearAllDepositIntervals();
|
||||
@@ -1854,7 +1940,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
null,
|
||||
sparkInfoRef.current.identityPubKey,
|
||||
);
|
||||
setSparkInformation(prev => ({ ...prev, transactions }));
|
||||
filterAndSetTransactions(transactions);
|
||||
hasRestoreCompleted.current = true;
|
||||
}
|
||||
|
||||
@@ -1883,6 +1969,7 @@ const SparkWalletProvider = ({ children }) => {
|
||||
async identityPubKey => {
|
||||
const { didWork, error } = await initWallet({
|
||||
setSparkInformation,
|
||||
filterAndSetTransactions,
|
||||
// toggleGlobalContactsInformation,
|
||||
// globalContactsInformation,
|
||||
mnemonic: accountMnemoinc,
|
||||
@@ -1960,6 +2047,9 @@ const SparkWalletProvider = ({ children }) => {
|
||||
toggleNewestPaymentTimestamp,
|
||||
isSendingPaymentRef,
|
||||
sparkInfoRef,
|
||||
updateHomepageScrollPosition,
|
||||
filterAndSetTransactions,
|
||||
updateHomepageTxPreferance,
|
||||
}),
|
||||
[
|
||||
sparkInformation,
|
||||
@@ -1976,6 +2066,9 @@ const SparkWalletProvider = ({ children }) => {
|
||||
toggleNewestPaymentTimestamp,
|
||||
isSendingPaymentRef,
|
||||
sparkInfoRef,
|
||||
updateHomepageScrollPosition,
|
||||
filterAndSetTransactions,
|
||||
updateHomepageTxPreferance,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -1372,16 +1373,24 @@ export const WebViewProvider = ({ children }) => {
|
||||
[blockAndResetWebview],
|
||||
);
|
||||
|
||||
const providerValues = useMemo(() => {
|
||||
return {
|
||||
webViewRef,
|
||||
sendWebViewRequest: sendWebViewRequestInternal,
|
||||
fileHash,
|
||||
changeSparkConnectionState,
|
||||
didRunHandshakeRef,
|
||||
};
|
||||
}, [
|
||||
webViewRef,
|
||||
sendWebViewRequestInternal,
|
||||
fileHash,
|
||||
changeSparkConnectionState,
|
||||
didRunHandshakeRef,
|
||||
]);
|
||||
|
||||
return (
|
||||
<WebViewContext.Provider
|
||||
value={{
|
||||
webViewRef,
|
||||
sendWebViewRequest: sendWebViewRequestInternal,
|
||||
fileHash,
|
||||
changeSparkConnectionState,
|
||||
didRunHandshakeRef,
|
||||
}}
|
||||
>
|
||||
<WebViewContext.Provider value={providerValues}>
|
||||
{children}
|
||||
{verifiedPath && (
|
||||
<WebView
|
||||
|
||||
Reference in New Issue
Block a user