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:
Blake Kaufman
2026-05-18 11:16:12 -04:00
committed by GitHub
parent 146ef9e953
commit 8676db8e16
21 changed files with 633 additions and 174 deletions
@@ -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();
});
});
@@ -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]);
@@ -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 -2
View File
@@ -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);
}
+13 -1
View File
@@ -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);
}
}
}
}
+14 -9
View File
@@ -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);
+22 -36
View File
@@ -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;
+98
View File
@@ -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
View File
@@ -1,5 +1,4 @@
import {
getCachedSparkTransactions,
getSingleTxDetails,
getSparkBitcoinPaymentRequest,
getSparkLightningPaymentStatus,
+95
View File
@@ -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,
+3 -1
View File
@@ -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,
});
+4 -1
View File
@@ -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(
+43 -22
View File
@@ -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>
);
+41 -23
View File
@@ -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
View File
@@ -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,
],
);
+18 -9
View File
@@ -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