Simplify confirm split payment (#778)

* fix small style issues

* fix touch gap

* fixing podfile

* creating standardized amountIn function

* show tx ids not amounts

* use standardized amount in

* use standardized amount in

* fixed no payment information crash

* downgrade rn and react-native-quick-crypto

* only show margin if two elements exist

* adding backup amount

* wait till hompage to call analytics data

* fix restore bug

* updating firebase

* fix check logic

* wrap to handle thrown execptions

* use utils package directly

* adding error handling

* group notification payments insted of handling them sequentually

* handle payment requset on confirm page

* improving split payment ux

* adding translations

* adding firebase sdk warning
This commit is contained in:
Blake Kaufman
2026-04-19 11:43:00 -04:00
committed by GitHub
parent 4d6fb9b6d3
commit 6760060083
35 changed files with 2136 additions and 2073 deletions
@@ -0,0 +1,98 @@
import {
calculateFlashnetAmountIn,
SEND_AMOUNT_INCREASE_BUFFER,
} from '../app/functions/spark/swapAmountUtils';
const PRICE = 1_000_000; // 1 sat = $1.00 at this mock price (simplifies math)
describe('calculateFlashnetAmountIn', () => {
describe('BTC→USD (isUsdAssetIn = false)', () => {
it('applies default 1% buffer to sats', () => {
const result = calculateFlashnetAmountIn({
baseAmountIn: 10_000,
isUsdAssetIn: false,
maxBalance: 100_000,
});
expect(result).toBe(Math.round(10_000 * SEND_AMOUNT_INCREASE_BUFFER));
});
it('caps to maxBalance when buffer exceeds it', () => {
const result = calculateFlashnetAmountIn({
baseAmountIn: 99_999,
isUsdAssetIn: false,
maxBalance: 100_000,
});
expect(result).toBe(100_000);
});
it('accepts a custom bufferMultiplier', () => {
const result = calculateFlashnetAmountIn({
baseAmountIn: 10_000,
isUsdAssetIn: false,
maxBalance: 100_000,
bufferMultiplier: 1.02,
});
expect(result).toBe(10_200);
});
it('returns integer', () => {
const result = calculateFlashnetAmountIn({
baseAmountIn: 9999,
isUsdAssetIn: false,
maxBalance: 50_000,
});
expect(Number.isInteger(result)).toBe(true);
});
});
describe('USD→BTC (isUsdAssetIn = true)', () => {
// 1_000_000 microdollars = $1.00
it('applies default 1% buffer and returns microdollars', () => {
// $10.00 in microdollars; PRICE = 1_000_000 means 1 sat = $1.00
// dollarBalanceSat = 1_000_000 → satsToDollars(1_000_000, 1_000_000) = $1_000_000
const base = 10_000_000;
const result = calculateFlashnetAmountIn({
baseAmountIn: base,
isUsdAssetIn: true,
dollarBalanceSat: 1_000_000,
currentPriceAInB: PRICE,
});
// buffer: 10.00 * 1.01 = 10.10 → 10_100_000 microdollars
expect(result).toBe(10_100_000);
});
it('caps to balance when buffer exceeds it (low balance scenario)', () => {
// base = $10 but USD balance only covers $5
// dollarBalanceSat = 5 sats, PRICE = 1_000_000 → satsToDollars(5, 1_000_000) = 5
const result = calculateFlashnetAmountIn({
baseAmountIn: 10_000_000, // $10
isUsdAssetIn: true,
dollarBalanceSat: 5, // 5 sats → $5 at PRICE
currentPriceAInB: PRICE,
});
// bufferedDollars = 10_000_000 * 1.01 / 1e6 = 10.10
// balanceDollars = satsToDollars(5, 1_000_000) = 5 * 1_000_000 / 1_000_000 = 5
// cappedDollars = min(10.10, 5) = 5.00
expect(result).toBe(5_000_000);
});
it('falls back to maxBalance/1e6 when dollarBalanceSat and currentPriceAInB are absent', () => {
const result = calculateFlashnetAmountIn({
baseAmountIn: 5_000_000, // $5.00
isUsdAssetIn: true,
maxBalance: 10_000_000, // $10.00 in microdollars
});
// 5.00 * 1.01 = 5.05 → 5_050_000
expect(result).toBe(5_050_000);
});
it('returns integer', () => {
const result = calculateFlashnetAmountIn({
baseAmountIn: 3_333_333,
isUsdAssetIn: true,
maxBalance: 100_000_000,
});
expect(Number.isInteger(result)).toBe(true);
});
});
});
@@ -48,7 +48,7 @@ export default function AnalyticsBudgetPage() {
const { masterInfoObject } = useGlobalContextProvider();
const { fiatStats } = useNodeContext();
const { bottomPadding } = useGlobalInsets();
const { textColor, backgroundOffset } = GetThemeColors();
const { textColor, backgroundOffset, backgroundColor } = GetThemeColors();
const { theme, darkModeType } = useGlobalThemeContext();
const { spentTotal, spentTxCount } = useAnalytics();
const { t } = useTranslation();
@@ -205,18 +205,13 @@ export default function AnalyticsBudgetPage() {
<FormattedSatText
balance={budgetAmount}
globalBalanceDenomination={userBalanceDenomination}
styles={[styles.statsValue, { color: COLORS.primary }]}
styles={styles.statsValue}
neverHideBalance={true}
/>
</View>
</View>
<View
style={[
styles.divider,
{ backgroundColor: textColor, opacity: 0.1 },
]}
/>
<View style={[styles.divider, { backgroundColor }]} />
{/* Spent this month */}
<View style={styles.statsRow}>
@@ -238,17 +233,12 @@ export default function AnalyticsBudgetPage() {
<FormattedSatText
balance={spentTotal}
globalBalanceDenomination={userBalanceDenomination}
styles={[styles.statsValue, { color: textColor }]}
styles={styles.statsValue}
neverHideBalance={true}
/>
</View>
<View
style={[
styles.divider,
{ backgroundColor: textColor, opacity: 0.1 },
]}
/>
<View style={[styles.divider, { backgroundColor }]} />
{/* Left to spend */}
<View style={styles.statsRow}>
@@ -265,7 +255,7 @@ export default function AnalyticsBudgetPage() {
<FormattedSatText
balance={leftToSpend}
globalBalanceDenomination={userBalanceDenomination}
styles={[styles.statsValue, { color: textColor }]}
styles={styles.statsValue}
neverHideBalance={true}
/>
</View>
@@ -297,13 +287,17 @@ const styles = StyleSheet.create({
statusText: {
fontSize: SIZES.smedium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
},
statusDivider: {
fontSize: SIZES.smedium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
},
statusPeriod: {
fontSize: SIZES.smedium,
fontFamily: FONT.Title_Regular,
includeFontPadding: false,
},
circleContainer: {
width: CIRCLE_SIZE,
@@ -329,10 +323,6 @@ const styles = StyleSheet.create({
includeFontPadding: false,
opacity: HIDDEN_OPACITY,
},
circleCenterAmount: {
fontSize: SIZES.large,
textAlign: 'center',
},
statsCard: {
width: '100%',
borderRadius: 16,
@@ -370,11 +360,11 @@ const styles = StyleSheet.create({
gap: 8,
},
statsValue: {
fontSize: SIZES.smedium,
textAlign: 'right',
includeFontPadding: false,
},
divider: {
height: StyleSheet.hairlineWidth,
height: 2,
marginHorizontal: 16,
},
});
@@ -34,8 +34,8 @@ export default function AnalyticsCreateBudgetPage() {
const formattedPresetAmount =
userBalanceDenomination === 'fiat'
? numberConverter(existingBudget?.amount, 'fiat', 2, fiatStats)
: existingBudget?.amount;
? numberConverter(existingBudget?.amount || 0, 'fiat', 2, fiatStats)
: existingBudget?.amount || 0;
const [amountValue, setAmountValue] = useState(
existingBudget?.amount ? String(formattedPresetAmount) : '',
@@ -185,7 +185,7 @@ export default function BalancePieChart() {
<View
style={[
styles.legendRow,
{
visibleSegments.length === 2 && {
[seg.key === 'btc' ? 'marginBottom' : 'marginTop']: 10,
},
]}
@@ -258,20 +258,22 @@ const styles = StyleSheet.create({
legendLabel: {
fontSize: SIZES.smedium,
opacity: 0.6,
width: 60,
includeFontPadding: false,
},
legendAmount: {
flex: 1,
fontSize: SIZES.medium,
textAlign: 'right',
includeFontPadding: false,
},
legendPercent: {
fontSize: SIZES.smedium,
opacity: HIDDEN_OPACITY,
textAlign: 'right',
includeFontPadding: false,
},
divider: {
height: 1,
height: 2,
borderRadius: 0.5,
},
});
@@ -33,7 +33,9 @@ import CustomSearchInput from '../../../../functions/CustomElements/searchInput'
import FormattedSatText from '../../../../functions/CustomElements/satTextDisplay';
import WordsQrToggle from '../../../../functions/CustomElements/wordsQrToggle';
import { dollarsToSats } from '../../../../functions/spark/flashnet';
import { validateSplitPayment } from '../../../../functions/payments/validateSplitPayment';
import { useFlashnet } from '../../../../../context-store/flashnetContext';
import { useUserBalanceContext } from '../../../../../context-store/userBalanceContext';
import { keyboardNavigate } from '../../../../functions/customNavigation';
import ContactProfileImage from './internalComponents/profileImage';
import { useImageCache } from '../../../../../context-store/imageCache';
@@ -49,7 +51,7 @@ export default function CreateSplitBill(props) {
} = props.route.params || {};
const { t } = useTranslation();
const { theme, darkModeType } = useGlobalThemeContext();
const { backgroundOffset, textInputBackground, textInputColor } =
const { backgroundOffset, textInputBackground, textInputColor, textColor } =
GetThemeColors();
const { globalContactsInformation } = useGlobalContacts();
const { masterInfoObject } = useGlobalContextProvider();
@@ -57,7 +59,9 @@ export default function CreateSplitBill(props) {
const { cache } = useImageCache();
const { fiatStats } = useNodeContext();
const getServerTime = useServerTimeOnly();
const { poolInfoRef } = useFlashnet();
const { poolInfoRef, poolInfo, swapLimits, swapUSDPriceDollars } =
useFlashnet();
const { bitcoinBalance, dollarBalanceSat } = useUserBalanceContext();
const [memo, setMemo] = useState('');
const [totalSats, setTotalSats] = useState(0); //BTC
@@ -150,9 +154,50 @@ export default function CreateSplitBill(props) {
[totalNative, n],
);
const price = poolInfo?.currentPriceAInB ?? 0;
const totalAmountSats = useMemo(() => {
if (!isUSD) return totalNative;
if (price <= 0) return 0;
return Math.round(dollarsToSats(totalNative / 100, price));
}, [isUSD, totalNative, price]);
const {
canPayBTC,
canPayUSD,
errorMessage: balanceErrorMessage,
} = useMemo(() => {
if (paymentType !== 'send' || totalAmountSats <= 0) {
return { canPayBTC: true, canPayUSD: true, errorMessage: null };
}
return validateSplitPayment({
totalSats: totalAmountSats,
paymentCurrency,
bitcoinBalance,
dollarBalanceSat,
swapLimits,
price,
masterInfoObject,
swapUSDPriceDollars,
t,
});
}, [
paymentType,
totalAmountSats,
paymentCurrency,
bitcoinBalance,
dollarBalanceSat,
swapLimits,
price,
masterInfoObject,
swapUSDPriceDollars,
t,
]);
const canConfirm = useMemo(() => {
if (totalNative <= 0) return false;
if (!memo.trim()) return false;
if (paymentType === 'send' && !canPayBTC && !canPayUSD) return false;
if (splitMode === 'even') return perPersonNative > 0;
// custom: every contact must have a valid positive integer and sum must equal total
const allSet = selectedContacts.every(c => {
@@ -168,30 +213,49 @@ export default function CreateSplitBill(props) {
selectedContacts,
customAmounts,
customTotal,
paymentType,
canPayBTC,
canPayUSD,
]);
const buildRecipients = useCallback(() => {
return selectedContacts.map((contact, index) => {
const isLast = index === selectedContacts.length - 1;
if (isUSD) {
const amountCents =
splitMode === 'even'
? perPersonNative
: typeof customAmounts[contact.uuid] === 'number'
? customAmounts[contact.uuid]
: parseInt(customAmounts[contact.uuid] || '0', 10);
let amountCents;
if (splitMode === 'even') {
amountCents = isLast
? totalNative - perPersonNative * (selectedContacts.length - 1)
: perPersonNative;
} else {
amountCents = parseInt(customAmounts[contact.uuid] || '0', 10);
}
const amountSat = dollarsToSats(
amountCents / 100,
poolInfoRef.currentPriceAInB,
);
return { contact, amountSats: amountSat, amountCents, currency: 'USD' };
}
const amountSats =
splitMode === 'even'
? perPersonNative
: parseInt(customAmounts[contact.uuid] || '0', 10);
let amountSats;
if (splitMode === 'even') {
amountSats = isLast
? totalNative - perPersonNative * (selectedContacts.length - 1)
: perPersonNative;
} else {
amountSats = parseInt(customAmounts[contact.uuid] || '0', 10);
}
return { contact, amountSats, amountCents: null, currency: 'BTC' };
});
}, [selectedContacts, splitMode, perPersonNative, customAmounts, isUSD]);
}, [
selectedContacts,
isUSD,
splitMode,
perPersonNative,
totalNative,
customAmounts,
]);
const handleConfirm = useCallback(async () => {
if (totalNative <= 0) {
@@ -208,7 +272,7 @@ export default function CreateSplitBill(props) {
});
return;
}
if (splitMode === 'custom' && !canConfirm) {
if ((splitMode === 'custom' && !canConfirm) || perPersonNative <= 0) {
navigate.navigate('ErrorScreen', {
errorMessage: t('contacts.splitBill.errors.noAmount', {
context: 'custom',
@@ -216,7 +280,7 @@ export default function CreateSplitBill(props) {
});
return;
}
if (!canConfirm || isLoading) return;
if (isLoading) return;
const recipients = buildRecipients();
@@ -257,6 +321,11 @@ export default function CreateSplitBill(props) {
setIsLoading(false);
}
} else {
if (balanceErrorMessage) {
navigate.navigate('ErrorScreen', { errorMessage: balanceErrorMessage });
return;
}
const docIds = recipients.map(r => r.contact.uuid);
const users = await getDocsByIds('blitzWalletUsers', docIds);
@@ -294,7 +363,6 @@ export default function CreateSplitBill(props) {
);
}
}, [
canConfirm,
isLoading,
buildRecipients,
paymentType,
@@ -306,7 +374,11 @@ export default function CreateSplitBill(props) {
navigate,
t,
totalSatsInt,
totalCentsInt,
splitMode,
canConfirm,
balanceErrorMessage,
perPersonNative,
]);
const contactElements = useMemo(() => {
@@ -322,7 +394,12 @@ export default function CreateSplitBill(props) {
const amountChip = isUSD ? (
<ThemeText
styles={styles.amountChip}
styles={[
styles.amountChip,
{
color: splitMode === 'custom' ? textInputColor : textColor,
},
]}
content={
contactAmount > 0 ? `$${(contactAmount / 100).toFixed(2)}` : '$0.00'
}
@@ -330,7 +407,10 @@ export default function CreateSplitBill(props) {
) : (
<FormattedSatText
autoAdjustFontSize
styles={styles.amountChip}
styles={{
...styles.amountChip,
color: splitMode === 'custom' ? textInputColor : textColor,
}}
balance={contactAmount}
/>
);
@@ -341,6 +421,8 @@ export default function CreateSplitBill(props) {
<ContactProfileImage
uri={cache[contact.uuid]?.localUri}
updated={cache[contact.uuid]?.updated}
theme={theme}
darkModeType={darkModeType}
/>
</View>
<View style={{ flex: 1 }}>
@@ -389,6 +471,8 @@ export default function CreateSplitBill(props) {
isUSD,
textInputBackground,
t,
textColor,
textInputColor,
]);
return (
@@ -934,14 +934,19 @@ const styles = StyleSheet.create({
gap: 10,
marginBottom: CONTENT_KEYBOARD_OFFSET,
},
splitPayContainer: { flexDirection: 'row', alignItems: 'center', gap: 5 },
splitPayContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: 5,
},
splitPayIcon: {
width: 25,
height: 25,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
right: -30,
right: -25,
},
contactListLetterImage: {
height: 60,
@@ -57,6 +57,7 @@ import { useTranslation } from 'react-i18next';
import DropdownMenu from '../../../../functions/CustomElements/dropdownMenu';
import {
BTC_ASSET_ADDRESS,
calculateFlashnetAmountIn,
dollarsToSats,
executeSwap,
getUserSwapHistory,
@@ -678,24 +679,25 @@ export default function CreateGiftDuration(props) {
// and the amountOut guard below will catch it with a clean error.
const totalAmountIn =
determinePaymentMethod === 'BTC'
? Math.min(
Math.round(
? calculateFlashnetAmountIn({
// base = target output + explicit pool fee; buffer adds the integrator margin
baseAmountIn:
totalSatAmount +
Math.round(
dollarsToSats(
Number(simulation.feePaidAssetIn) / Math.pow(10, 6),
) +
totalSatAmount * (INTEGRATOR_FEE + 0.005),
),
bitcoinBalance,
)
: Math.min(
Math.round(
totalFiatAmount +
Number(simulation.feePaidAssetIn) +
totalFiatAmount * 0.005,
),
dollarBalanceToken * Math.pow(10, 6),
);
),
),
isUsdAssetIn: false,
maxBalance: bitcoinBalance,
})
: calculateFlashnetAmountIn({
// base = target output + explicit pool fee in microdollars
baseAmountIn:
totalFiatAmount + Number(simulation.feePaidAssetIn),
isUsdAssetIn: true,
maxBalance: dollarBalanceToken * Math.pow(10, 6),
});
const executionResponse = await executeSwap(currentWalletMnemoinc, {
poolId: poolInfoRef.lpPublicKey,
File diff suppressed because it is too large Load Diff
+2 -7
View File
@@ -1,14 +1,9 @@
import {getApp} from '@react-native-firebase/app';
import { utils } from '@react-native-firebase/app';
export function checkGooglePlayServices() {
try {
const app = getApp();
console.log(app.utils());
const areGoogleServicesEnabled =
app.utils().playServicesAvailability.isAvailable;
utils().playServicesAvailability.isAvailable;
return areGoogleServicesEnabled;
} catch (err) {
console.log('Error getting google services information', err);
+35 -11
View File
@@ -44,7 +44,11 @@ export async function publishMessage({
}
}
export async function publishBulkMessages(messagePayloads) {
export async function publishBulkMessages(
messagePayloads,
privateKey,
globalContactsInformation,
) {
try {
crashlyticsLogReport('Beginning to publish bulk contact messages');
@@ -61,16 +65,30 @@ export async function publishBulkMessages(messagePayloads) {
if (!success) return false;
// Push notifications are best-effort after the atomic write
for (const p of messagePayloads) {
sendPushNotification({
selectedContactUsername: p.selectedContact.uniqueName,
myProfile: p.globalContactsInformation.myProfile,
data: p.data,
privateKey: p.privateKey,
retrivedContact: p.retrivedContact,
masterInfoObject: p.masterInfoObject,
});
}
const messages = (
await Promise.all(
messagePayloads.map(async p => {
const message = await sendPushNotification({
selectedContactUsername: p.selectedContact.uniqueName,
myProfile: p.globalContactsInformation.myProfile,
data: p.data,
privateKey: p.privateKey,
retrivedContact: p.retrivedContact,
masterInfoObject: p.masterInfoObject,
returnOnly: true,
});
return message || [];
}),
)
).flat();
// fire and forget
await fetchBackend(
`bulkPushNoticications`,
{ pushNotifications: messages },
privateKey,
globalContactsInformation.myProfile.uuid,
);
return true;
} catch (err) {
@@ -86,6 +104,7 @@ export async function sendPushNotification({
privateKey,
retrivedContact,
masterInfoObject,
returnOnly = false,
}) {
try {
crashlyticsLogReport('Sending push notification');
@@ -213,6 +232,11 @@ export async function sendPushNotification({
decryptPubKey: retrivedContact.uuid,
};
}
requestData.useNewNotifications = useNewNotifications;
if (returnOnly) {
return requestData;
}
const response = await fetchBackend(
`contactsPushNotificationV${useNewNotifications ? '4' : '3'}`,
@@ -0,0 +1,101 @@
import displayCorrectDenomination from '../displayCorrectDenomination';
import { dollarsToSats, satsToDollars } from '../spark/flashnet';
/**
* Determines whether a split payment can be funded from BTC balance, USD balance,
* or neither. Used in both createSplitBill (to block navigation) and
* confirmSplitPayment (to derive valid sending options).
*
* @param {object} params
* @param {number} params.totalSats - Total payment amount in sats
* @param {string} params.paymentCurrency - 'BTC' | 'USD' what recipients receive
* @param {number} params.bitcoinBalance - User's BTC balance in sats
* @param {number} params.dollarBalanceSat - User's USD balance expressed in sats
* @param {{usd: number, bitcoin: number}} params.swapLimits - Min swap amounts
* @param {number} params.price - poolInfo.currentPriceAInB (sats per dollar)
* @param {function} params.t - i18n translation function
* @returns {{ canPayBTC: boolean, canPayUSD: boolean, errorMessage: string|null }}
*/
export function validateSplitPayment({
totalSats,
paymentCurrency,
bitcoinBalance,
dollarBalanceSat,
swapLimits,
price,
masterInfoObject,
swapUSDPriceDollars,
t,
}) {
const isUSD = paymentCurrency === 'USD';
const minUsdSwapSats =
price > 0 ? Math.round(dollarsToSats(swapLimits.usd, price)) : Infinity;
const minBtcSwapSats = swapLimits.bitcoin;
// Compare in integer USD cents to avoid fiat→sats round-trip rounding errors.
// Both user amount and limits are expressed at the same display precision (2 dp).
const totalCents =
price > 0 ? Math.round(satsToDollars(totalSats, price) * 100) : 0;
const minBtcSwapCents =
price > 0
? Math.round(satsToDollars(minBtcSwapSats, price) * 100)
: Infinity;
const minUsdSwapCents = Math.round(swapLimits.usd * 100);
const hasBtcForAmount = bitcoinBalance >= totalSats;
const hasUsdForAmount = dollarBalanceSat >= totalSats;
const aboveBtcSwapMin = totalCents >= minBtcSwapCents;
const aboveUsdSwapMin = totalCents >= minUsdSwapCents;
let canPayBTC, canPayUSD;
if (isUSD) {
// Recipients receive USD tokens: direct USD or BTC→USD swap
canPayUSD = hasUsdForAmount;
canPayBTC = hasBtcForAmount && aboveBtcSwapMin;
} else {
// Recipients receive BTC: direct BTC or USD→BTC swap
canPayBTC = hasBtcForAmount;
canPayUSD = hasUsdForAmount && aboveUsdSwapMin;
}
let errorMessage = null;
if (!canPayBTC && !canPayUSD) {
if (isUSD && hasBtcForAmount && !aboveBtcSwapMin) {
errorMessage = t('wallet.sendPages.acceptButton.swapMinimumError', {
currency1: t('constants.bitcoin_upper'),
currency2: t('constants.dollars_upper'),
amount: displayCorrectDenomination({
amount: minBtcSwapSats,
masterInfoObject: {
...masterInfoObject,
userBalanceDenomination: 'fiat',
},
fiatStats: {
value: swapUSDPriceDollars,
coin: 'USD',
},
}),
});
} else if (!isUSD && hasUsdForAmount && !aboveUsdSwapMin) {
errorMessage = t('wallet.sendPages.acceptButton.swapMinimumError', {
currency1: t('constants.dollars_upper'),
currency2: t('constants.bitcoin_upper'),
amount: displayCorrectDenomination({
amount: minUsdSwapSats,
masterInfoObject: {
...masterInfoObject,
userBalanceDenomination: 'sats',
},
fiatStats: {
value: swapUSDPriceDollars,
coin: 'USD',
},
}),
});
} else {
errorMessage = t('wallet.sendPages.acceptButton.balanceError');
}
}
return { canPayBTC, canPayUSD, errorMessage };
}
+60 -48
View File
@@ -10,10 +10,7 @@ import {
swapTokenToBitcoin,
USD_ASSET_ADDRESS,
} from './flashnet';
import {
publishMessage,
publishBulkMessages,
} from '../messaging/publishMessage';
import { publishBulkMessages } from '../messaging/publishMessage';
import { bulkUpdateSparkTransactions } from './transactions';
import getReceiveAddressAndContactForContactsPayment from '../../components/admin/homeComponents/contacts/internalComponents/getReceiveAddressAndKindForPayment';
import customUUID from '../customUUID';
@@ -173,7 +170,13 @@ export async function bulkSparkPayment(
// Nothing to fulfill if every invoice failed to generate
if (invoiceBatch.length === 0) {
return { successful: [], failed, totalPaid: 0, groupId: null };
return {
successful: [],
failed,
totalPaid: 0,
groupId: null,
error: failed[0].error,
};
}
// ── Phase 2: Single batch fulfill ─────────────────────────────────────────
@@ -198,7 +201,13 @@ export async function bulkSparkPayment(
}
if (!fulfillResult || !fulfillResult?.didWork)
return { successful: [], failed, totalPaid: 0, groupId: null };
return {
successful: [],
failed,
totalPaid: 0,
groupId: null,
error: fulfillResult.error,
};
// ── Phase 3: Match results back to recipients ──────────────────────────────
// The SDK returns { invoice, transferResponse } / { invoice, error } in each
@@ -262,17 +271,17 @@ export async function bulkSparkPayment(
currentTime,
} = notifyParams;
for (const {
contact,
amountSats,
transferId,
transfer,
contactFull,
currency,
amountCents,
} of successful) {
try {
await publishMessage({
const pushNotifications = successful.map(
({
contact,
amountSats,
transferId,
transfer,
contactFull,
currency,
amountCents,
}) => {
return {
toPubKey: contact.uuid,
fromPubKey: globalContactsInformation.myProfile.uuid,
data: {
@@ -296,16 +305,15 @@ export async function bulkSparkPayment(
retrivedContact: contactFull,
currentTime,
masterInfoObject,
});
} catch (notifyErr) {
// Notification failure is non-fatal
console.log(
'bulkSparkPayment: notification failed for',
contact.uniqueName,
notifyErr,
);
}
}
};
},
);
await publishBulkMessages(
pushNotifications,
privateKey,
globalContactsInformation,
);
}
// ── Phase 5: Persist to SQLite ─────────────────────────────────────────────
@@ -444,16 +452,16 @@ export async function bulkSparkPayment(
currentTime,
} = notifyParams;
for (const {
contact,
amountSats,
amountCents,
currency,
txHash: entryTxHash,
contactFull,
} of successful) {
try {
await publishMessage({
const pushNotifications = successful.map(
({
contact,
amountSats,
amountCents,
currency,
txHash: entryTxHash,
contactFull,
}) => {
return {
toPubKey: contact.uuid,
fromPubKey: globalContactsInformation.myProfile.uuid,
data: {
@@ -463,7 +471,7 @@ export async function bulkSparkPayment(
isRequest: false,
didSend: true,
wasSeen: null,
paymentDenomination: 'USD',
paymentDenomination: currency === 'USD' ? 'USD' : 'BTC',
amountDollars:
amountCents != null ? (amountCents / 100).toFixed(2) : null,
txid: entryTxHash,
@@ -475,15 +483,15 @@ export async function bulkSparkPayment(
retrivedContact: contactFull,
currentTime,
masterInfoObject,
});
} catch (notifyErr) {
console.log(
'bulkSparkPayment: USD notification failed for',
contact.uniqueName,
notifyErr,
);
}
}
};
},
);
await publishBulkMessages(
pushNotifications,
privateKey,
globalContactsInformation,
);
}
// ── Phase 5: Persist to SQLite ─────────────────────────────────────────────
@@ -627,7 +635,11 @@ export async function bulkPaymentRequest(recipients, memo, senderInfo) {
return { successful: [], failed };
}
const success = await publishBulkMessages(payloads);
const success = await publishBulkMessages(
payloads,
privateKey,
globalContactsInformation,
);
if (!success) {
return {
+31 -77
View File
@@ -39,23 +39,35 @@ import {
import { decode } from 'bolt11';
// ============================================
// CONSTANTS
// CONSTANTS & PURE UTILITIES
// ============================================
import {
BTC_ASSET_ADDRESS,
USD_ASSET_ADDRESS,
FLASHNET_POOL_IDENTITY_KEY,
DEFAULT_SLIPPAGE_BPS,
SEND_AMOUNT_INCREASE_BUFFER,
DEFAULT_MAX_SLIPPAGE_BPS,
INTEGRATOR_FEE,
INTEGRATOR_FEE_BPS,
satsToDollars,
dollarsToSats,
calculateFlashnetAmountIn,
} from './swapAmountUtils';
// Standard Bitcoin pubkey for pools (constant across Flashnet)
export const BTC_ASSET_ADDRESS =
'020202020202020202020202020202020202020202020202020202020202020202';
export const USD_ASSET_ADDRESS =
'3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca';
export const FLASHNET_POOL_IDENTITY_KEY =
'02894808873b896e21d29856a6d7bb346fb13c019739adb9bf0b6a8b7e28da53da';
// Default slippage tolerance
export const DEFAULT_SLIPPAGE_BPS = 100; // 1%
export const SEND_AMOUNT_INCREASE_BUFFER = 1.01; // 1%
export const DEFAULT_MAX_SLIPPAGE_BPS = 300; // 3% for lightning payments
export const INTEGRATOR_FEE = 0.005; // .5%
export {
BTC_ASSET_ADDRESS,
USD_ASSET_ADDRESS,
FLASHNET_POOL_IDENTITY_KEY,
DEFAULT_SLIPPAGE_BPS,
SEND_AMOUNT_INCREASE_BUFFER,
DEFAULT_MAX_SLIPPAGE_BPS,
INTEGRATOR_FEE,
INTEGRATOR_FEE_BPS,
satsToDollars,
dollarsToSats,
calculateFlashnetAmountIn,
};
// ============================================
// HELPER FUNCTIONS
@@ -364,7 +376,7 @@ export const simulateSwap = async (
assetInAddress,
assetOutAddress,
amountIn,
integratorFeeRateBps = 50,
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
},
) => {
try {
@@ -432,7 +444,7 @@ export const executeSwap = async (
amountIn,
minAmountOut, // Optional - will be calculated if not provided
maxSlippageBps = DEFAULT_SLIPPAGE_BPS,
integratorFeeRateBps = 50,
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
},
) => {
try {
@@ -705,7 +717,7 @@ export const getLightningPaymentQuote = async (
mnemonic,
invoice,
tokenAddress,
integratorFeeRateBps = 50,
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
maxSlippageBps = DEFAULT_MAX_SLIPPAGE_BPS,
) => {
try {
@@ -780,7 +792,7 @@ export const payLightningWithToken = async (
maxLightningFeeSats = null,
rollbackOnFailure = true,
useExistingBtcBalance = false,
integratorFeeRateBps = 50,
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
},
) => {
try {
@@ -1386,64 +1398,6 @@ export const getCurrentPrice = async (mnemonic, poolId) => {
}
};
/**
* Convert sats to dollars
* @param {string|number|bigint} sats - Amount of satoshis (100,000,000)
* @param {string|number|bigint} currentPriceAinB - Price of Bitcoin in dollars
* @returns {number} Amount in dollars
*/
export function satsToDollars(sats, currentPriceAinB) {
try {
const DOLLAR_DECIMALS = 1_000_000;
const numSats = typeof sats === 'bigint' ? Number(sats) : Number(sats || 0);
const numPrice =
typeof currentPriceAinB === 'bigint'
? Number(currentPriceAinB)
: Number(currentPriceAinB || 0);
if (isNaN(numSats) || isNaN(numPrice) || numPrice === 0) {
return 0;
}
return (numSats * numPrice) / DOLLAR_DECIMALS;
} catch (error) {
console.error('Error in satsToDollars:', error, { sats, currentPriceAinB });
return 0;
}
}
/**
* Convert dollars to sats
* @param {string|number|bigint} dollars - Amount of dollars (1,000,000)
* @param {string|number|bigint} currentPriceAinB - Price of Bitcoin in dollars
* @returns {number} Amount in sats
*/
export function dollarsToSats(dollars, currentPriceAinB) {
try {
const DOLLAR_DECIMALS = 1_000_000;
const numDollars =
typeof dollars === 'bigint' ? Number(dollars) : Number(dollars || 0);
const numPrice =
typeof currentPriceAinB === 'bigint'
? Number(currentPriceAinB)
: Number(currentPriceAinB || 0);
if (isNaN(numDollars) || isNaN(numPrice) || numPrice === 0) {
return 0;
}
return (numDollars * DOLLAR_DECIMALS) / numPrice;
} catch (error) {
console.error('Error in dollarsToSats:', error, {
dollars,
currentPriceAinB,
});
return 0;
}
}
/**
* Convert exchangeRate to fiat price
* @param {string|number|bigint} currentPriceAinB - Price of Bitcoin in dollars
+12 -17
View File
@@ -28,12 +28,12 @@ import {
import sha256Hash from '../hash';
import calculateProgressiveBracketFee from './calculateSupportFee';
import {
calculateFlashnetAmountIn,
dollarsToSats,
executeSwap,
getUserSwapHistory,
payLightningWithToken,
satsToDollars,
SEND_AMOUNT_INCREASE_BUFFER,
USD_ASSET_ADDRESS,
} from './flashnet';
import {
@@ -377,17 +377,12 @@ export const sparkPaymenWrapper = async ({
let usedUSDB = false;
if (needsSwap) {
if (usablePaymentMethod === 'USD') {
const amountInWithBuffer = Math.min(
(swapPaymentQuote.amountIn * SEND_AMOUNT_INCREASE_BUFFER) /
Math.pow(10, 6),
satsToDollars(
swapPaymentQuote.dollarBalanceSat,
poolInfoRef.currentPriceAInB,
),
);
const formatted = Math.round(
amountInWithBuffer.toFixed(2) * Math.pow(10, 6),
);
const formatted = calculateFlashnetAmountIn({
baseAmountIn: swapPaymentQuote.amountIn,
isUsdAssetIn: true,
dollarBalanceSat: swapPaymentQuote.dollarBalanceSat,
currentPriceAInB: poolInfoRef.currentPriceAInB,
});
executionResponse = await executeSwap(mnemonic, {
poolId: swapPaymentQuote.poolId,
assetInAddress: swapPaymentQuote.assetInAddress,
@@ -396,11 +391,11 @@ export const sparkPaymenWrapper = async ({
});
usedUSDB = true;
} else {
const amountInWithBuffer = Math.min(
swapPaymentQuote.amountIn * SEND_AMOUNT_INCREASE_BUFFER,
swapPaymentQuote.bitcoinBalance,
);
const formatted = Math.round(amountInWithBuffer);
const formatted = calculateFlashnetAmountIn({
baseAmountIn: swapPaymentQuote.amountIn,
isUsdAssetIn: false,
maxBalance: swapPaymentQuote.bitcoinBalance,
});
executionResponse = await executeSwap(mnemonic, {
poolId: swapPaymentQuote.poolId,
assetInAddress: swapPaymentQuote.assetInAddress,
+100
View File
@@ -0,0 +1,100 @@
// Pure math utilities for Flashnet swap amount calculations.
// No SDK imports — safe to test in Jest without mocking native modules.
export const BTC_ASSET_ADDRESS =
'020202020202020202020202020202020202020202020202020202020202020202';
export const USD_ASSET_ADDRESS =
'3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca';
export const FLASHNET_POOL_IDENTITY_KEY =
'02894808873b896e21d29856a6d7bb346fb13c019739adb9bf0b6a8b7e28da53da';
export const DEFAULT_SLIPPAGE_BPS = 100; // 1%
export const SEND_AMOUNT_INCREASE_BUFFER = 1.01; // 1%
export const DEFAULT_MAX_SLIPPAGE_BPS = 300; // 3% for lightning payments
export const INTEGRATOR_FEE = 0.005; // .5%
export const INTEGRATOR_FEE_BPS = 50;
/**
* Convert sats to dollars.
* @param {string|number|bigint} sats
* @param {string|number|bigint} currentPriceAinB - BTC price in dollars (microdollar units)
* @returns {number} Amount in dollars
*/
export function satsToDollars(sats, currentPriceAinB) {
try {
const DOLLAR_DECIMALS = 1_000_000;
const numSats = typeof sats === 'bigint' ? Number(sats) : Number(sats || 0);
const numPrice =
typeof currentPriceAinB === 'bigint'
? Number(currentPriceAinB)
: Number(currentPriceAinB || 0);
if (isNaN(numSats) || isNaN(numPrice) || numPrice === 0) return 0;
return (numSats * numPrice) / DOLLAR_DECIMALS;
} catch (error) {
console.error('Error in satsToDollars:', error, { sats, currentPriceAinB });
return 0;
}
}
/**
* Convert dollars to sats.
* @param {string|number|bigint} dollars
* @param {string|number|bigint} currentPriceAinB - BTC price in dollars (microdollar units)
* @returns {number} Amount in sats
*/
export function dollarsToSats(dollars, currentPriceAinB) {
try {
const DOLLAR_DECIMALS = 1_000_000;
const numDollars =
typeof dollars === 'bigint' ? Number(dollars) : Number(dollars || 0);
const numPrice =
typeof currentPriceAinB === 'bigint'
? Number(currentPriceAinB)
: Number(currentPriceAinB || 0);
if (isNaN(numDollars) || isNaN(numPrice) || numPrice === 0) return 0;
return (numDollars * DOLLAR_DECIMALS) / numPrice;
} catch (error) {
console.error('Error in dollarsToSats:', error, {
dollars,
currentPriceAinB,
});
return 0;
}
}
/**
* Calculate the final amountIn for a flashnet swap execution.
* Applies a buffer and caps to available balance.
* Always errs toward providing more input to avoid falling short of target output.
* Especially important when the swap output funds a subsequent payment.
*
* @param {number} baseAmountIn - In smallest units: sats (BTC path) or microdollars (USD path)
* @param {boolean} isUsdAssetIn - true = USDBTC swap, false = BTCUSD swap
* @param {number} [maxBalance] - Balance cap in same units as baseAmountIn (required for BTC
* path; USD path can use dollarBalanceSat + currentPriceAInB)
* @param {number} [dollarBalanceSat] - USD balance expressed in sats (USD path precision cap)
* @param {number} [currentPriceAInB] - Pool price, required when dollarBalanceSat is provided
* @param {number} [bufferMultiplier] - Overshoot factor; defaults to SEND_AMOUNT_INCREASE_BUFFER
* @returns {number} Integer amountIn ready for executeSwap
*/
export function calculateFlashnetAmountIn({
baseAmountIn,
isUsdAssetIn,
maxBalance,
dollarBalanceSat,
currentPriceAInB,
bufferMultiplier = SEND_AMOUNT_INCREASE_BUFFER,
}) {
if (isUsdAssetIn) {
// Work in dollars for cent-level precision, then convert back to microdollars.
const bufferedDollars = (baseAmountIn * bufferMultiplier) / Math.pow(10, 6);
const balanceDollars =
dollarBalanceSat != null && currentPriceAInB != null
? satsToDollars(dollarBalanceSat, currentPriceAInB)
: maxBalance / Math.pow(10, 6);
const cappedDollars = Math.min(bufferedDollars, balanceDollars);
return Math.round(parseFloat(cappedDollars.toFixed(2)) * Math.pow(10, 6));
}
// BTC path: stay in sats
return Math.round(Math.min(baseAmountIn * bufferMultiplier, maxBalance));
}
+1 -1
View File
@@ -20,7 +20,7 @@ export async function transformTxToPaymentObject(
sparkAddress,
forcePaymentType,
isRestore,
unpaidLNInvoices,
unpaidLNInvoices = [],
identityPubKey,
numTxsBeingRestored = 1,
forceOutgoing = false,
+17 -7
View File
@@ -7,7 +7,23 @@ import { NEAR_BUDGET_LIMIT, OVER_BUDGET_LIMIT } from '../constants';
// accepting them as parameters (test file defines its own copy inline).
export function computeBudgetWarning(budget, spentTotal) {
if (!budget || !budget.amount || budget.amount <= 0) {
try {
if (!budget || !budget.amount || budget.amount <= 0) {
return {
shouldWarn: false,
isOverBudget: false,
spentPercent: 0,
leftToSpend: 0,
};
}
const budgetAmount = budget.amount;
const spentPercent = spentTotal / budgetAmount;
const leftToSpend = Math.max(budgetAmount - spentTotal, 0);
const shouldWarn = spentPercent >= NEAR_BUDGET_LIMIT;
const isOverBudget = spentPercent >= OVER_BUDGET_LIMIT;
return { shouldWarn, isOverBudget, spentPercent, leftToSpend };
} catch (err) {
console.log('compute budget warning error', err);
return {
shouldWarn: false,
isOverBudget: false,
@@ -15,12 +31,6 @@ export function computeBudgetWarning(budget, spentTotal) {
leftToSpend: 0,
};
}
const budgetAmount = budget.amount;
const spentPercent = spentTotal / budgetAmount;
const leftToSpend = Math.max(budgetAmount - spentTotal, 0);
const shouldWarn = spentPercent >= NEAR_BUDGET_LIMIT;
const isOverBudget = spentPercent >= OVER_BUDGET_LIMIT;
return { shouldWarn, isOverBudget, spentPercent, leftToSpend };
}
export function useBudgetWarning(sendingAmount = 0) {
+1
View File
@@ -316,6 +316,7 @@ const styles = StyleSheet.create({
budgetStatusLabel: {
fontSize: SIZES.smedium,
fontWeight: '500',
includeFontPadding: false,
},
progressBarTrack: {
height: 6,
+45 -3
View File
@@ -48,8 +48,8 @@ export default function ConfirmTxPage(props) {
const [isAddingContact, setIsAddingContact] = useState(false);
const isLNURLAuth = props.route.params?.useLNURLAuth;
const transaction = props.route.params?.transaction;
const hasError = props.route.params?.error;
const paymentInformation = transaction?.details;
const paymentInformation = transaction?.details || {};
const hasError = props.route.params?.error || !paymentInformation;
const lnurlAddress = normalizeLNURLAddress(props.route.params?.lnurlAddress);
const isBlitzAddress = isBlitzLNURLAddress(lnurlAddress);
const lnurlUsername = lnurlAddress?.split('@')[0]?.toLowerCase();
@@ -87,7 +87,7 @@ export default function ConfirmTxPage(props) {
const paymentFee = paymentInformation?.fee;
const errorMessage = hasError;
const errorMessage = hasError || t('errormessages.genericError');
const amount = paymentInformation?.amount || 0;
@@ -200,6 +200,48 @@ export default function ConfirmTxPage(props) {
);
}
if (props.route.params?.isSplitPayment && props.route.params?.isRequset) {
return (
<GlobalThemeView useStandardWidth={true} styles={styles.globalConatianer}>
<LottieView
ref={animationRef}
source={didSucceed ? confirmAnimation : errorAnimation}
loop={false}
style={{
width: screenDimensions.width / 1.5,
height: screenDimensions.width / 1.5,
maxWidth: 400,
maxHeight: 400,
}}
/>
<ThemeText
styles={{ fontSize: SIZES.large, marginBottom: 10 }}
content={t('screens.inAccount.confirmTxPage.bulkSuccess')}
/>
<CustomButton
buttonStyles={{
width: INSET_WINDOW_WIDTH,
backgroundColor: !theme ? COLORS.primary : COLORS.darkModeText,
marginTop: 'auto',
paddingHorizontal: 15,
}}
textStyles={{
...styles.buttonText,
color: !theme ? COLORS.darkModeText : COLORS.lightModeText,
}}
actionFunction={() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
navigate.popToTop();
});
});
}}
textContent={t('constants.continue')}
/>
</GlobalThemeView>
);
}
return (
<GlobalThemeView useStandardWidth={true} styles={styles.globalConatianer}>
<LottieView
+3
View File
@@ -123,6 +123,9 @@ export default function ConnectingToNodeLoadingScreen({
crashlyticsLogReport('Loaded users settings from firebase');
}
// causes error in firebase let acync wait
//https://github.com/firebase/firebase-ios-sdk/issues/15974#issuecomment-4155423268
//https://github.com/firebase/firebase-ios-sdk/pull/15991
toggleContactsPrivateKey(privateKey);
console.log(balanceSnapshot, placeholderTxs, 'balance and tx snapshot');
@@ -164,8 +164,7 @@ export default function TechnicalTransactionDetails(props) {
<>
<ThemeText
content={t(
'screens.inAccount.technicalTransactionDetails.amountBreakdown',
{ defaultValue: 'Amount Breakdown' },
'screens.inAccount.technicalTransactionDetails.transferIds',
)}
styles={styles.headerText}
/>
@@ -182,15 +181,6 @@ export default function TechnicalTransactionDetails(props) {
// BTC: raw sats passed directly.
// USD: amountCents * 10_000 = micros (matches DB amount unit and
// what FormattedSatText + formatTokensNumber expect for LRC20).
const rawAmount = isLRC20Payment
? (entry.amountCents ?? 0) * 10_000
: entry.amountSats ?? 0;
const displayBalance = isLRC20Payment
? formatTokensNumber(
rawAmount,
selectedToken?.tokenMetadata?.decimals,
)
: rawAmount;
const isLast = index === successfulGroup.length - 1;
@@ -219,15 +209,14 @@ export default function TechnicalTransactionDetails(props) {
styles={styles.infoName}
CustomNumberOfLines={1}
/>
{!details.isLRC20Payment && (
<ThemeText
content={entry?.transferId}
styles={styles.transferId}
CustomNumberOfLines={1}
/>
)}
</View>
<FormattedSatText
neverHideBalance={true}
styles={styles.infoValue}
balance={displayBalance}
useCustomLabel={isLRC20Payment}
customLabel={selectedToken?.tokenMetadata?.tokenTicker}
useMillionDenomination={true}
/>
</View>
{!isLast && (
<View style={[styles.rowDivider, { backgroundColor }]} />
@@ -271,6 +260,12 @@ const styles = StyleSheet.create({
includeFontPadding: false,
flexShrink: 1,
},
transferId: {
fontSize: SIZES.small,
opacity: HIDDEN_OPACITY,
includeFontPadding: false,
flexShrink: 1,
},
infoNameSmall: {
fontSize: SIZES.smedium,
opacity: HIDDEN_OPACITY,
+53 -28
View File
@@ -10,27 +10,35 @@ import { useFlashnet } from './flashnetContext';
import { getMonthlyTransactions } from '../app/functions/spark/transactions';
import { getSatsFromTx } from '../app/functions/getSatsFromTx';
import { buildCumulativeData } from '../app/components/admin/homeComponents/analytics/cumulativeLineChartHelpers';
import { useAppStatus } from './appStatus';
const AnalyticsContext = createContext(null);
export function AnalyticsProvider({ children }) {
const { sparkInformation } = useSparkWallet();
const { didGetToHomepage } = useAppStatus();
const { poolInfoRef } = useFlashnet();
const [inTxs, setInTxs] = useState([]);
const [outTxs, setOutTxs] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function load() {
if (!sparkInformation.identityPubKey) return;
if (!sparkInformation.identityPubKey || !didGetToHomepage) return;
setIsLoading(true);
try {
const startTime = Date.now();
const [incoming, outgoing] = await Promise.all([
getMonthlyTransactions(sparkInformation.identityPubKey, 'INCOMING'),
getMonthlyTransactions(sparkInformation.identityPubKey, 'OUTGOING'),
]);
setInTxs(incoming);
setOutTxs(outgoing);
const elapsed = Date.now() - startTime;
const minDuration = 500;
await new Promise(resolve =>
setTimeout(resolve, Math.max(60, minDuration - elapsed)),
);
} catch (e) {
console.error('AnalyticsContext load error', e);
} finally {
@@ -38,11 +46,15 @@ export function AnalyticsProvider({ children }) {
}
}
load();
}, [sparkInformation.identityPubKey, sparkInformation.transactions]);
}, [
sparkInformation.identityPubKey,
sparkInformation.transactions,
didGetToHomepage,
]);
const incomeTotal = useMemo(
() =>
inTxs.reduce((sum, tx) => {
const incomeTotal = useMemo(() => {
try {
return inTxs.reduce((sum, tx) => {
try {
return (
sum + getSatsFromTx(tx, poolInfoRef.currentPriceAInB, 'INCOMING')
@@ -50,13 +62,16 @@ export function AnalyticsProvider({ children }) {
} catch {
return sum;
}
}, 0),
[inTxs],
);
}, 0);
} catch (err) {
console.log('eror calcuating total', err);
return 0;
}
}, [inTxs]);
const spentTotal = useMemo(
() =>
outTxs.reduce((sum, tx) => {
const spentTotal = useMemo(() => {
try {
return outTxs.reduce((sum, tx) => {
try {
return (
sum + getSatsFromTx(tx, poolInfoRef.currentPriceAInB, 'OUTGOING')
@@ -64,31 +79,40 @@ export function AnalyticsProvider({ children }) {
} catch {
return sum;
}
}, 0),
[outTxs],
);
}, 0);
} catch (err) {
console.log('error calcuating spent', err);
return 0;
}
}, [outTxs]);
const cumulativeIncomeData = useMemo(
() =>
buildCumulativeData(
const cumulativeIncomeData = useMemo(() => {
try {
return buildCumulativeData(
inTxs,
undefined,
poolInfoRef.currentPriceAInB,
'INCOMING',
),
[inTxs],
);
);
} catch (err) {
console.log('error creating cumulative income data', err);
return [];
}
}, [inTxs]);
const cumulativeSpentData = useMemo(
() =>
buildCumulativeData(
const cumulativeSpentData = useMemo(() => {
try {
return buildCumulativeData(
outTxs,
undefined,
poolInfoRef.currentPriceAInB,
'OUTGOING',
),
[outTxs],
);
);
} catch (err) {
console.log('error creating cumulative spend data', err);
return [];
}
}, [outTxs]);
return (
<AnalyticsContext.Provider
@@ -111,6 +135,7 @@ export function AnalyticsProvider({ children }) {
export function useAnalytics() {
const ctx = useContext(AnalyticsContext);
if (!ctx) throw new Error('useAnalytics must be used within AnalyticsProvider');
if (!ctx)
throw new Error('useAnalytics must be used within AnalyticsProvider');
return ctx;
}
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1210"
LastUpgradeVersion = "2640"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+611 -426
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "Angebots-ID",
"destinationAddress": "Zieladresse",
"destinationAsset": "Ziel-Asset",
"destinationChain": "Ziel-Kette"
"destinationChain": "Ziel-Kette",
"transferIds": "Enthaltene Personen"
},
"viewAllTxPage": {
"title": "Transaktionen",
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "Quote ID",
"destinationAddress": "Destination Address",
"destinationAsset": "Destination Asset",
"destinationChain": "Destination Chain"
"destinationChain": "Destination Chain",
"transferIds": "People Included"
},
"viewAllTxPage": {
"title": "Transactions",
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "ID de Cotización",
"destinationAddress": "Dirección de Destino",
"destinationAsset": "Activo de Destino",
"destinationChain": "Cadena de Destino"
"destinationChain": "Cadena de Destino",
"transferIds": "Personas incluidas"
},
"viewAllTxPage": {
"title": "Transacciones",
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "ID du Devis",
"destinationAddress": "Adresse de Destination",
"destinationAsset": "Actif de Destination",
"destinationChain": "Chaîne de Destination"
"destinationChain": "Chaîne de Destination",
"transferIds": "Personnes incluses"
},
"viewAllTxPage": {
"title": "Transactions",
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "ID Quotazione",
"destinationAddress": "Indirizzo di Destinazione",
"destinationAsset": "Asset di Destinazione",
"destinationChain": "Catena di Destinazione"
"destinationChain": "Catena di Destinazione",
"transferIds": "Persone incluse"
},
"viewAllTxPage": {
"title": "Transazioni",
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "ID da cotação",
"destinationAddress": "Endereço de destino",
"destinationAsset": "Moeda de destino",
"destinationChain": "Rede de destino"
"destinationChain": "Rede de destino",
"transferIds": "Pessoas incluídas"
},
"viewAllTxPage": {
"title": "Transações",
+2 -1
View File
@@ -2038,7 +2038,8 @@
"quoteId": "ID котировки",
"destinationAddress": "Адрес назначения",
"destinationAsset": "Актив назначения",
"destinationChain": "Сеть назначения"
"destinationChain": "Сеть назначения",
"transferIds": "Включённые участники"
},
"viewAllTxPage": {
"title": "Транзакции",
+2 -1
View File
@@ -2037,7 +2037,8 @@
"quoteId": "Offert-ID",
"destinationAddress": "Destinationsadress",
"destinationAsset": "Destinations-tillgång",
"destinationChain": "Destinations-kedja"
"destinationChain": "Destinations-kedja",
"transferIds": "Inkluderade personer"
},
"viewAllTxPage": {
"title": "Transaktioner",
+13 -13
View File
@@ -25,13 +25,13 @@
"@noble/hashes": "^1.8.0",
"@noble/secp256k1": "^3.0.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-firebase/app": "^23.4.0",
"@react-native-firebase/auth": "^23.4.0",
"@react-native-firebase/crashlytics": "^23.4.0",
"@react-native-firebase/firestore": "^23.4.0",
"@react-native-firebase/functions": "^23.4.0",
"@react-native-firebase/messaging": "^23.4.0",
"@react-native-firebase/storage": "^23.4.0",
"@react-native-firebase/app": "^24.0.0",
"@react-native-firebase/auth": "^24.0.0",
"@react-native-firebase/crashlytics": "^24.0.0",
"@react-native-firebase/firestore": "^24.0.0",
"@react-native-firebase/functions": "^24.0.0",
"@react-native-firebase/messaging": "^24.0.0",
"@react-native-firebase/storage": "^24.0.0",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.3.13",
"@react-navigation/drawer": "^7.3.12",
@@ -78,7 +78,7 @@
"nostr-tools": "^2.15.0",
"react": "19.1.0",
"react-i18next": "^16.0.0",
"react-native": "0.81.5",
"react-native": "0.81.4",
"react-native-context-menu-view": "^1.19.0",
"react-native-country-flag": "^2.0.2",
"react-native-country-picker-modal": "^2.0.0",
@@ -93,7 +93,7 @@
"react-native-pdf-from-image": "^0.3.6",
"react-native-qrcode-svg": "^6.3.15",
"react-native-quick-base64": "^2.2.2",
"react-native-quick-crypto": "^1.0.16",
"react-native-quick-crypto": "0.7.17",
"react-native-reanimated": "^4.2.2",
"react-native-restart-newarch": "^1.0.85",
"react-native-safe-area-context": "~5.6.0",
@@ -119,10 +119,10 @@
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.81.5",
"@react-native/eslint-config": "0.81.5",
"@react-native/metro-config": "0.81.5",
"@react-native/typescript-config": "0.81.5",
"@react-native/babel-preset": "0.81.4",
"@react-native/eslint-config": "0.81.4",
"@react-native/metro-config": "0.81.4",
"@react-native/typescript-config": "0.81.4",
"@types/jest": "^29.5.13",
"@types/react": "~19.1.10",
"@types/react-test-renderer": "^19.1.0",
+372 -483
View File
File diff suppressed because it is too large Load Diff