From 70b14f07fd1229d625e1005e2de7e8c9133986de Mon Sep 17 00:00:00 2001
From: Blake Kaufman <68204898+BlakeKaufman@users.noreply.github.com>
Date: Thu, 14 May 2026 19:56:44 -0400
Subject: [PATCH] Migrate ln to orchestra (#831)
* adding new translations
* migrate USD > BTC LN to orchestra
* adding orchestra tests
* updating call locations
* add exponential backoff + resubmit swap quote if first time failed
* show failed Tx and add support button
* minor fixes
* bump timeout to 1 min
* making sure usd quote doesn't block btc
---
.../spark/orchestraLightning.test.js | 81 +++++
.../contacts/sendAndRequestPage.js | 3 +
.../sendBitcoin/functions/decodeSendAdress.js | 2 +-
.../functions/processBolt11Invoice.js | 26 +-
.../sendBitcoin/functions/processLNUrlPay.js | 26 +-
.../sendBitcoin/sendPaymentScreen.js | 3 +
.../sendBitcoin/stablecoinSendScreen.js | 1 +
app/functions/combinedTransactionsSpark.js | 135 ++++-----
app/functions/spark/flashnet.js | 284 +++++++++++-------
app/functions/spark/orchestraLightning.js | 105 +++++++
app/functions/spark/payments.js | 88 ++----
app/functions/spark/restore.js | 49 ++-
app/screens/inAccount/confirmTxPage.js | 7 +-
app/screens/inAccount/expandedTxPage.js | 109 ++++---
locales/de-DE/translation.json | 6 +-
locales/en/translation.json | 6 +-
locales/es/translation.json | 6 +-
locales/fr/translation.json | 6 +-
locales/it/translation.json | 6 +-
locales/pt-BR/translation.json | 6 +-
locales/ru/translation.json | 6 +-
locales/sv/translation.json | 6 +-
22 files changed, 644 insertions(+), 323 deletions(-)
create mode 100644 __tests__/functions/spark/orchestraLightning.test.js
create mode 100644 app/functions/spark/orchestraLightning.js
diff --git a/__tests__/functions/spark/orchestraLightning.test.js b/__tests__/functions/spark/orchestraLightning.test.js
new file mode 100644
index 00000000..8b7cefdf
--- /dev/null
+++ b/__tests__/functions/spark/orchestraLightning.test.js
@@ -0,0 +1,81 @@
+import {
+ getLightningInvoiceAmountSats,
+ mapOrchestraQuoteToLightningQuote,
+ normalizeOrchestraBackendError,
+} from '../../../app/functions/spark/orchestraLightning';
+
+const FIXED_AMOUNT_INVOICE =
+ 'lnbc20u1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7kxqrrsssp5m6kmam774klwlh4dhmhaatd7al02m0h0m6kmam774klwlh4dhmhs9qypqqqcqpf3cwux5979a8j28d4ydwahx00saa68wq3az7v9jdgzkghtxnkf3z5t7q5suyq2dl9tqwsap8j0wptc82cpyvey9gf6zyylzrm60qtcqsq7egtsq';
+
+describe('orchestraLightning helpers', () => {
+ test('decodes fixed-amount BOLT11 invoice sats', () => {
+ expect(getLightningInvoiceAmountSats(FIXED_AMOUNT_INVOICE)).toBe(2000);
+ });
+
+ test('uses explicit amount override before decoding invoice', () => {
+ expect(getLightningInvoiceAmountSats('not-an-invoice', 1234)).toBe(1234);
+ });
+
+ test('maps Orchestra quote response to legacy Lightning quote shape', () => {
+ const expiresAt = Date.now() + 120000;
+ const quote = mapOrchestraQuoteToLightningQuote(
+ {
+ quoteId: 'quote_123',
+ depositAddress: 'spark1deposit',
+ amountIn: '2500000',
+ estimatedOut: '2000',
+ expiresAt,
+ fee: 12500,
+ },
+ 2000,
+ );
+
+ expect(quote).toEqual(
+ expect.objectContaining({
+ invoiceAmountSats: 2000,
+ estimatedLightningFee: 0,
+ btcAmountRequired: 2000,
+ tokenAmountRequired: 2500000,
+ estimatedAmmFee: 12500,
+ poolId: 'quote_123',
+ quoteId: 'quote_123',
+ depositAddress: 'spark1deposit',
+ expiresAt,
+ estimatedOut: '2000',
+ orchestra: true,
+ }),
+ );
+ });
+
+ test('rejects malformed Orchestra quote responses', () => {
+ expect(() =>
+ mapOrchestraQuoteToLightningQuote(
+ {
+ quoteId: 'quote_123',
+ amountIn: '2500000',
+ expiresAt: Date.now() + 120000,
+ },
+ 2000,
+ ),
+ ).toThrow('Missing Orchestra deposit address');
+ });
+
+ test('preserves backend error code and minimum sats', () => {
+ expect(
+ normalizeOrchestraBackendError(
+ {
+ error: {
+ code: 'amount_too_small',
+ message: 'Minimum is 5000 sats',
+ minimumSats: '5000',
+ },
+ },
+ 'fallback',
+ ),
+ ).toEqual({
+ code: 'amount_too_small',
+ message: 'Minimum is 5000 sats',
+ minimumSats: '5000',
+ });
+ });
+});
diff --git a/app/components/admin/homeComponents/contacts/sendAndRequestPage.js b/app/components/admin/homeComponents/contacts/sendAndRequestPage.js
index 10d5f100..57c9b955 100644
--- a/app/components/admin/homeComponents/contacts/sendAndRequestPage.js
+++ b/app/components/admin/homeComponents/contacts/sendAndRequestPage.js
@@ -216,6 +216,9 @@ export default function SendAndRequestPage(props) {
currentWalletMnemoinc,
invoiceResponse.pr,
USD_ASSET_ADDRESS,
+ undefined,
+ undefined,
+ { amountSats: amount },
);
if (!quote.didWork)
throw new Error(quote.error || 'Fee quote failed');
diff --git a/app/components/admin/homeComponents/sendBitcoin/functions/decodeSendAdress.js b/app/components/admin/homeComponents/sendBitcoin/functions/decodeSendAdress.js
index a5223a20..8624208b 100644
--- a/app/components/admin/homeComponents/sendBitcoin/functions/decodeSendAdress.js
+++ b/app/components/admin/homeComponents/sendBitcoin/functions/decodeSendAdress.js
@@ -440,7 +440,7 @@ function withTimeout(promise, t) {
reject(
new Error(t('wallet.sendPages.handlingAddressErrors.timeoutError')),
),
- 20000,
+ 60000,
),
),
]);
diff --git a/app/components/admin/homeComponents/sendBitcoin/functions/processBolt11Invoice.js b/app/components/admin/homeComponents/sendBitcoin/functions/processBolt11Invoice.js
index 2886da13..be04adfd 100644
--- a/app/components/admin/homeComponents/sendBitcoin/functions/processBolt11Invoice.js
+++ b/app/components/admin/homeComponents/sendBitcoin/functions/processBolt11Invoice.js
@@ -98,13 +98,29 @@ export default async function processBolt11Invoice(input, context) {
if (needUsdFee && !hasUsdQuote) {
usdPromiseIndex = promises.length;
- promises.push(
- getLightningPaymentQuote(
- currentWalletMnemoinc,
- input.data.address,
- USD_ASSET_ADDRESS,
+ const bolt11QuoteTimeout = new Promise(resolve =>
+ setTimeout(
+ () =>
+ resolve({
+ didWork: false,
+ error: 'Lightning payment quote timed out',
+ }),
+ 25000,
),
);
+ promises.push(
+ Promise.race([
+ getLightningPaymentQuote(
+ currentWalletMnemoinc,
+ input.data.address,
+ USD_ASSET_ADDRESS,
+ undefined,
+ undefined,
+ { amountSats: amountSat },
+ ),
+ bolt11QuoteTimeout,
+ ]),
+ );
}
if (needBtcFee && !hasBtcFee) {
diff --git a/app/components/admin/homeComponents/sendBitcoin/functions/processLNUrlPay.js b/app/components/admin/homeComponents/sendBitcoin/functions/processLNUrlPay.js
index a6d7fadd..dc6c9e2e 100644
--- a/app/components/admin/homeComponents/sendBitcoin/functions/processLNUrlPay.js
+++ b/app/components/admin/homeComponents/sendBitcoin/functions/processLNUrlPay.js
@@ -186,13 +186,29 @@ export default async function processLNUrlPay(input, context) {
if (needUsdFee && !hasUsdQuote) {
usdPromiseIndex = promises.length;
- promises.push(
- getLightningPaymentQuote(
- currentWalletMnemoinc,
- invoice,
- USD_ASSET_ADDRESS,
+ const lnurlQuoteTimeout = new Promise(resolve =>
+ setTimeout(
+ () =>
+ resolve({
+ didWork: false,
+ error: 'Lightning payment quote timed out',
+ }),
+ 25000,
),
);
+ promises.push(
+ Promise.race([
+ getLightningPaymentQuote(
+ currentWalletMnemoinc,
+ invoice,
+ USD_ASSET_ADDRESS,
+ undefined,
+ undefined,
+ { amountSats: amountSat },
+ ),
+ lnurlQuoteTimeout,
+ ]),
+ );
}
if (needBtcFee && !hasBtcFee) {
diff --git a/app/components/admin/homeComponents/sendBitcoin/sendPaymentScreen.js b/app/components/admin/homeComponents/sendBitcoin/sendPaymentScreen.js
index eb21915f..8fa52e21 100644
--- a/app/components/admin/homeComponents/sendBitcoin/sendPaymentScreen.js
+++ b/app/components/admin/homeComponents/sendBitcoin/sendPaymentScreen.js
@@ -512,6 +512,9 @@ export default function SendPaymentScreen(props) {
currentWalletMnemoinc,
invoice,
USD_ASSET_ADDRESS,
+ undefined,
+ undefined,
+ { amountSats: amount },
);
if (!quote.didWork)
throw new Error(quote.error || 'Fee quote failed');
diff --git a/app/components/admin/homeComponents/sendBitcoin/stablecoinSendScreen.js b/app/components/admin/homeComponents/sendBitcoin/stablecoinSendScreen.js
index 7edafea2..afc08965 100644
--- a/app/components/admin/homeComponents/sendBitcoin/stablecoinSendScreen.js
+++ b/app/components/admin/homeComponents/sendBitcoin/stablecoinSendScreen.js
@@ -419,6 +419,7 @@ export default function StablecoinSendScreen() {
supportFee: 0,
description: description || '',
address: quote.depositAddress,
+ sourceSparkAddress: sparkInformation.sparkAddress,
time: Date.now(),
createdAt: Date.now(),
direction: 'OUTGOING',
diff --git a/app/functions/combinedTransactionsSpark.js b/app/functions/combinedTransactionsSpark.js
index 38ccb35b..43e7faa4 100644
--- a/app/functions/combinedTransactionsSpark.js
+++ b/app/functions/combinedTransactionsSpark.js
@@ -24,6 +24,7 @@ import { isFlashnetTransfer } from './spark/handleFlashnetTransferIds';
import { satsToDollars } from './spark/flashnet';
import ThemeIcon from './CustomElements/themeIcon';
import { HIDDEN_OPACITY, INSET_WINDOW_WIDTH } from '../constants/theme';
+import { isOrchestraSwapFailed } from './spark/orchestraLightning';
// Constants to avoid re-creating objects
const TRANSACTION_CONSTANTS = {
@@ -164,9 +165,10 @@ const getTxIconName = (
isFailedPayment,
isReceive,
) => {
+ if (isFailedPayment) return { icon: 'CircleX', bg: null };
+
// Default: Lightning / Bitcoin / Spark — directional arrows
return { icon: isReceive ? 'ArrowDown' : 'ArrowUp', bg: null };
- if (isFailedPayment) return { icon: 'CircleX', bg: null };
// Pending / swap pending
if (showSwapConversion) return { icon: 'Clock', bg: null };
@@ -350,7 +352,10 @@ export default function getFormattedHomepageTxsForSpark(props) {
const paymentDate = new Date(paymentDetails.time).getTime();
const uniuqeIDFromTx = currentTransaction.sparkID;
- const isFailedPayment = paymentStatus === TRANSACTION_CONSTANTS.FAILED;
+ const showLNOrchestraAsFailed = isOrchestraSwapFailed(currentTransaction);
+ const isFailedPayment =
+ paymentStatus === TRANSACTION_CONSTANTS.FAILED ||
+ showLNOrchestraAsFailed;
// Calculate time difference once
// const timeDifferenceInDays =
@@ -601,37 +606,21 @@ export const UserTransaction = memo(function UserTransaction({
const descriptionTextStyle = useMemo(
() => ({
...styles.descriptionText,
- color: isFailedPayment
- ? theme && darkModeType
- ? COLORS.darkModeText
- : COLORS.failedTransaction
- : theme
- ? COLORS.darkModeText
- : COLORS.lightModeText,
- fontStyle: isFailedPayment ? 'italic' : 'normal',
+ color: theme ? COLORS.darkModeText : COLORS.lightModeText,
}),
- [isFailedPayment, theme, darkModeType],
+ [theme, darkModeType],
);
const dateTextStyle = useMemo(
() => ({
...styles.dateText,
- fontWeight: isFailedPayment ? 400 : 300,
- color: isFailedPayment
- ? theme && darkModeType
- ? COLORS.darkModeText
- : COLORS.failedTransaction
- : theme
- ? COLORS.darkModeText
- : COLORS.lightModeText,
- fontStyle: isFailedPayment ? 'italic' : 'normal',
+ color: theme ? COLORS.darkModeText : COLORS.lightModeText,
}),
- [isFailedPayment, theme, darkModeType],
+ [theme, darkModeType],
);
// Pre-calculate description content
const descriptionContent = useMemo(() => {
- if (isFailedPayment) return t('transactionLabelText.notSent');
// if (userBalanceDenomination === 'hidden') return HIDDEN_BALANCE_TEXT;
if (isDefaultDescription || !paymentDescription) {
return transaction.details.direction === TRANSACTION_CONSTANTS.OUTGOING
@@ -640,7 +629,6 @@ export const UserTransaction = memo(function UserTransaction({
}
return paymentDescription;
}, [
- isFailedPayment,
userBalanceDenomination,
isDefaultDescription,
paymentDescription,
@@ -700,59 +688,54 @@ export const UserTransaction = memo(function UserTransaction({
)}
/>
- {!isFailedPayment && (
-
- {showSwapConversion ? (
-
- ) : (
-
- )}
-
- )}
+
+ {showSwapConversion ? (
+
+ ) : (
+
+ )}
+
+
{!isLastItem && (
)}
diff --git a/app/functions/spark/flashnet.js b/app/functions/spark/flashnet.js
index 7d417a88..ffc845e8 100644
--- a/app/functions/spark/flashnet.js
+++ b/app/functions/spark/flashnet.js
@@ -11,12 +11,15 @@ import {
} from '@flashnet/sdk';
import {
getFlashnetClient,
+ getSparkAddress,
getSingleTxDetails,
getSparkLightningPaymentStatus,
initializeFlashnet,
selectSparkRuntime,
+ sendSparkTokens,
validateWebViewResponse,
} from '.';
+import { getPublicKey } from 'nostr-tools';
import i18next from 'i18next';
import {
OPERATION_TYPES,
@@ -37,6 +40,14 @@ import {
getSingleSparkLightningRequest,
} from './transactions';
import { decode } from 'bolt11';
+import fetchBackend from '../../../db/handleBackend';
+import { privateKeyFromSeedWords } from '../nostrCompatability';
+import {
+ getLightningInvoiceAmountSats,
+ isUsableOrchestraQuote,
+ mapOrchestraQuoteToLightningQuote,
+ normalizeOrchestraBackendError,
+} from './orchestraLightning';
// ============================================
// CONSTANTS & PURE UTILITIES
@@ -719,62 +730,68 @@ export const getLightningPaymentQuote = async (
mnemonic,
invoice,
tokenAddress,
- integratorFeeRateBps = INTEGRATOR_FEE_BPS,
- maxSlippageBps = DEFAULT_MAX_SLIPPAGE_BPS,
+ _integratorFeeRateBps = INTEGRATOR_FEE_BPS,
+ _maxSlippageBps = DEFAULT_MAX_SLIPPAGE_BPS,
+ options = {},
) => {
try {
- const runtime = await selectSparkRuntime(mnemonic);
- if (runtime === 'webview') {
- const response = await sendWebViewRequestGlobal(
- OPERATION_TYPES.getLightningPaymentQuote,
- {
- mnemonic,
- invoice,
- tokenAddress,
- integratorFeeRateBps,
- maxSlippageBps,
- },
- );
- return validateWebViewResponse(
- response,
- 'Not able to getLightningPaymentQuote',
- );
- } else {
- const client = getFlashnetClient(mnemonic);
-
- const quote = await client.getPayLightningWithTokenQuote(
- invoice,
- tokenAddress,
- {
- integratorFeeRateBps,
- maxSlippageBps,
- },
- );
-
- return {
- didWork: true,
- quote: {
- invoiceAmountSats: quote.invoiceAmountSats,
- estimatedLightningFee: quote.estimatedLightningFee,
- btcAmountRequired: quote.btcAmountRequired,
- tokenAmountRequired: quote.tokenAmountRequired,
- estimatedAmmFee: quote.estimatedAmmFee,
- executionPrice: quote.executionPrice,
- priceImpact: quote.priceImpactPct,
- poolId: quote.poolId,
- fee: quote.btcAmountRequired - quote.invoiceAmountSats,
- },
- };
+ if (tokenAddress !== USD_ASSET_ADDRESS) {
+ throw new Error('Only USDB Lightning payments are supported');
}
- } catch (error) {
- console.warn(
- 'Get Lightning quote error:',
- formatError(error, 'getLightningPaymentQuote'),
+
+ const invoiceAmountSats = getLightningInvoiceAmountSats(
+ invoice,
+ options.amountSats,
);
+ const privateKey =
+ options.contactsPrivateKey || (await privateKeyFromSeedWords(mnemonic));
+ const publicKey = options.publicKey || getPublicKey(privateKey);
+ const sparkAddressResponse = options.refundAddress
+ ? { didWork: true, response: options.refundAddress }
+ : await getSparkAddress(mnemonic);
+
+ if (!sparkAddressResponse.didWork || !sparkAddressResponse.response) {
+ throw new Error(
+ sparkAddressResponse.error || 'Unable to derive Spark refund address',
+ );
+ }
+
+ const result = await fetchBackend(
+ 'createFlashnetStablecoinQuoteV2',
+ {
+ recipientAddress: invoice,
+ destinationChain: 'lightning',
+ destinationAsset: 'BTC',
+ amountSats: invoiceAmountSats,
+ sourceMethod: 'usdb',
+ refundAddress: sparkAddressResponse.response,
+ },
+ privateKey,
+ publicKey,
+ );
+
+ const quote = mapOrchestraQuoteToLightningQuote(result, invoiceAmountSats);
+ return { didWork: true, quote };
+ } catch (error) {
+ const backendError = normalizeOrchestraBackendError(
+ error,
+ 'Unable to get Lightning quote',
+ );
+ console.warn('Get Lightning quote error:', {
+ operation: 'getLightningPaymentQuote',
+ message: backendError.message,
+ code: backendError.code,
+ minimumSats: backendError.minimumSats,
+ });
return {
didWork: false,
- error: error.message,
- details: formatError(error, 'getLightningPaymentQuote'),
+ error: backendError.message,
+ details: {
+ operation: 'getLightningPaymentQuote',
+ message: backendError.message,
+ code: backendError.code,
+ minimumSats: backendError.minimumSats,
+ },
};
}
};
@@ -791,85 +808,122 @@ export const payLightningWithToken = async (
invoice,
tokenAddress,
maxSlippageBps = DEFAULT_MAX_SLIPPAGE_BPS,
- maxLightningFeeSats = null,
- rollbackOnFailure = true,
- useExistingBtcBalance = false,
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
+ quote = null,
+ amountSats,
+ contactsPrivateKey,
+ publicKey,
+ refundAddress,
},
) => {
try {
- const runtime = await selectSparkRuntime(mnemonic);
- if (runtime === 'webview') {
- const response = await sendWebViewRequestGlobal(
- OPERATION_TYPES.payLightningWithToken,
- {
- mnemonic,
- invoice,
- tokenAddress,
- maxSlippageBps,
- maxLightningFeeSats,
- rollbackOnFailure,
- useExistingBtcBalance,
- integratorFeeRateBps,
- },
- );
- return validateWebViewResponse(
- response,
- 'Not able to payLightningWithToken',
- );
- } else {
- const client = getFlashnetClient(mnemonic);
+ if (tokenAddress !== USD_ASSET_ADDRESS) {
+ throw new Error('Only USDB Lightning payments are supported');
+ }
- const result = await client.payLightningWithToken({
+ const privateKey =
+ contactsPrivateKey || (await privateKeyFromSeedWords(mnemonic));
+ const resolvedPublicKey = publicKey || getPublicKey(privateKey);
+ const sparkAddressResponse = refundAddress
+ ? { didWork: true, response: refundAddress }
+ : await getSparkAddress(mnemonic);
+
+ if (!sparkAddressResponse.didWork || !sparkAddressResponse.response) {
+ throw new Error(
+ sparkAddressResponse.error || 'Unable to derive Spark source address',
+ );
+ }
+
+ let paymentQuote = quote;
+ if (!isUsableOrchestraQuote(paymentQuote)) {
+ const quoteResponse = await getLightningPaymentQuote(
+ mnemonic,
invoice,
tokenAddress,
- maxSlippageBps,
- maxLightningFeeSats: maxLightningFeeSats || undefined,
- rollbackOnFailure,
- useExistingBtcBalance,
integratorFeeRateBps,
- integratorPublicKey: process.env.BLITZ_SPARK_PUBLICKEY,
- });
+ maxSlippageBps,
+ {
+ amountSats,
+ contactsPrivateKey: privateKey,
+ publicKey: resolvedPublicKey,
+ refundAddress: sparkAddressResponse.response,
+ },
+ );
- console.log('token lightning payment response:', result);
-
- if (result.success) {
- return {
- didWork: true,
- result: {
- success: true,
- lightningPaymentId: result.lightningPaymentId,
- tokenAmountSpent: result.tokenAmountSpent,
- btcAmountReceived: result.btcAmountReceived,
- swapTransferId: result.swapTransferId,
- ammFeePaid: result.ammFeePaid,
- lightningFeePaid: result.lightningFeePaid,
- poolId: result.poolId,
- },
- };
- } else {
- return {
- didWork: false,
- error: result.error,
- result: {
- success: false,
- error: result.error,
- poolId: result.poolId,
- tokenAmountSpent: result.tokenAmountSpent,
- btcAmountReceived: result.btcAmountReceived,
- },
- };
+ if (!quoteResponse.didWork) {
+ throw new Error(quoteResponse.error || 'Unable to create quote');
}
+ paymentQuote = quoteResponse.quote;
}
- } catch (error) {
- console.warn(
- 'Pay Lightning with token error:',
- formatError(error, 'payLightningWithToken'),
+
+ const tokenAmount = Number(paymentQuote.tokenAmountRequired);
+ if (!Number.isFinite(tokenAmount) || tokenAmount <= 0) {
+ throw new Error('Invalid Orchestra token amount');
+ }
+
+ const tokenPayment = await sendSparkTokens({
+ tokenIdentifier: USDB_TOKEN_ID,
+ tokenAmount: Number(Math.ceil(tokenAmount)),
+ receiverSparkAddress: paymentQuote.depositAddress,
+ mnemonic,
+ });
+
+ if (!tokenPayment.didWork) {
+ throw new Error(tokenPayment.error || 'Unable to send USDB deposit');
+ }
+
+ const sparkTxHash = tokenPayment.response;
+ await fetchBackend(
+ 'submitFlashnetStablecoinOrder',
+ {
+ quoteId: paymentQuote.quoteId,
+ sparkTxHash,
+ sourceSparkAddress: sparkAddressResponse.response,
+ },
+ privateKey,
+ resolvedPublicKey,
);
+
+ return {
+ didWork: true,
+ result: {
+ success: true,
+ tokenAmountSpent: tokenAmount,
+ btcAmountReceived:
+ Number(paymentQuote.estimatedOut) ||
+ Number(paymentQuote.invoiceAmountSats) ||
+ 0,
+ swapTransferId: sparkTxHash,
+ ammFeePaid: Number(paymentQuote.estimatedAmmFee || 0),
+ lightningFeePaid: 0,
+ poolId: paymentQuote.quoteId,
+ quoteId: paymentQuote.quoteId,
+ depositAddress: paymentQuote.depositAddress,
+ expiresAt: paymentQuote.expiresAt,
+ orchestra: true,
+ sourceSparkAddress: sparkAddressResponse.response,
+ },
+ };
+ } catch (error) {
+ const backendError = normalizeOrchestraBackendError(
+ error,
+ 'Unable to pay Lightning invoice with USDB',
+ );
+ console.warn('Pay Lightning with token error:', {
+ operation: 'payLightningWithToken',
+ message: backendError.message,
+ code: backendError.code,
+ minimumSats: backendError.minimumSats,
+ });
return {
didWork: false,
- error: error.message,
- details: formatError(error, 'payLightningWithToken'),
+ error: backendError.message,
+ details: {
+ operation: 'payLightningWithToken',
+ message: backendError.message,
+ code: backendError.code,
+ minimumSats: backendError.minimumSats,
+ },
};
}
};
diff --git a/app/functions/spark/orchestraLightning.js b/app/functions/spark/orchestraLightning.js
new file mode 100644
index 00000000..aea36fcc
--- /dev/null
+++ b/app/functions/spark/orchestraLightning.js
@@ -0,0 +1,105 @@
+import { decode } from 'bolt11';
+
+export function getLightningInvoiceAmountSats(invoice, amountSats) {
+ const fallbackAmount = Number(amountSats);
+ if (Number.isFinite(fallbackAmount) && fallbackAmount > 0) {
+ return Math.round(fallbackAmount);
+ }
+
+ const decodedInvoice = decode(invoice);
+ const invoiceAmount = Number(decodedInvoice?.satoshis);
+ if (!Number.isFinite(invoiceAmount) || invoiceAmount <= 0) {
+ throw new Error('Lightning invoice amount is required for USD payments');
+ }
+
+ return Math.round(invoiceAmount);
+}
+
+export function normalizeOrchestraBackendError(result, fallbackMessage) {
+ const rawError = result?.error || result;
+ const message =
+ rawError?.message ||
+ (typeof rawError === 'string' ? rawError : null) ||
+ fallbackMessage;
+
+ return {
+ message,
+ code: rawError?.code,
+ minimumSats: rawError?.minimumSats,
+ };
+}
+
+export function mapOrchestraQuoteToLightningQuote(result, invoiceAmountSats) {
+ if (!result || typeof result !== 'object') {
+ throw new Error('Invalid Orchestra quote response');
+ }
+
+ if (result.error) {
+ const backendError = normalizeOrchestraBackendError(
+ result,
+ 'Unable to create Lightning quote',
+ );
+ const error = new Error(backendError.message);
+ error.code = backendError.code;
+ error.minimumSats = backendError.minimumSats;
+ throw error;
+ }
+
+ const tokenAmountRequired = Number(result.amountIn);
+ const estimatedAmmFee = Number(result.quoteFees || 0);
+ const expiresAt = Number(result.expiresAt);
+
+ if (!result.quoteId) throw new Error('Missing Orchestra quote ID');
+ if (!result.depositAddress) {
+ throw new Error('Missing Orchestra deposit address');
+ }
+ if (!Number.isFinite(tokenAmountRequired) || tokenAmountRequired <= 0) {
+ throw new Error('Invalid Orchestra quote amount');
+ }
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
+ throw new Error('Invalid Orchestra quote expiration');
+ }
+
+ return {
+ invoiceAmountSats,
+ estimatedLightningFee: 0,
+ btcAmountRequired: invoiceAmountSats,
+ tokenAmountRequired,
+ estimatedAmmFee,
+ executionPrice: null,
+ priceImpact: null,
+ poolId: result.quoteId,
+ fee: estimatedAmmFee,
+ quoteId: result.quoteId,
+ depositAddress: result.depositAddress,
+ expiresAt,
+ estimatedOut: result.estimatedOut,
+ orchestra: true,
+ };
+}
+
+export function isUsableOrchestraQuote(quote) {
+ return (
+ !!quote?.orchestra &&
+ !!quote?.quoteId &&
+ !!quote?.depositAddress &&
+ Number.isFinite(Number(quote?.tokenAmountRequired)) &&
+ Number(quote.tokenAmountRequired) > 0 &&
+ Number(quote?.expiresAt || 0) > Date.now()
+ );
+}
+
+export function isOrchestraSwapFailed(tx) {
+ let details;
+ try {
+ details = JSON.parse(tx.details);
+ } catch {
+ details = tx.details;
+ }
+
+ return (
+ tx?.paymentStatus === 'completed' &&
+ details?.isFlashnetStablecoin === true &&
+ details?.runcount > 20
+ );
+}
diff --git a/app/functions/spark/payments.js b/app/functions/spark/payments.js
index f57b9035..9ee58508 100644
--- a/app/functions/spark/payments.js
+++ b/app/functions/spark/payments.js
@@ -11,7 +11,6 @@ import {
getSparkAddress,
sparkWallet,
sendSparkTokens,
- getSparkLightningSendRequest,
getSingleTxDetails,
getSparkPaymentStatus,
} from '.';
@@ -22,7 +21,6 @@ import {
import {
DEFAULT_PAYMENT_EXPIRY_SEC,
IS_SPARK_ID,
- IS_SPARK_REQUEST_ID,
USDB_TOKEN_ID,
} from '../../constants';
import sha256Hash from '../hash';
@@ -153,6 +151,9 @@ export const sparkPaymenWrapper = async ({
const swapPaymentResponse = await payLightningWithToken(mnemonic, {
invoice: address,
tokenAddress: USD_ASSET_ADDRESS,
+ quote: swapPaymentQuote,
+ amountSats,
+ refundAddress: sparkInformation?.sparkAddress,
});
if (!swapPaymentResponse.didWork)
@@ -161,76 +162,45 @@ export const sparkPaymenWrapper = async ({
'Error when sending lightning payment from USD balance',
);
- // delete swap transfer and combine all info into one tx
- setFlashnetTransfer(swapPaymentResponse.result.swapTransferId);
-
- const [lightningSendResponse, userSwaps] = await Promise.all([
- IS_SPARK_REQUEST_ID.test(
- swapPaymentResponse.result.lightningPaymentId,
- )
- ? getSparkLightningSendRequest(
- swapPaymentResponse.result.lightningPaymentId,
- mnemonic,
- )
- : getSingleTxDetails(
- mnemonic,
- swapPaymentResponse.result.lightningPaymentId,
- ),
- getUserSwapHistory(mnemonic, 5),
- ]);
-
- if (userSwaps.didWork) {
- const swap = userSwaps.swaps.find(
- savedSwap =>
- savedSwap.outboundTransferId ===
- swapPaymentResponse.result.swapTransferId,
- );
-
- // if swap is found delte from tx history
- if (swap) {
- setFlashnetTransfer(swap.inboundTransferId);
- }
- }
-
- const didUseLightning = IS_SPARK_REQUEST_ID.test(
- swapPaymentResponse.result.lightningPaymentId,
- );
-
- const usdToSatFee = dollarsToSats(
- swapPaymentResponse.result.ammFeePaid / 1000000,
- poolInfoRef.currentPriceAInB,
- );
- const lnFee = swapPaymentResponse.result.lightningFeePaid;
-
+ const now = Date.now();
const tx = {
- id: swapPaymentResponse.result.lightningPaymentId,
- paymentStatus: didUseLightning ? 'pending' : 'completed',
- paymentType: didUseLightning ? 'lightning' : 'spark',
+ id: swapPaymentResponse.result.swapTransferId,
+ paymentStatus: 'pending',
+ paymentType: 'spark',
accountId: sparkInformation.identityPubKey,
details: {
sendingUUID,
- fee: Math.round(usdToSatFee + lnFee),
- totalFee: Math.round(usdToSatFee + lnFee),
+ fee: Math.round(
+ dollarsToSats(
+ swapPaymentResponse.result.ammFeePaid / 1000000,
+ poolInfoRef.currentPriceAInB,
+ ),
+ ),
+ totalFee: Math.round(
+ dollarsToSats(
+ swapPaymentResponse.result.ammFeePaid / 1000000,
+ poolInfoRef.currentPriceAInB,
+ ),
+ ),
supportFee: 0,
amount:
swapPaymentResponse.result.tokenAmountSpent -
swapPaymentResponse.result.ammFeePaid,
description: memo || '',
- address: address,
- time: new Date(
- lightningSendResponse[
- didUseLightning ? 'updatedAt' : 'updatedTime'
- ],
- ).getTime(),
- createdAt: new Date(
- lightningSendResponse[
- didUseLightning ? 'createdAt' : 'createdTime'
- ],
- ).getTime(),
+ address: swapPaymentResponse.result.depositAddress,
+ sourceSparkAddress: swapPaymentResponse.result.sourceSparkAddress,
+ time: now,
+ createdAt: now,
direction: 'OUTGOING',
preimage: '',
isLRC20Payment: true,
LRC20Token: USDB_TOKEN_ID,
+ isFlashnetStablecoin: true,
+ quoteId: swapPaymentResponse.result.quoteId,
+ destinationAddress: address,
+ destinationChain: 'lightning',
+ destinationAsset: 'BTC',
+ sourceMethod: 'USD',
...(paymentInfo?.data?.successAction
? { successAction: paymentInfo.data.successAction }
: {}),
diff --git a/app/functions/spark/restore.js b/app/functions/spark/restore.js
index 5bccf425..ff6cdffb 100644
--- a/app/functions/spark/restore.js
+++ b/app/functions/spark/restore.js
@@ -499,6 +499,21 @@ export async function fullRestoreSparkState({
}
}
+function shouldRunOnThisTick(runcount, lastRunTimestamp) {
+ if (runcount < 10) return true; // first 10 calls: let the 10s interval handle it naturally
+ if (runcount > 22) return false; // after 21 calls, stop backoff and check every tick to avoid infinite backoff
+
+ if (!lastRunTimestamp) return true;
+
+ const backoffRun = runcount - 10; // 0-indexed backoff phase
+ const backoffMs = Math.min(
+ 10_000 * Math.pow(2, backoffRun), // 10s, 20s, 40s, 80s...
+ 300_000, // cap at 5 minutes
+ );
+
+ return Date.now() - lastRunTimestamp >= backoffMs;
+}
+
export async function checkFlashnetStablecoinStatusLogic(
tx,
contactsPrivateKey,
@@ -509,19 +524,39 @@ export async function checkFlashnetStablecoinStatusLogic(
typeof tx.details === 'string' ? JSON.parse(tx.details) : tx.details;
if (!details?.isFlashnetStablecoin || !details?.quoteId) return null;
+ const runcount = details.runcount || 0;
+
+ // Skip this tick if we haven't waited long enough
+ if (!shouldRunOnThisTick(runcount, details.lastRunTimestamp)) return null;
+
const statusResult = await fetchBackend(
'checkFlashnetStablecoinStatus',
- { quoteId: details.quoteId },
+ {
+ quoteId: details.quoteId,
+ sourceSparkAddress: details.sourceSparkAddress,
+ sparkTxHash: tx.sparkID,
+ },
contactsPrivateKey,
publicKey,
);
- if (!statusResult || statusResult.error) return null;
+ if (!statusResult || statusResult.error)
+ return {
+ id: tx.sparkID,
+ paymentStatus: details.runcount === 21 ? 'completed' : 'pending',
+ paymentType: tx.paymentType,
+ accountId: tx.accountId,
+ details: {
+ ...details,
+ runcount: runcount + 1,
+ lastRunTimestamp: Date.now(), // <-- persist when we last fetched
+ },
+ };
const newStatus =
- statusResult.status === 'completed'
+ statusResult.status === 'completed' || details.runcount === 21
? 'completed'
- : statusResult.status === 'failed'
+ : ['refunded', 'failed'].includes(statusResult.status)
? 'failed'
: null;
@@ -532,7 +567,11 @@ export async function checkFlashnetStablecoinStatusLogic(
paymentStatus: newStatus,
paymentType: tx.paymentType,
accountId: tx.accountId,
- details,
+ details: {
+ ...details,
+ runcount: runcount + 1,
+ lastRunTimestamp: Date.now(), // <-- persist when we last fetched
+ },
};
} catch {
return null;
diff --git a/app/screens/inAccount/confirmTxPage.js b/app/screens/inAccount/confirmTxPage.js
index fdfd8f3b..a7214e99 100644
--- a/app/screens/inAccount/confirmTxPage.js
+++ b/app/screens/inAccount/confirmTxPage.js
@@ -171,7 +171,12 @@ export default function ConfirmTxPage(props) {
/>
diff --git a/app/screens/inAccount/expandedTxPage.js b/app/screens/inAccount/expandedTxPage.js
index c6ff963f..bd8cdf30 100644
--- a/app/screens/inAccount/expandedTxPage.js
+++ b/app/screens/inAccount/expandedTxPage.js
@@ -25,6 +25,7 @@ import formatTokensNumber from '../../functions/lrc20/formatTokensBalance';
import { useTranslation } from 'react-i18next';
import { useGlobalInsets } from '../../../context-store/insetsProvider';
import { useAppStatus } from '../../../context-store/appStatus';
+import { useToast } from '../../../context-store/toastManager';
import { formatLocalTimeShort } from '../../functions/timeFormatter';
import { useEffect, useMemo, useRef, useState } from 'react';
import CustomSearchInput from '../../functions/CustomElements/searchInput';
@@ -39,7 +40,7 @@ import { useImageCache } from '../../../context-store/imageCache';
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import CustomSettingsTopBar from '../../functions/CustomElements/settingsTopBar';
import { currentPriceAinBToPriceDollars } from '../../functions/spark/flashnet';
-import { formatBalanceAmount } from '../../functions';
+import { formatBalanceAmount, copyToClipboard } from '../../functions';
import ThemeIcon from '../../functions/CustomElements/themeIcon';
import {
claimSparkHodlLightningPayment,
@@ -52,6 +53,8 @@ import useAdaptiveButtonLayout from '../../hooks/useAdaptiveButtonLayout';
import { useNavigateToContact } from '../../components/admin/homeComponents/contacts/utils/navigateToExpandedContact';
import { INSET_WINDOW_WIDTH, WINDOWWIDTH } from '../../constants/theme';
import ProfileImageRow from '../../components/admin/homeComponents/contacts/internalComponents/profileImageRow';
+import { isOrchestraSwapFailed } from '../../functions/spark/orchestraLightning';
+import { openComposer } from 'react-native-email-link';
export default function ExpandedTx(props) {
const { decodedAddedContacts } = useGlobalContactsInfo();
@@ -72,9 +75,13 @@ export default function ExpandedTx(props) {
const techicalDetailsLabel = t('screens.inAccount.expandedTxPage.detailsBTN');
const claimHTLCLabel = t('screens.inAccount.expandedTxPage.claimPayment');
+ const contactSupportLabel = t(
+ 'screens.inAccount.expandedTxPage.contactSupport',
+ );
+ const { showToast } = useToast();
const { shouldStack, containerProps, getLabelProps } =
- useAdaptiveButtonLayout([techicalDetailsLabel, claimHTLCLabel]);
+ useAdaptiveButtonLayout([techicalDetailsLabel, contactSupportLabel]);
const [transaction, setTransaction] = useState(
props.route.params.transaction,
@@ -83,6 +90,8 @@ export default function ExpandedTx(props) {
const isBulkPayment = !!transaction.details?.isBulkPayment;
const bulkPaymentGroup = transaction.details?.bulkPaymentGroup ?? [];
+ const showLNOrchestraAsFailed = isOrchestraSwapFailed(transaction);
+
// Contacts for ProfileImageRow — successful recipients only
const bulkContacts = bulkPaymentGroup
.filter(e => e.status !== 'failed')
@@ -145,7 +154,8 @@ export default function ExpandedTx(props) {
? t('screens.inAccount.expandedTxPage.gift')
: transaction.paymentType;
- const isFailedPayment = transaction.paymentStatus === 'failed';
+ const isFailedPayment =
+ transaction.paymentStatus === 'failed' || showLNOrchestraAsFailed;
const isPending =
transaction.paymentStatus === 'pending' || transaction.isBalancePending;
const isSuccessful = !isFailedPayment && !isPending;
@@ -257,6 +267,33 @@ export default function ExpandedTx(props) {
}
};
+ const handleContactSupport = async () => {
+ const fields = [
+ ['Payment ID', transaction.sparkID],
+ ['Quote ID', transaction.details?.quoteId],
+ ['Destination Address', transaction.details?.destinationAddress],
+ ['Destination Asset', transaction.details?.destinationAsset],
+ ['Destination Chain', transaction.details?.destinationChain],
+ ['Preimage', transaction.details?.preimage],
+ ['Address', transaction.details?.address],
+ ['Bitcoin TX ID', transaction.details?.onChainTxid],
+ ];
+ const body = fields
+ .filter(([, v]) => v)
+ .map(([k, v]) => `${k}: ${v}`)
+ .join('\n');
+
+ try {
+ await openComposer({
+ to: 'blake@blitzwalletapp.com',
+ subject: 'Failed Payment Support',
+ body,
+ });
+ } catch {
+ copyToClipboard('blake@blitzwalletapp.com', showToast, null);
+ }
+ };
+
const isFlashnetStablecoin = !!transaction.details?.isFlashnetStablecoin;
const isLRC20Payment = transaction.details.isLRC20Payment;
@@ -578,7 +615,12 @@ export default function ExpandedTx(props) {
{renderInfoRow(
t('constants.type'),
isFlashnetStablecoin
- ? t('screens.inAccount.expandedTxPage.chainSwap')
+ ? t('screens.inAccount.expandedTxPage.chainSwap', {
+ context:
+ transaction.details.destinationChain === 'lightning'
+ ? 'lightning'
+ : 'other',
+ })
: transactionPaymentType,
true,
{
@@ -675,42 +717,29 @@ export default function ExpandedTx(props) {
content={techicalDetailsLabel}
/>
- {transaction.details.isHoldInvoice &&
- isPending &&
- !transaction.details.didClaimHTLC && (
-
- {isClaimingHtlc ? (
-
- ) : (
-
- )}
-
- )}
+ {isFailedPayment && (
+
+
+
+ )}
{/* Receipt Dots */}
diff --git a/locales/de-DE/translation.json b/locales/de-DE/translation.json
index 7859a262..9f828b2f 100644
--- a/locales/de-DE/translation.json
+++ b/locales/de-DE/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Mit USDB bezahlen",
"chainNotSupportedForAsset": "Nicht verfügbar in diesem Netzwerk",
"swapInProgress": "Tausch läuft",
- "swapInProgressDescription": "Ihre Mittel wurden gesendet. Der Stablecoin-Tausch wird verarbeitet und kann einige Minuten dauern.",
+ "swapInProgressDescription": "Ihre Mittel wurden gesendet. Die Zahlung wird verarbeitet und kann einige Minuten dauern.",
"noAmount": "Bitte gib einen Betrag zum Senden ein.",
"quoteStillLoading": "Angebot wird abgerufen, bitte einen Moment warten.",
"noQuote": "Angebot konnte nicht abgerufen werden. Bitte versuche es erneut.",
@@ -2065,7 +2065,9 @@
"gift": "Geschenk",
"claimPayment": "Zahlung einfordern",
"splitPayment": "Aufteilen",
- "chainSwap": "Chain-Tausch"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Support"
},
"explorePage": {
"timeLeft": "({{time}} verbleibend)",
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 0a8dbfe0..bbb525b4 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Pay with USDb",
"chainNotSupportedForAsset": "Not available on this network",
"swapInProgress": "Payment in Progress",
- "swapInProgressDescription": "Your funds have been sent. The stablecoin payment is processing and may take a few minutes.",
+ "swapInProgressDescription": "Your funds have been sent. The payment is processing and may take a few minutes.",
"noAmount": "Please enter an amount to send.",
"quoteStillLoading": "Getting a quote, please wait a moment.",
"noQuote": "Could not get a quote. Please try again.",
@@ -2065,7 +2065,9 @@
"gift": "Gift",
"claimPayment": "Claim payment",
"splitPayment": "Split",
- "chainSwap": "Stablecoin"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Support"
},
"explorePage": {
"timeLeft": "({{time}} left)",
diff --git a/locales/es/translation.json b/locales/es/translation.json
index 358976b0..ef1dbaeb 100644
--- a/locales/es/translation.json
+++ b/locales/es/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Pagar con USDb",
"chainNotSupportedForAsset": "No disponible en esta red",
"swapInProgress": "Intercambio en Proceso",
- "swapInProgressDescription": "Tus fondos han sido enviados. El intercambio de stablecoin está procesándose y puede tardar unos minutos.",
+ "swapInProgressDescription": "Tus fondos han sido enviados. El pago se está procesando y puede tardar unos minutos.",
"noAmount": "Por favor, ingresa un monto para enviar.",
"quoteStillLoading": "Obteniendo cotización, por favor espera un momento.",
"noQuote": "No se pudo obtener una cotización. Por favor, inténtalo de nuevo.",
@@ -2065,7 +2065,9 @@
"gift": "Regalo",
"claimPayment": "Reclamar pago",
"splitPayment": "Dividir",
- "chainSwap": "Intercambio en Cadena"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Soporte"
},
"explorePage": {
"timeLeft": "({{time}} restantes)",
diff --git a/locales/fr/translation.json b/locales/fr/translation.json
index 60602fe4..f1034ff5 100644
--- a/locales/fr/translation.json
+++ b/locales/fr/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Payer avec USDb",
"chainNotSupportedForAsset": "Non disponible sur ce réseau",
"swapInProgress": "Échange en Cours",
- "swapInProgressDescription": "Vos fonds ont été envoyés. L'échange de stablecoin est en cours de traitement et peut prendre quelques minutes.",
+ "swapInProgressDescription": "Vos fonds ont été envoyés. Le paiement est en cours de traitement et peut prendre quelques minutes.",
"noAmount": "Veuillez saisir un montant à envoyer.",
"quoteStillLoading": "Obtention d'un devis, veuillez patienter un instant.",
"noQuote": "Impossible d'obtenir un devis. Veuillez réessayer.",
@@ -2065,7 +2065,9 @@
"gift": "Cadeau",
"claimPayment": "Réclamer le paiement",
"splitPayment": "Partager",
- "chainSwap": "Échange de Chaîne"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Support"
},
"explorePage": {
"timeLeft": "({{time}} restantes)",
diff --git a/locales/it/translation.json b/locales/it/translation.json
index 3bd59edb..df5940ec 100644
--- a/locales/it/translation.json
+++ b/locales/it/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Paga con USDb",
"chainNotSupportedForAsset": "Non disponibile su questa rete",
"swapInProgress": "Scambio in Corso",
- "swapInProgressDescription": "I tuoi fondi sono stati inviati. Lo scambio di stablecoin è in elaborazione e potrebbe richiedere alcuni minuti.",
+ "swapInProgressDescription": "I tuoi fondi sono stati inviati. Il pagamento è in elaborazione e potrebbe richiedere alcuni minuti.",
"noAmount": "Inserisci un importo da inviare.",
"quoteStillLoading": "Ottenendo una quotazione, attendi un momento.",
"noQuote": "Impossibile ottenere una quotazione. Riprova.",
@@ -2065,7 +2065,9 @@
"gift": "Regalo",
"claimPayment": "Richiedi pagamento",
"splitPayment": "Dividi",
- "chainSwap": "Chain Swap"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Supporto"
},
"explorePage": {
"timeLeft": "({{time}} rimanenti)",
diff --git a/locales/pt-BR/translation.json b/locales/pt-BR/translation.json
index 347c4716..4056ff00 100644
--- a/locales/pt-BR/translation.json
+++ b/locales/pt-BR/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Pagar com USDB",
"chainNotSupportedForAsset": "Não disponível nesta rede",
"swapInProgress": "Transação em andamento",
- "swapInProgressDescription": "Seus fundos foram enviados. O pagamento de dólar digital está sendo processado e pode levar alguns minutos.",
+ "swapInProgressDescription": "Seus fundos foram enviados. O pagamento está sendo processado e pode levar alguns minutos.",
"noAmount": "Por favor, insira uma quantia para enviar.",
"quoteStillLoading": "Obtendo cotação, aguarde um momento.",
"noQuote": "Não foi possível obter uma cotação. Por favor, tente novamente.",
@@ -2065,7 +2065,9 @@
"gift": "Vale-Blitz",
"claimPayment": "Reivindicar pagamento",
"splitPayment": "Dividir",
- "chainSwap": "Dólar digital"
+ "chainSwap_other": "Dólar digital",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Suporte"
},
"explorePage": {
"timeLeft": "({{time}} restantes)",
diff --git a/locales/ru/translation.json b/locales/ru/translation.json
index 8dca7908..7f2b77b5 100644
--- a/locales/ru/translation.json
+++ b/locales/ru/translation.json
@@ -1295,7 +1295,7 @@
"payWithUSDb": "Оплатить через USDb",
"chainNotSupportedForAsset": "Недоступно в этой сети",
"swapInProgress": "Обмен выполняется",
- "swapInProgressDescription": "Ваши средства отправлены. Обмен стейблкоина обрабатывается и может занять несколько минут.",
+ "swapInProgressDescription": "Ваши средства отправлены. Платёж обрабатывается и может занять несколько минут.",
"noAmount": "Пожалуйста, введите сумму для отправки.",
"quoteStillLoading": "Получение котировки, подождите немного.",
"noQuote": "Не удалось получить котировку. Попробуйте снова.",
@@ -2066,7 +2066,9 @@
"gift": "Подарок",
"claimPayment": "Получить платёж",
"splitPayment": "Разделить",
- "chainSwap": "Обмен в сети"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Поддержка"
},
"explorePage": {
"timeLeft": "(осталось {{time}})",
diff --git a/locales/sv/translation.json b/locales/sv/translation.json
index e4879de3..4b7cc479 100644
--- a/locales/sv/translation.json
+++ b/locales/sv/translation.json
@@ -1294,7 +1294,7 @@
"payWithUSDb": "Betala med USDb",
"chainNotSupportedForAsset": "Inte tillgängligt i detta nätverk",
"swapInProgress": "Byte pågår",
- "swapInProgressDescription": "Dina pengar har skickats. Stablecoin-bytet bearbetas och kan ta några minuter.",
+ "swapInProgressDescription": "Dina pengar har skickats. Betalningen behandlas och kan ta några minuter.",
"noAmount": "Ange ett belopp att skicka.",
"quoteStillLoading": "Hämtar offert, vänta ett ögonblick.",
"noQuote": "Det gick inte att hämta en offert. Försök igen.",
@@ -2065,7 +2065,9 @@
"gift": "Gåva",
"claimPayment": "Ta emot betalning",
"splitPayment": "Dela",
- "chainSwap": "Kedjeswap"
+ "chainSwap_other": "Stablecoin",
+ "chainSwap_lightning": "Lightning",
+ "contactSupport": "Support"
},
"explorePage": {
"timeLeft": "({{time}} kvar)",