Improve liquid (#890)

* remove unused imports

* remove legacy liquid navigation listener

* add extra details prop to spark receive payment wrapper

* improve robusness of liquid swaps

* bump breez sdk + change swap message

* making sure to only mark failed if payment truly failed

* improve fee
This commit is contained in:
Blake Kaufman
2026-06-04 14:32:39 -04:00
committed by GitHub
parent 924e04822b
commit 52dd4e44ec
21 changed files with 977 additions and 183 deletions
+1 -5
View File
@@ -53,10 +53,7 @@ import {
LOGIN_SECURITY_MODE_TYPE_KEY,
} from './app/constants';
import { LiquidEventProvider } from './context-store/liquidEventContext';
import {
LiquidNavigationListener,
RootstockNavigationListener,
} from './context-store/SDKNavigation';
import { RootstockNavigationListener } from './context-store/SDKNavigation';
import {
GlobalThemeProvider,
useGlobalThemeContext,
@@ -598,7 +595,6 @@ function ResetStack(): JSX.Element | null {
{/* <StatusBar style={theme ? 'light' : 'dark'} translucent={true} /> */}
<HandleLNURLPayments />
<RootstockNavigationListener />
<LiquidNavigationListener />
<ToastContainer />
<SparkConnectionManager />
{/* <EcashNavigationListener /> */}
@@ -0,0 +1,241 @@
// liquidToSparkSwap orchestrates the Liquid -> Spark auto-swap: reuse-or-mint a
// fixed-amount spark lightning invoice, insert a pending placeholder keyed by
// the invoice id, then pay it from Liquid. Dependencies are mocked so we can
// steer each branch.
jest.mock('../../../app/functions/breezLiquid', () => ({
breezLiquidPaymentWrapper: jest.fn(),
}));
jest.mock('../../../app/functions/spark/payments', () => ({
sparkReceivePaymentWrapper: jest.fn(),
}));
jest.mock('../../../app/functions/spark/transactions', () => ({
deleteUnpaidSparkLightningTransaction: jest.fn(),
getActiveLiquidSwapInvoice: jest.fn(),
getSparkTransactionBySparkId: jest.fn(),
insertSparkTransactionPlaceholders: jest.fn(),
updateSparkTransactionDetails: jest.fn(),
bulkUpdateSparkTransactions: jest.fn(),
}));
jest.mock('i18next', () => ({
__esModule: true,
default: { t: key => key },
}));
const {
breezLiquidPaymentWrapper,
} = require('../../../app/functions/breezLiquid');
const {
sparkReceivePaymentWrapper,
} = require('../../../app/functions/spark/payments');
const {
deleteUnpaidSparkLightningTransaction,
getActiveLiquidSwapInvoice,
getSparkTransactionBySparkId,
insertSparkTransactionPlaceholders,
updateSparkTransactionDetails,
bulkUpdateSparkTransactions,
} = require('../../../app/functions/spark/transactions');
const liquidToSparkSwap =
require('../../../app/functions/spark/liquidToSparkSwap').default;
const sparkInformation = { identityPubKey: 'acct-1' };
const baseArgs = {
mnemonic: 'seed words',
sparkInformation,
spendableSat: 50000,
sendWebViewRequest: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
getActiveLiquidSwapInvoice.mockResolvedValue(null);
sparkReceivePaymentWrapper.mockImplementation(({ amountSats }) => ({
didWork: true,
data: { id: `invoice-id-${amountSats}` },
invoice: `lnbc-encoded-${amountSats}`,
}));
deleteUnpaidSparkLightningTransaction.mockResolvedValue(true);
breezLiquidPaymentWrapper.mockImplementation(({ getFee }) => {
if (getFee) return Promise.resolve({ didWork: true, fee: 103 });
return Promise.resolve({ didWork: true });
});
insertSparkTransactionPlaceholders.mockResolvedValue(true);
updateSparkTransactionDetails.mockResolvedValue(true);
getSparkTransactionBySparkId.mockResolvedValue({ paymentStatus: 'pending' });
bulkUpdateSparkTransactions.mockResolvedValue(true);
});
test('mints a non-zero invoice below spendable minus fees, inserts a pending placeholder keyed by the invoice id, then pays it', async () => {
const result = await liquidToSparkSwap(baseArgs);
expect(result).toEqual({ didWork: true });
// First invoice is exploratory, second is the final amount after fee quote.
expect(sparkReceivePaymentWrapper).toHaveBeenCalledTimes(2);
const receiveArgs = sparkReceivePaymentWrapper.mock.calls[1][0];
expect(receiveArgs.paymentType).toBe('lightning');
expect(receiveArgs.amountSats).toBe(49894);
expect(
sparkReceivePaymentWrapper.mock.calls[0][0].amountSats +
34 +
19 +
Math.ceil(
sparkReceivePaymentWrapper.mock.calls[0][0].amountSats * 0.001,
) +
Math.ceil(sparkReceivePaymentWrapper.mock.calls[0][0].amountSats * 0.01) +
2,
).toBeLessThan(baseArgs.spendableSat);
expect(receiveArgs.extraDetails.isLiquidSwapCandidate).toBe(true);
expect(typeof receiveArgs.extraDetails.swapExpiresAt).toBe('number');
expect(deleteUnpaidSparkLightningTransaction).toHaveBeenCalledWith(
'invoice-id-49400',
);
expect(updateSparkTransactionDetails).toHaveBeenCalledWith(
'invoice-id-49894',
expect.objectContaining({
isLiquidSwap: true,
isLiquidSwapCandidate: false,
swapInvoice: 'lnbc-encoded-49894',
swapAmountSat: 49894,
swapFeeSat: 103,
fee: 103,
}),
);
// Placeholder inserted, pending, keyed by the invoice id.
expect(insertSparkTransactionPlaceholders).toHaveBeenCalledTimes(1);
const [[placeholder]] = insertSparkTransactionPlaceholders.mock.calls[0];
expect(placeholder.id).toBe('invoice-id-49894');
expect(placeholder.accountId).toBe('acct-1');
expect(placeholder.paymentStatus).toBe('pending');
expect(placeholder.paymentType).toBe('lightning');
expect(placeholder.details.isLiquidSwap).toBe(true);
expect(placeholder.details.amount).toBe(49894);
expect(placeholder.details.fee).toBe(103);
// Quoted the invoice, then paid the final fixed-amount invoice without drain.
expect(breezLiquidPaymentWrapper).toHaveBeenLastCalledWith({
paymentType: 'lightning',
invoice: 'lnbc-encoded-49894',
});
});
test('reuses an active swap invoice instead of minting a new one', async () => {
getActiveLiquidSwapInvoice.mockResolvedValue({
sparkID: 'reused-invoice-id',
amount: 49400,
details: {
isLiquidSwap: true,
swapInvoice: 'lnbc-reused',
swapAmountSat: 49400,
},
});
const result = await liquidToSparkSwap(baseArgs);
expect(result).toEqual({ didWork: true });
expect(sparkReceivePaymentWrapper).not.toHaveBeenCalled();
expect(breezLiquidPaymentWrapper).toHaveBeenLastCalledWith({
paymentType: 'lightning',
invoice: 'lnbc-reused',
});
const [[placeholder]] = insertSparkTransactionPlaceholders.mock.calls[0];
expect(placeholder.id).toBe('reused-invoice-id');
expect(placeholder.details.amount).toBe(49400);
expect(placeholder.details.fee).toBe(103);
});
test('a concurrent call is rejected while a swap is in progress', async () => {
// Park the first call on its very first await (after the lock is taken).
let releaseLookup;
getActiveLiquidSwapInvoice.mockImplementation(
() =>
new Promise(resolve => {
releaseLookup = () => resolve(null);
}),
);
const first = liquidToSparkSwap(baseArgs);
await Promise.resolve();
const second = await liquidToSparkSwap(baseArgs);
expect(second.didWork).toBe(false);
expect(second.error).toMatch(/in progress/i);
releaseLookup();
await first;
// Only the first call did real work; it still performs the quote-adjust cycle.
expect(sparkReceivePaymentWrapper).toHaveBeenCalledTimes(2);
});
test('marks the placeholder failed when the Liquid payment fails', async () => {
breezLiquidPaymentWrapper.mockImplementation(({ getFee }) => {
if (getFee) return Promise.resolve({ didWork: true, fee: 103 });
return Promise.resolve({
didWork: false,
error: { message: 'boltz rejected' },
});
});
const result = await liquidToSparkSwap(baseArgs);
expect(result.didWork).toBe(false);
expect(bulkUpdateSparkTransactions).toHaveBeenCalledTimes(1);
const [[failedTx]] = bulkUpdateSparkTransactions.mock.calls[0];
expect(failedTx.id).toBe('invoice-id-49894');
expect(failedTx.accountId).toBe('acct-1');
expect(failedTx.paymentStatus).toBe('failed');
});
test('uses a fee-aware retry amount after a quote failure instead of dropping by 10%', async () => {
let quoteCalls = 0;
breezLiquidPaymentWrapper.mockImplementation(({ getFee }) => {
if (getFee) {
quoteCalls += 1;
if (quoteCalls === 1) {
return Promise.resolve({
didWork: false,
error: { message: 'amount too high' },
});
}
return Promise.resolve({ didWork: true, fee: 103 });
}
return Promise.resolve({ didWork: true });
});
const result = await liquidToSparkSwap({
...baseArgs,
spendableSat: 1_000_000,
});
expect(result).toEqual({ didWork: true });
expect(sparkReceivePaymentWrapper).toHaveBeenCalledTimes(3);
expect(sparkReceivePaymentWrapper.mock.calls[0][0].amountSats).toBe(989063);
expect(sparkReceivePaymentWrapper.mock.calls[1][0].amountSats).toBe(967195);
expect(sparkReceivePaymentWrapper.mock.calls[2][0].amountSats).toBe(989062);
expect(
sparkReceivePaymentWrapper.mock.calls[1][0].amountSats,
).toBeGreaterThan(900000);
expect(deleteUnpaidSparkLightningTransaction).toHaveBeenCalledWith(
'invoice-id-989063',
);
});
test('does nothing destructive when the spark wallet is not connected', async () => {
const result = await liquidToSparkSwap({
...baseArgs,
sparkInformation: {},
});
expect(result.didWork).toBe(false);
expect(sparkReceivePaymentWrapper).not.toHaveBeenCalled();
expect(insertSparkTransactionPlaceholders).not.toHaveBeenCalled();
expect(breezLiquidPaymentWrapper).not.toHaveBeenCalled();
// No placeholder id yet, so no failed-cleanup write either.
expect(bulkUpdateSparkTransactions).not.toHaveBeenCalled();
});
@@ -196,6 +196,57 @@ describe('Spark transaction bulk update guards', () => {
});
});
it('only overwrites an existing fee when the incoming fee is larger', async () => {
const mockDb = createMockDb();
mockDb.getAllAsync.mockResolvedValue([
{
sparkID: 'liquid-swap',
paymentStatus: 'pending',
paymentType: 'lightning',
accountId: 'identity-pubkey',
details: JSON.stringify({
amount: 49254,
fee: 250,
direction: 'INCOMING',
}),
},
]);
const { bulkUpdateSparkTransactions } = loadTransactionsModule(mockDb);
await bulkUpdateSparkTransactions([
{
id: 'liquid-swap',
paymentStatus: 'completed',
paymentType: 'lightning',
accountId: 'identity-pubkey',
details: {
amount: 49254,
fee: 0,
direction: 'INCOMING',
},
},
]);
let updateCall = findUpdateCall(mockDb);
expect(JSON.parse(updateCall[1][3]).fee).toBe(250);
mockDb.runAsync.mockClear();
await bulkUpdateSparkTransactions([
{
id: 'liquid-swap',
paymentStatus: 'completed',
paymentType: 'lightning',
accountId: 'identity-pubkey',
details: {
fee: 300,
},
},
]);
updateCall = findUpdateCall(mockDb);
expect(JSON.parse(updateCall[1][3]).fee).toBe(300);
});
it('runs SAR correlation before BEGIN and preserves its description on update', async () => {
const callOrder = [];
const mockDb = createMockDb();
@@ -0,0 +1,145 @@
// When a settled lightning payment matches a Liquid-swap request, the payment
// object must carry useTempId/tempId so bulkUpdateSparkTransactions remaps the
// pre-inserted pending placeholder (keyed by the invoice id) onto the final
// spark id instead of creating a duplicate row.
jest.mock('bolt11', () => ({ decode: jest.fn() }));
jest.mock('../../../app/functions/spark/index', () => ({
getSparkPaymentStatus: jest.fn(() => 'completed'),
sparkPaymentType: jest.fn(() => 'lightning'),
}));
jest.mock('../../../app/functions/spark/calculateSupportFee', () => ({
__esModule: true,
default: jest.fn(async () => 0),
}));
jest.mock('../../../app/functions/spark/transactions', () => ({
deleteUnpaidSparkLightningTransaction: jest.fn(),
getActiveAutoSwapByAmount: jest.fn(),
updateSparkTransactionDetails: jest.fn(),
}));
jest.mock('../../../app/functions/spark/flashnet', () => ({
FLASHNET_POOL_IDENTITY_KEY: 'pool-key',
getActiveSwapTransferIds: jest.fn(async () => []),
getUserSwapHistory: jest.fn(async () => []),
}));
jest.mock('../../../app/functions/spark/handleFlashnetTransferIds', () => ({
setFlashnetTransfer: jest.fn(),
}));
jest.mock('i18next', () => ({
__esModule: true,
default: { t: key => key },
}));
const {
deleteUnpaidSparkLightningTransaction,
} = require('../../../app/functions/spark/transactions');
const {
transformTxToPaymentObject,
} = require('../../../app/functions/spark/transformTxToPayment');
test('liquid-swap match remaps the placeholder via useTempId/tempId', async () => {
const tx = {
totalValue: 49500,
status: 'TRANSFER_STATUS_COMPLETED',
transferDirection: 'INCOMING',
receiverIdentityPublicKey: 'acct-1',
senderIdentityPublicKey: 'someone',
createdTime: '2026-06-03T00:00:00.000Z',
transfer: { sparkId: 'final-spark-id' },
userRequest: {
id: 'invoice-id-1',
typename: 'LightningReceiveRequest',
invoice: { encodedInvoice: '' },
},
};
const unpaidLNInvoices = [
{
sparkID: 'invoice-id-1',
description: 'Liquid swap',
shouldNavigate: 0,
details: JSON.stringify({
isLiquidSwap: true,
createdTime: 111,
fee: 250,
swapFeeSat: 250,
}),
},
];
const result = await transformTxToPaymentObject(
tx,
'spark-addr',
'lightning',
false,
unpaidLNInvoices,
'acct-1',
1,
false,
[],
'seed words',
);
expect(result.id).toBe('final-spark-id');
expect(result.useTempId).toBe(true);
expect(result.tempId).toBe('invoice-id-1');
expect(result.paymentStatus).toBe('completed');
expect(result.paymentType).toBe('lightning');
expect(result.details.isLiquidSwap).toBe(true);
expect(result.details.fee).toBe(250);
expect(result.details.totalFee).toBe(250);
expect(result.details.amount).toBe(49500);
// It is not a USD swap, so the request row is cleaned up once claimed.
expect(deleteUnpaidSparkLightningTransaction).toHaveBeenCalledWith(
'invoice-id-1',
);
});
test('a normal (non-liquid) lightning match does not set tempId', async () => {
const tx = {
totalValue: 1000,
status: 'TRANSFER_STATUS_COMPLETED',
transferDirection: 'INCOMING',
receiverIdentityPublicKey: 'acct-1',
senderIdentityPublicKey: 'someone',
createdTime: '2026-06-03T00:00:00.000Z',
transfer: { sparkId: 'final-spark-id-2' },
userRequest: {
id: 'invoice-id-2',
typename: 'LightningReceiveRequest',
invoice: { encodedInvoice: '' },
},
};
const unpaidLNInvoices = [
{
sparkID: 'invoice-id-2',
description: 'normal',
shouldNavigate: 0,
details: JSON.stringify({ createdTime: 222 }),
},
];
const result = await transformTxToPaymentObject(
tx,
'spark-addr',
'lightning',
false,
unpaidLNInvoices,
'acct-1',
1,
false,
[],
'seed words',
);
expect(result.id).toBe('final-spark-id-2');
expect(result.useTempId).toBeUndefined();
expect(result.tempId).toBeUndefined();
});
@@ -1,4 +1,3 @@
// import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
import {
MIN_USD_BTC_LIGHTNING_SWAP,
SATSPERBITCOIN,
@@ -1,45 +0,0 @@
// import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
// import {SATSPERBITCOIN} from '../../../../../constants';
// import {
// crashlyticsLogReport,
// crashlyticsRecordErrorReport,
// } from '../../../../../functions/crashlyticsLogs';
// export default function processBolt12Offer(input, context) {
// const {
// nodeInformation,
// masterInfoObject,
// navigate,
// goBackFunction,
// comingFromAccept,
// enteredPaymentInfo,
// } = context;
// try {
// crashlyticsLogReport('Handling decode bolt12 offers');
// const amountMsat = comingFromAccept ? enteredPaymentInfo.amount * 1000 : 0;
// const fiatValue =
// !!amountMsat &&
// Number(amountMsat / 1000) /
// (SATSPERBITCOIN / (nodeInformation.fiatStats?.value || 65000));
// return {
// data: input,
// type: InputTypeVariant.BOLT12_OFFER,
// paymentNetwork: 'lightning',
// sendAmount: !amountMsat
// ? ''
// : `${
// masterInfoObject.userBalanceDenomination != 'fiat'
// ? `${Math.round(amountMsat / 1000)}`
// : fiatValue < 0.01
// ? ''
// : `${fiatValue.toFixed(2)}`
// }`,
// canEditPayment: comingFromAccept ? false : !amountMsat,
// };
// } catch (err) {
// console.log('process bolt12 invoice error', err);
// crashlyticsRecordErrorReport(err.message);
// }
// }
@@ -1,4 +1,3 @@
// import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
import {
MIN_USD_BTC_LIGHTNING_SWAP,
SATSPERBITCOIN,
@@ -14,10 +14,12 @@ import CustomButton from '../../../../../functions/CustomElements/button';
import FormattedSatText from '../../../../../functions/CustomElements/satTextDisplay';
import SkeletonTextPlaceholder from '../../../../../functions/CustomElements/skeletonTextView';
import { useAppStatus } from '../../../../../../context-store/appStatus';
import { useGlobalContactsInfo } from '../../../../../../context-store/globalContacts';
import displayCorrectDenomination from '../../../../../functions/displayCorrectDenomination';
import { useGlobalContextProvider } from '../../../../../../context-store/context';
import { useNodeContext } from '../../../../../../context-store/nodeContext';
import { useSparkWallet } from '../../../../../../context-store/sparkContext';
import { useKeysContext } from '../../../../../../context-store/keys';
import { useWebView } from '../../../../../../context-store/webViewContext';
import liquidToSparkSwap from '../../../../../functions/spark/liquidToSparkSwap';
import { useTranslation } from 'react-i18next';
import GetThemeColors from '../../../../../hooks/themeColors';
@@ -29,7 +31,9 @@ import {
export default function LiquidSwapsPage() {
const { minMaxLiquidSwapAmounts } = useAppStatus();
const { globalContactsInformation } = useGlobalContactsInfo();
const { sparkInformation } = useSparkWallet();
const { accountMnemoinc } = useKeysContext();
const { sendWebViewRequest } = useWebView();
const { masterInfoObject, toggleMasterInfoObject } =
useGlobalContextProvider();
const { fiatStats } = useNodeContext();
@@ -101,9 +105,12 @@ export default function LiquidSwapsPage() {
try {
setIsSwapping(true);
const response = await liquidToSparkSwap(
globalContactsInformation.myProfile.uniqueName,
);
const response = await liquidToSparkSwap({
mnemonic: accountMnemoinc,
sparkInformation,
spendableSat,
sendWebViewRequest,
});
if (!response.didWork) throw new Error(t(response.error));
navigate.navigate('ErrorScreen', {
+17 -9
View File
@@ -9,7 +9,7 @@ import {
receivePayment,
sendPayment,
} from '@breeztech/react-native-breez-sdk-liquid';
import {BLITZ_DEFAULT_PAYMENT_DESCRIPTION} from '../../constants';
import { BLITZ_DEFAULT_PAYMENT_DESCRIPTION } from '../../constants';
import {
crashlyticsLogReport,
crashlyticsRecordErrorReport,
@@ -56,7 +56,7 @@ export async function breezLiquidReceivePaymentWrapper({
});
const destination = res.destination;
return {destination, receiveFeesSat};
return { destination, receiveFeesSat };
} catch (err) {
console.log(err);
crashlyticsRecordErrorReport(err.message);
@@ -68,17 +68,21 @@ export async function breezLiquidPaymentWrapper({
sendAmount,
invoice,
shouldDrain,
getFee = false,
}) {
try {
crashlyticsLogReport('Starting liquid payment process');
let optionalAmount;
if (paymentType === 'bolt12') {
if (paymentType === 'bolt12' || sendAmount) {
optionalAmount = {
type: AmountVariant.BITCOIN,
type: PayAmountVariant.BITCOIN,
receiverAmountSat: sendAmount,
};
} else if (paymentType === 'bip21Liquid' && shouldDrain) {
} else if (
(paymentType === 'bip21Liquid' || paymentType === 'lightning') &&
shouldDrain
) {
optionalAmount = {
type: PayAmountVariant.DRAIN,
};
@@ -93,17 +97,21 @@ export async function breezLiquidPaymentWrapper({
// If the fees are acceptable, continue to create the Send Payment
const sendFeesSat = prepareResponse.feesSat;
console.log(`Fees: ${sendFeesSat} sats`);
if (getFee) {
return { fee: sendFeesSat, didWork: true, prepareResponse };
}
console.log('Sending payment');
const sendResponse = await sendPayment({
prepareResponse,
});
const payment = sendResponse.payment;
return {payment, fee: sendFeesSat, didWork: true};
return { payment, fee: sendFeesSat, didWork: true };
} catch (err) {
console.log(err);
crashlyticsRecordErrorReport(err.message);
return {error: err, didWork: false};
return { error: err, didWork: false };
}
}
@@ -144,10 +152,10 @@ export async function breezLiquidLNAddressPaymentWrapper({
});
result.data.payment;
const payment = result.data.payment;
return {payment, fee: feesSat, didWork: true};
return { payment, fee: feesSat, didWork: true };
} catch (err) {
console.log(err, 'BREEZ LIQUID TO LN ADDRESS PAYMENT WRAPPER');
crashlyticsRecordErrorReport(err.message);
return {error: err, didWork: false};
return { error: err, didWork: false };
}
}
-1
View File
@@ -2,7 +2,6 @@ import { InputTypes } from 'bitcoin-address-parser';
import { getLNAddressForLiquidPayment } from '../../components/admin/homeComponents/sendBitcoin/functions/payments';
import getLNURLDetails from '../lnurl/getLNURLDetails';
import { sparkPaymenWrapper } from '../spark/payments';
// import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
/**
* Pay to a Lightning address using the most efficient available payment method
@@ -1,4 +1,3 @@
// import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
import { InputTypes } from 'bitcoin-address-parser';
const merchants = [
+394 -41
View File
@@ -1,51 +1,404 @@
// import {InputTypeVariant} from '@breeztech/react-native-breez-sdk-liquid';
import {breezLiquidLNAddressPaymentWrapper} from '../breezLiquid';
import i18next from 'i18next';
import getLNURLDetails from '../lnurl/getLNURLDetails';
import {InputTypes} from 'bitcoin-address-parser';
import { breezLiquidPaymentWrapper } from '../breezLiquid';
import { sparkReceivePaymentWrapper } from './payments';
import {
bulkUpdateSparkTransactions,
deleteUnpaidSparkLightningTransaction,
getActiveLiquidSwapInvoice,
getSparkTransactionBySparkId,
hasPaidSparkLightningInvoice,
insertSparkTransactionPlaceholders,
updateSparkTransactionDetails,
} from './transactions';
export default async function liquidToSparkSwap(contactUsername) {
try {
let maxRunCount = 5;
let runCount = 0;
let parsedData = null;
while (maxRunCount > runCount && !parsedData) {
runCount += 1;
try {
const didGetData = await getLNURLDetails(
`${contactUsername}@blitzwalletapp.com`,
);
if (!didGetData) throw new Error('Unable to get lnurl data');
const parsed = {
type: InputTypes.LNURL_PAY,
data: {
...didGetData,
metadataStr: didGetData.metadata, //added for breeze
domain: 'blitzwalletapp.com', //added for breeze
},
};
parsedData = parsed;
break;
} catch (err) {
console.log('Error parsing LNURL, assuming its a backend issue');
await new Promise(res => setTimeout(res, 1000));
}
// How long a generated Liquid->Spark swap invoice stays reusable. The payment
// happens almost immediately, so this only needs to cover an app restart that
// happens mid-swap; we track it explicitly to avoid SDK timestamp-unit guessing.
const LIQUID_SWAP_INVOICE_EXPIRY_SECONDS = 60 * 60;
const LIQUID_SWAP_MAX_QUOTE_ATTEMPTS = 8;
const LIQUID_SWAP_LOCKUP_TX_FEE_SATS = 34;
const LIQUID_SWAP_CLAIM_TX_FEE_SATS = 19;
const LIQUID_SWAP_SERVICE_FEE_RATE = 0.001;
const LIQUID_SWAP_PARTNER_FEE_RATE = 0.01;
const LIQUID_SWAP_ROUNDING_BUFFER_SATS = 2;
const LIQUID_SWAP_RETRY_PERCENT_STEP = 0.1;
const LIQUID_SWAP_RETRY_FEE_MULTIPLIER = 2;
const LIQUID_SWAP_UNKNOWN_UPPER_BOUND_SATS = Number.MAX_SAFE_INTEGER;
// In-memory reentrancy guard. Liquid balance updates can fire several times in
// quick succession (sync + payment events); without this they would each kick
// off a concurrent swap and race over the same funds.
let isRunningLiquidSwap = false;
function normalizeSats(value) {
return Math.max(Math.ceil(Number(value) || 0), 0);
}
function getPercentageFeeSat(amountSat, feeRate) {
return Math.ceil(Math.max(Number(amountSat) || 0, 0) * feeRate);
}
function getModeledBaseSwapFeeSat(amountSat) {
return (
LIQUID_SWAP_LOCKUP_TX_FEE_SATS +
LIQUID_SWAP_CLAIM_TX_FEE_SATS +
getPercentageFeeSat(amountSat, LIQUID_SWAP_SERVICE_FEE_RATE)
);
}
function getPartnerFeeSat(amountSat) {
return getPercentageFeeSat(amountSat, LIQUID_SWAP_PARTNER_FEE_RATE);
}
function getEstimatedSwapFeeSat(amountSat) {
return getModeledBaseSwapFeeSat(amountSat) + getPartnerFeeSat(amountSat);
}
function getTrustedOrEstimatedFeeSat(amountSat, trustedFeeSat) {
if (trustedFeeSat === undefined || trustedFeeSat === null) {
return getEstimatedSwapFeeSat(amountSat);
}
return normalizeSats(trustedFeeSat);
}
function getTotalRequiredSat(amountSat, trustedFeeSat) {
return (
amountSat +
getTrustedOrEstimatedFeeSat(amountSat, trustedFeeSat) +
LIQUID_SWAP_ROUNDING_BUFFER_SATS
);
}
function invoiceFitsSpendable({ amountSat, feeSat, spendableSat }) {
return amountSat > 0 && getTotalRequiredSat(amountSat, feeSat) < spendableSat;
}
function getInvoiceAmountForFee(spendableSat, feeSat) {
let low = 1;
let high = Math.max(Math.floor(Number(spendableSat) || 0) - 1, 0);
let bestAmountSat = 0;
while (low <= high) {
const amountSat = Math.floor((low + high) / 2);
if (
invoiceFitsSpendable({
amountSat,
feeSat,
spendableSat,
})
) {
bestAmountSat = amountSat;
low = amountSat + 1;
} else {
high = amountSat - 1;
}
}
if (!parsedData) throw new Error('errormessages.invoiceRetrivalError');
return bestAmountSat;
}
const paymentResponse = await breezLiquidLNAddressPaymentWrapper({
description: i18next.t('swapMessages.liquid'),
paymentInfo: parsedData.data,
shouldDrain: true,
});
function getNextAmountAfterQuoteFailure({ amountSat, spendableSat }) {
const feeAwareTargetSat = getInvoiceAmountForFee(spendableSat);
if (feeAwareTargetSat > 0 && feeAwareTargetSat < amountSat) {
return feeAwareTargetSat;
}
if (!paymentResponse.didWork) throw new Error(paymentResponse.error);
const percentStepSat = Math.ceil(amountSat * LIQUID_SWAP_RETRY_PERCENT_STEP);
const feeStepSat =
getEstimatedSwapFeeSat(amountSat) * LIQUID_SWAP_RETRY_FEE_MULTIPLIER;
const reductionSat = Math.max(1, Math.min(percentStepSat, feeStepSat));
return {didWork: true};
return Math.floor(amountSat - reductionSat);
}
function getSwapError(error, fallback) {
return error?.message || error || fallback;
}
async function cleanupUnusedSwapInvoice(invoiceId) {
if (!invoiceId) return;
try {
await deleteUnpaidSparkLightningTransaction(invoiceId);
} catch (err) {
console.log(err);
return {didWork: false, error: err.message};
console.log('liquidToSparkSwap invoice cleanup error', err);
}
}
async function createSparkSwapInvoice({
amountSat,
mnemonic,
sendWebViewRequest,
}) {
const invoiceResponse = await sparkReceivePaymentWrapper({
paymentType: 'lightning',
amountSats: amountSat,
memo: i18next.t('swapMessages.liquid'),
mnemoinc: mnemonic,
sendWebViewRequest,
shouldNavigate: false,
includeSparkAddress: false,
expirySeconds: LIQUID_SWAP_INVOICE_EXPIRY_SECONDS,
extraDetails: {
isLiquidSwapCandidate: true,
swapAmountSat: amountSat,
swapExpiresAt: Date.now() + LIQUID_SWAP_INVOICE_EXPIRY_SECONDS * 1000,
},
});
if (!invoiceResponse.didWork) throw new Error(invoiceResponse.error);
return {
placeholderId: invoiceResponse.data.id,
bolt11: invoiceResponse.invoice,
amountSat,
};
}
async function quoteLiquidLightningFee(bolt11) {
const feeResponse = await breezLiquidPaymentWrapper({
paymentType: 'lightning',
invoice: bolt11,
getFee: true,
});
if (!feeResponse.didWork) {
throw new Error(
getSwapError(feeResponse.error, 'Unable to estimate Liquid swap fee'),
);
}
return Math.ceil(Number(feeResponse.fee) || 0);
}
async function getReusableSwapInvoice(spendableSat) {
const existingInvoice = await getActiveLiquidSwapInvoice();
const bolt11 = existingInvoice?.details?.swapInvoice;
const amountSat = Math.floor(
Number(existingInvoice?.details?.swapAmountSat || existingInvoice?.amount),
);
if (!existingInvoice) return null;
if (!bolt11 || !amountSat) {
await cleanupUnusedSwapInvoice(existingInvoice.sparkID);
return null;
}
try {
const feeSat = await quoteLiquidLightningFee(bolt11);
if (
invoiceFitsSpendable({
amountSat,
feeSat,
spendableSat,
})
) {
return {
placeholderId: existingInvoice.sparkID,
bolt11,
amountSat,
feeSat,
};
}
} catch (err) {
console.log('liquidToSparkSwap reusable invoice fee quote error', err);
}
await cleanupUnusedSwapInvoice(existingInvoice.sparkID);
return null;
}
async function createQuotedSwapInvoice({
spendableSat,
mnemonic,
sendWebViewRequest,
}) {
const triedAmounts = new Set();
let nextAmountSat = getInvoiceAmountForFee(spendableSat);
let upperBoundSat = LIQUID_SWAP_UNKNOWN_UPPER_BOUND_SATS;
let lastError;
for (
let attempt = 0;
attempt < LIQUID_SWAP_MAX_QUOTE_ATTEMPTS;
attempt += 1
) {
if (nextAmountSat <= 0) break;
if (triedAmounts.has(nextAmountSat)) nextAmountSat -= 1;
triedAmounts.add(nextAmountSat);
const invoice = await createSparkSwapInvoice({
amountSat: nextAmountSat,
mnemonic,
sendWebViewRequest,
});
let feeSat;
try {
feeSat = await quoteLiquidLightningFee(invoice.bolt11);
} catch (err) {
lastError = err;
await cleanupUnusedSwapInvoice(invoice.placeholderId);
upperBoundSat = Math.min(upperBoundSat, nextAmountSat - 1);
nextAmountSat = getNextAmountAfterQuoteFailure({
amountSat: nextAmountSat,
spendableSat,
});
nextAmountSat = Math.min(nextAmountSat, upperBoundSat);
continue;
}
const targetAmountSat = Math.min(
getInvoiceAmountForFee(spendableSat, feeSat),
upperBoundSat,
);
if (targetAmountSat <= 0) {
await cleanupUnusedSwapInvoice(invoice.placeholderId);
throw new Error('Insufficient Liquid balance to cover swap fees');
}
if (
nextAmountSat !== targetAmountSat &&
!triedAmounts.has(targetAmountSat)
) {
await cleanupUnusedSwapInvoice(invoice.placeholderId);
nextAmountSat = targetAmountSat;
continue;
}
if (
invoiceFitsSpendable({
amountSat: nextAmountSat,
feeSat,
spendableSat,
})
) {
return { ...invoice, feeSat };
}
await cleanupUnusedSwapInvoice(invoice.placeholderId);
nextAmountSat = Math.min(targetAmountSat, nextAmountSat - 1);
}
throw new Error(
getSwapError(lastError, 'Unable to create a payable Liquid swap invoice'),
);
}
async function persistSwapInvoiceDetails({
placeholderId,
bolt11,
amountSat,
feeSat,
}) {
await updateSparkTransactionDetails(placeholderId, {
isLiquidSwap: true,
isLiquidSwapCandidate: false,
swapInvoice: bolt11,
swapAmountSat: amountSat,
swapFeeSat: feeSat,
fee: feeSat,
swapExpiresAt: Date.now() + LIQUID_SWAP_INVOICE_EXPIRY_SECONDS * 1000,
});
}
// Sweeps spendable Liquid funds into Spark by paying a locally
// generated fixed-amount Spark lightning invoice. Because we mint the invoice
// ourselves we know its id up front, so we insert a pending placeholder
// transaction immediately (visible in history) and the existing Spark
// reconciliation updates that same row when the payment settles.
export default async function liquidToSparkSwap({
mnemonic,
sparkInformation,
spendableSat,
sendWebViewRequest,
}) {
if (isRunningLiquidSwap) {
return { didWork: false, error: 'Liquid swap already in progress' };
}
isRunningLiquidSwap = true;
const accountId = sparkInformation?.identityPubKey;
let placeholderId;
let swapInvoice;
try {
if (!accountId) throw new Error('Spark wallet not connected');
const normalizedSpendableSat = Math.floor(Number(spendableSat) || 0);
if (normalizedSpendableSat <= 1) {
throw new Error('Insufficient Liquid balance to cover swap fees');
}
swapInvoice =
(await getReusableSwapInvoice(normalizedSpendableSat)) ||
(await createQuotedSwapInvoice({
spendableSat: normalizedSpendableSat,
mnemonic,
sendWebViewRequest,
}));
placeholderId = swapInvoice.placeholderId;
await persistSwapInvoiceDetails(swapInvoice);
const pendingSwapTx = {
id: placeholderId,
accountId,
paymentStatus: 'pending',
paymentType: 'lightning',
details: {
direction: 'INCOMING',
isLiquidSwap: true,
amount: swapInvoice.amountSat,
fee: swapInvoice.feeSat,
time: Date.now(),
createdTime: Date.now(),
description: i18next.t('swapMessages.liquid'),
},
};
// Show the pending swap in history right away once the swap starts.
await insertSparkTransactionPlaceholders([pendingSwapTx]);
const paymentResponse = await breezLiquidPaymentWrapper({
paymentType: 'lightning',
invoice: swapInvoice.bolt11,
});
if (!paymentResponse.didWork) {
throw new Error(
getSwapError(paymentResponse.error, 'Liquid swap payment failed'),
);
}
return { didWork: true };
} catch (err) {
console.log('liquidToSparkSwap error', err);
// Don't leave the placeholder pending forever. If the payment actually went
// through despite a failed response, a later settled payment will remap
// onto this id and flip it back to completed.
if (placeholderId && accountId) {
const savedTransaction = await getSparkTransactionBySparkId(
placeholderId,
accountId,
);
if (savedTransaction.paymentStatus !== 'completed') {
try {
await bulkUpdateSparkTransactions([
{
id: placeholderId,
accountId,
paymentStatus: 'failed',
paymentType: 'lightning',
details: { isLiquidSwap: true },
},
]);
} catch (cleanupErr) {
console.log(
'liquidToSparkSwap placeholder cleanup error',
cleanupErr,
);
}
}
}
return { didWork: false, error: err.message };
} finally {
isRunningLiquidSwap = false;
}
}
+2
View File
@@ -659,6 +659,7 @@ export const sparkReceivePaymentWrapper = async ({
paymentHash,
holdExpirySeconds,
encryptedPreimage,
extraDetails = {},
}) => {
try {
// if (!sparkWallet[sha256Hash(mnemoinc)])
@@ -723,6 +724,7 @@ export const sparkReceivePaymentWrapper = async ({
shouldNavigate: true,
isBlitzContactPayment: false,
performSwaptoUSD,
...extraDetails,
},
};
await addSingleUnpaidSparkLightningTransaction(tempTransaction);
+65 -9
View File
@@ -80,6 +80,25 @@ const resolvePaymentStatusForUpdate = (
return incomingPaymentStatus;
};
const isMeaningfulDetailValue = (key, value, shouldUpdateDescription) =>
(value !== '' && value !== null && value !== undefined && value !== 0) ||
(key === 'description' && shouldUpdateDescription);
const shouldUseIncomingDetailValue = (
key,
incomingValue,
existingValue,
shouldUpdateDescription,
) => {
if (key === 'fee') {
const incomingFee = Number(incomingValue);
const existingFee = Number(existingValue) || 0;
return Number.isFinite(incomingFee) && incomingFee > existingFee;
}
return isMeaningfulDetailValue(key, incomingValue, shouldUpdateDescription);
};
export const insertSparkTransactionPlaceholders = async (
transactions,
updateType = 'transactions',
@@ -773,6 +792,40 @@ export const getAllUnpaidSparkLightningInvoices = async () => {
}
};
// Returns a still-valid Liquid->Spark swap lightning request (created by the
// auto-swap flow) so we can reuse it instead of minting a new invoice on every
// balance update or after an app restart. Validity is tracked by the explicit
// `swapExpiresAt` (ms) we store at creation time to avoid SDK timestamp-unit
// ambiguity.
export const getActiveLiquidSwapInvoice = async () => {
try {
await ensureSparkDatabaseReady();
const rows = await sqlLiteDB.getAllAsync(
`SELECT * FROM ${LIGHTNING_REQUEST_IDS_TABLE_NAME}
WHERE json_extract(details, '$.isLiquidSwap') = 1`,
);
const now = Date.now();
const active = rows.find(row => {
try {
const details = row.details ? JSON.parse(row.details) : {};
return Number(details.swapExpiresAt) > now;
} catch {
return false;
}
});
if (!active) return null;
try {
active.details = active.details ? JSON.parse(active.details) : {};
} catch {
active.details = {};
}
return active;
} catch (error) {
console.error('Error fetching active liquid swap invoice:', error);
return null;
}
};
export const getAllUnpaidHoldInvoicesFromTxs = async () => {
try {
await ensureSparkDatabaseReady();
@@ -1037,10 +1090,12 @@ export const bulkUpdateSparkTransactions = async (transactions, ...data) => {
for (const key in tx.details) {
const value = tx.details[key];
if (
value !== '' &&
value !== null &&
value !== undefined &&
value !== 0
shouldUseIncomingDetailValue(
key,
value,
mergedDetails[key],
shouldUpdateDescription,
)
) {
mergedDetails[key] = value;
}
@@ -1140,11 +1195,12 @@ export const bulkUpdateSparkTransactions = async (transactions, ...data) => {
for (const key in newDetails) {
const value = newDetails[key];
if (
(value !== '' &&
value !== null &&
value !== undefined &&
value !== 0) ||
(key === 'description' && shouldUpdateDescription)
shouldUseIncomingDetailValue(
key,
value,
merged[key],
shouldUpdateDescription,
)
) {
merged[key] = value;
}
+16 -3
View File
@@ -70,6 +70,13 @@ export async function transformTxToPaymentObject(
: undefined;
const isSwapPayment = foundInvoice && foundInvoiceDetails.performSwaptoUSD;
const isLiquidSwap = foundInvoice && foundInvoiceDetails.isLiquidSwap;
const liquidSwapFee = Number(
foundInvoiceDetails?.fee ?? foundInvoiceDetails?.swapFeeSat ?? 0,
);
const effectivePaymentFee = isLiquidSwap
? Math.max(paymentFee, Number.isFinite(liquidSwapFee) ? liquidSwapFee : 0)
: paymentFee;
if (isSwapPayment) {
updateSparkTransactionDetails(foundInvoice.sparkID, {
@@ -92,6 +99,10 @@ export async function transformTxToPaymentObject(
return {
id: tx.transfer ? tx.transfer.sparkId : tx.id,
// The Liquid->Spark auto-swap pre-inserts a pending placeholder keyed by
// the lightning request id. Remap it onto the settled payment so the
// placeholder is updated rather than duplicated.
...(isLiquidSwap && { useTempId: true, tempId: userRequestId }),
paymentStatus: isSwapPayment
? 'pending'
: status === 'completed' || preimage
@@ -101,10 +112,12 @@ export async function transformTxToPaymentObject(
accountId: accountId,
details: {
...foundInvoiceDetails,
fee: paymentFee,
totalFee: paymentFee + supportFee,
fee: effectivePaymentFee,
totalFee: effectivePaymentFee + supportFee,
supportFee: supportFee,
amount: paymentAmount - paymentFee,
amount: isLiquidSwap
? paymentAmount
: paymentAmount - effectivePaymentFee,
address: userRequest
? isSendRequest
? userRequest?.encodedInvoice
-33
View File
@@ -4,7 +4,6 @@ import { useAppStatus } from './appStatus';
import { crashlyticsLogReport } from '../app/functions/crashlyticsLogs';
import { useRootstockProvider } from './rootstockSwapContext';
import i18next from 'i18next';
import { useNodeContext } from './nodeContext';
export function RootstockNavigationListener() {
const navigation = useNavigation();
@@ -37,35 +36,3 @@ export function RootstockNavigationListener() {
return null;
}
export function LiquidNavigationListener() {
const navigation = useNavigation();
const { didGetToHomepage } = useAppStatus();
const { pendingLiquidPayment, setPendingLiquidPayment } = useNodeContext();
const isNavigating = useRef(false); // Use a ref for local state
useEffect(() => {
if (!pendingLiquidPayment) return;
if (!didGetToHomepage) {
setPendingLiquidPayment(null);
return;
}
if (isNavigating.current) return;
crashlyticsLogReport(`Navigating to confirm tx page in liquid listener `);
isNavigating.current = true;
setTimeout(() => {
requestAnimationFrame(() => {
navigation.navigate('ErrorScreen', {
errorMessage: i18next.t('errormessages.receivedLiquid'),
});
isNavigating.current = false;
console.log('cleaning up navigation for liquid');
});
}, 100);
setPendingLiquidPayment(null);
}, [pendingLiquidPayment, didGetToHomepage]);
return null;
}
+18 -14
View File
@@ -12,7 +12,7 @@ import loadNewFiatData from '../app/functions/saveAndUpdateFiatData';
import { useKeysContext } from './keys';
import { useAppStatus } from './appStatus';
import { useSparkWallet } from './sparkContext';
import { useGlobalContactsInfo } from './globalContacts';
import { useWebView } from './webViewContext';
import liquidToSparkSwap from '../app/functions/spark/liquidToSparkSwap';
import { useAuthContext } from './authContext';
import { ensureLiquidConnection } from '../app/functions/breezLiquid/liquidNodeManager';
@@ -22,12 +22,11 @@ import { SATSPERBITCOIN } from '../app/constants';
const NodeContextManager = createContext(null);
const GLobalNodeContextProider = ({ children }) => {
const { globalContactsInformation } = useGlobalContactsInfo();
const { sparkInformation } = useSparkWallet();
const { sendWebViewRequest } = useWebView();
const { contactsPrivateKey, publicKey, accountMnemoinc } = useKeysContext();
const { didGetToHomepage, minMaxLiquidSwapAmounts } = useAppStatus();
const { masterInfoObject } = useGlobalContextProvider();
const [pendingLiquidPayment, setPendingLiquidPayment] = useState(null);
const [liquidNodeInformation, setLiquidNodeInformation] = useState({
didConnectToNode: null,
transactions: [],
@@ -108,11 +107,18 @@ const GLobalNodeContextProider = ({ children }) => {
useEffect(() => {
async function swapLiquidToSpark() {
try {
if (liquidNodeInformation.userBalance > minMaxLiquidSwapAmounts.min) {
setPendingLiquidPayment(true);
await liquidToSparkSwap(
globalContactsInformation.myProfile.uniqueName,
);
// Only sweep funds that aren't already locked in an in-flight send,
// so overlapping balance updates can't trigger a double swap.
const spendableSat =
liquidNodeInformation.userBalance -
(liquidNodeInformation.pendingSend || 0);
if (spendableSat > minMaxLiquidSwapAmounts.min) {
await liquidToSparkSwap({
mnemonic: accountMnemoinc,
sparkInformation,
spendableSat,
sendWebViewRequest,
});
}
} catch (err) {
console.log('transfering liquid to spark error', err);
@@ -126,10 +132,12 @@ const GLobalNodeContextProider = ({ children }) => {
}, [
didGetToHomepage,
liquidNodeInformation.userBalance,
minMaxLiquidSwapAmounts,
liquidNodeInformation.pendingSend,
minMaxLiquidSwapAmounts.min,
sparkInformation.didConnect,
sparkInformation.identityPubKey,
globalContactsInformation?.myProfile?.uniqueName,
accountMnemoinc,
sendWebViewRequest,
masterInfoObject.enabledLiquidAutoSwap,
]);
@@ -147,8 +155,6 @@ const GLobalNodeContextProider = ({ children }) => {
toggleLiquidNodeInformation,
toggleFiatStats,
fiatStats,
pendingLiquidPayment,
setPendingLiquidPayment,
SATS_PER_DOLLAR,
}),
[
@@ -156,8 +162,6 @@ const GLobalNodeContextProider = ({ children }) => {
fiatStats,
toggleFiatStats,
toggleLiquidNodeInformation,
pendingLiquidPayment,
setPendingLiquidPayment,
SATS_PER_DOLLAR,
],
);
+8 -8
View File
@@ -1195,12 +1195,12 @@ PODS:
- BoringSSL-GRPC/Implementation (0.0.37):
- BoringSSL-GRPC/Interface (= 0.0.37)
- BoringSSL-GRPC/Interface (0.0.37)
- breez_sdk_liquid (0.11.9):
- BreezSDKLiquid (= 0.11.9)
- breez_sdk_liquid (0.11.13):
- BreezSDKLiquid (= 0.11.13)
- React-Core
- breez_sdk_liquidFFI (0.11.9)
- BreezSDKLiquid (0.11.9):
- breez_sdk_liquidFFI (= 0.11.9)
- breez_sdk_liquidFFI (0.11.13)
- BreezSDKLiquid (0.11.13):
- breez_sdk_liquidFFI (= 0.11.13)
- DoubleConversion (1.1.6)
- EXApplication (7.0.7):
- ExpoModulesCore
@@ -5553,9 +5553,9 @@ SPEC CHECKSUMS:
abseil: a05cc83bf02079535e17169a73c5be5ba47f714b
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508
breez_sdk_liquid: 9c382c355ae97f1b801d04ca09e56cece7337183
breez_sdk_liquidFFI: d7ffd1b509eb220a6a479ff188572382d0aa5c2a
BreezSDKLiquid: 609bb38df7f8e92825a022c86d37bedbe7e8d795
breez_sdk_liquid: e05cd39b19b9e029ecf50195e4e7df0aefb745cd
breez_sdk_liquidFFI: f05fadc0611126ade76d1fe6761ed8b020aabefb
BreezSDKLiquid: ee6bf5a57f1b2533dc3c14c24c9773496f17b756
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXApplication: 296622817d459f46b6c5fe8691f4aac44d2b79e7
EXConstants: a95804601ee4a6aa7800645f9b070d753b1142b3
+1 -1
View File
@@ -2665,7 +2665,7 @@
"contact": "Hi, add me on Blitz!"
},
"swapMessages": {
"liquid": "Liquid to Spark Transfer"
"liquid": "Liquid payment"
},
"tabs": {
"home": "Wallet",
+1 -1
View File
@@ -19,7 +19,7 @@
"dependencies": {
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@branta-ops/branta": "^3.1.2",
"@breeztech/react-native-breez-sdk-liquid": "^0.11.9",
"@breeztech/react-native-breez-sdk-liquid": "^0.11.13",
"@buildonspark/spark-sdk": "^0.8.0",
"@craftzdog/react-native-buffer": "^6.1.0",
"@flashnet/sdk": "^0.5.7",
+5 -5
View File
@@ -1783,13 +1783,13 @@ __metadata:
languageName: node
linkType: hard
"@breeztech/react-native-breez-sdk-liquid@npm:^0.11.9":
version: 0.11.9
resolution: "@breeztech/react-native-breez-sdk-liquid@npm:0.11.9"
"@breeztech/react-native-breez-sdk-liquid@npm:^0.11.13":
version: 0.11.13
resolution: "@breeztech/react-native-breez-sdk-liquid@npm:0.11.13"
peerDependencies:
react: "*"
react-native: "*"
checksum: cd359313669852aa36f5046a7217f64734b88ee11c581273ac1a6314c8081eb6d60d2f31785321fbe89bf3a1d8add508576d7e216287b5d5430e7ad199a1a979
checksum: 48ba269676182de36b3689f5940bfcc0a2849bac3ed641c68c6e2cf6e89df9c27b12606df3ed63b5ec2ab21ff5b7a9696384ab4d28111810dba0efb63f2c824c
languageName: node
linkType: hard
@@ -4924,7 +4924,7 @@ __metadata:
"@babel/preset-env": ^7.28.3
"@babel/runtime": ^7.25.0
"@branta-ops/branta": ^3.1.2
"@breeztech/react-native-breez-sdk-liquid": ^0.11.9
"@breeztech/react-native-breez-sdk-liquid": ^0.11.13
"@buildonspark/spark-sdk": ^0.8.0
"@craftzdog/react-native-buffer": ^6.1.0
"@flashnet/sdk": ^0.5.7