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
This commit is contained in:
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -440,7 +440,7 @@ function withTimeout(promise, t) {
|
||||
reject(
|
||||
new Error(t('wallet.sendPages.handlingAddressErrors.timeoutError')),
|
||||
),
|
||||
20000,
|
||||
60000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{!isFailedPayment && (
|
||||
<View>
|
||||
{showSwapConversion ? (
|
||||
<FormattedSatText
|
||||
neverHideBalance={
|
||||
frompage === TRANSACTION_CONSTANTS.VIEW_ALL_PAGE
|
||||
}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
containerStyles={styles.amountContainer}
|
||||
styles={{ color: amountColor }}
|
||||
frontText={APPROXIMATE_SYMBOL}
|
||||
balance={
|
||||
satsToDollars(
|
||||
transaction.details.amount,
|
||||
poolInfoRef.currentPriceAInB,
|
||||
) *
|
||||
(1 - (poolInfoRef.lpFeeBps / 100 + 1) / 100)
|
||||
}
|
||||
useCustomLabel={true}
|
||||
customLabel={token?.tokenTicker || 'USDB'}
|
||||
useMillionDenomination={true}
|
||||
/>
|
||||
) : (
|
||||
<FormattedSatText
|
||||
neverHideBalance={
|
||||
frompage === TRANSACTION_CONSTANTS.VIEW_ALL_PAGE
|
||||
}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
containerStyles={styles.amountContainer}
|
||||
styles={{ color: amountColor }}
|
||||
frontText={
|
||||
userBalanceDenomination !== 'hidden'
|
||||
? transaction.details.direction ===
|
||||
TRANSACTION_CONSTANTS.INCOMING
|
||||
? '+'
|
||||
: '-'
|
||||
: ''
|
||||
}
|
||||
balance={
|
||||
isLRC20Payment
|
||||
? formatTokensNumber(
|
||||
transaction.details.amount,
|
||||
token?.decimals,
|
||||
)
|
||||
: transaction.details.amount
|
||||
}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={token?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View>
|
||||
{showSwapConversion ? (
|
||||
<FormattedSatText
|
||||
neverHideBalance={frompage === TRANSACTION_CONSTANTS.VIEW_ALL_PAGE}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
containerStyles={styles.amountContainer}
|
||||
styles={{ color: amountColor }}
|
||||
frontText={APPROXIMATE_SYMBOL}
|
||||
balance={
|
||||
satsToDollars(
|
||||
transaction.details.amount,
|
||||
poolInfoRef.currentPriceAInB,
|
||||
) *
|
||||
(1 - (poolInfoRef.lpFeeBps / 100 + 1) / 100)
|
||||
}
|
||||
useCustomLabel={true}
|
||||
customLabel={token?.tokenTicker || 'USDB'}
|
||||
useMillionDenomination={true}
|
||||
/>
|
||||
) : (
|
||||
<FormattedSatText
|
||||
neverHideBalance={frompage === TRANSACTION_CONSTANTS.VIEW_ALL_PAGE}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
containerStyles={styles.amountContainer}
|
||||
styles={{ color: amountColor }}
|
||||
frontText={
|
||||
userBalanceDenomination !== 'hidden'
|
||||
? transaction.details.direction ===
|
||||
TRANSACTION_CONSTANTS.INCOMING
|
||||
? '+'
|
||||
: '-'
|
||||
: ''
|
||||
}
|
||||
balance={
|
||||
isLRC20Payment
|
||||
? formatTokensNumber(
|
||||
transaction.details.amount,
|
||||
token?.decimals,
|
||||
)
|
||||
: transaction.details.amount
|
||||
}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={token?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!isLastItem && (
|
||||
<View style={[styles.txDivider, { backgroundColor: textColor }]} />
|
||||
)}
|
||||
|
||||
+169
-115
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -171,7 +171,12 @@ export default function ConfirmTxPage(props) {
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{ textTransform: 'capitalize' }}
|
||||
content={t('wallet.stablecoinSend.chainSwap')}
|
||||
content={t('screens.inAccount.expandedTxPage.chainSwap', {
|
||||
context:
|
||||
paymentInformation.destinationChain === 'lightning'
|
||||
? 'lightning'
|
||||
: 'other',
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{transaction.details.isHoldInvoice &&
|
||||
isPending &&
|
||||
!transaction.details.didClaimHTLC && (
|
||||
<TouchableOpacity
|
||||
onPress={claimHTLC}
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
backgroundColor: theme
|
||||
? COLORS.darkModeText
|
||||
: COLORS.primary,
|
||||
},
|
||||
shouldStack ? styles.buttonStacked : styles.buttonColumn,
|
||||
]}
|
||||
disabled={isClaimingHtlc}
|
||||
>
|
||||
{isClaimingHtlc ? (
|
||||
<ActivityIndicator
|
||||
color={
|
||||
theme ? COLORS.lightModeText : COLORS.darkModeText
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ThemeText
|
||||
styles={{
|
||||
includeFontPadding: false,
|
||||
color: theme
|
||||
? COLORS.lightModeText
|
||||
: COLORS.darkModeText,
|
||||
}}
|
||||
{...getLabelProps(1)}
|
||||
content={claimHTLCLabel}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isFailedPayment && (
|
||||
<TouchableOpacity
|
||||
onPress={handleContactSupport}
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
backgroundColor: theme
|
||||
? COLORS.darkModeText
|
||||
: COLORS.primary,
|
||||
},
|
||||
shouldStack ? styles.buttonStacked : styles.buttonColumn,
|
||||
]}
|
||||
>
|
||||
<ThemeText
|
||||
styles={{
|
||||
includeFontPadding: false,
|
||||
color: theme ? COLORS.lightModeText : COLORS.darkModeText,
|
||||
}}
|
||||
{...getLabelProps(1)}
|
||||
content={contactSupportLabel}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Receipt Dots */}
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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}})",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user