From 27734a1032cf89c7894574ee77184bc28b973202 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Sat, 9 May 2026 23:40:27 +0200 Subject: [PATCH] prepfer-async mode for melting --- package.json | 2 +- src/i18n_messages/en.json | 1 + src/i18n_messages/es.json | 1 + src/i18n_messages/pt.json | 1 + src/i18n_messages/sk.json | 1 + src/models/WalletStore.ts | 7 +- src/screens/TransferScreen.tsx | 55 ++++++++++++- src/services/minibitsService.ts | 6 +- src/services/wallet/sendTask.ts | 82 +++++++++---------- src/services/wallet/transferTask.ts | 93 ++++++++++++++++++--- src/services/walletService.ts | 120 +++++++++++++++++++++++++++- 11 files changed, 307 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 7ec1dad1..f432bb60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.4.2-beta.3", + "version": "0.4.2-beta.4", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json index 5a76a4d8..6810590b 100644 --- a/src/i18n_messages/en.json +++ b/src/i18n_messages/en.json @@ -293,6 +293,7 @@ "payCommon_failed": "Payment failed", "payCommon_insufficientFunds": "There are not enough funds to send this amount.", "payCommon_isPending": "Payment is pending", + "payCommon_isInProgress": "Payment in progress", "payCommon_maximumPay": "Maximum amount to pay is %{amount} %{currency}.", "payCommon_minimumPay": "Minimal payment amount is %{amount} %{currency}.", "payCommon_minimumWithdraw": "Minimal withdraw amount is %{amount} %{currency}.", diff --git a/src/i18n_messages/es.json b/src/i18n_messages/es.json index a1722dfa..bee2c775 100644 --- a/src/i18n_messages/es.json +++ b/src/i18n_messages/es.json @@ -292,6 +292,7 @@ "payCommon_failed": "Pago fallido", "payCommon_insufficientFunds": "No hay fondos suficientes para enviar esta cantidad.", "payCommon_isPending": "El pago está pendiente", + "payCommon_isInProgress": "Pago en curso", "payCommon_maximumPay": "El monto máximo a pagar es %{amount} %{currency} .", "payCommon_minimumPay": "El monto mínimo de pago es %{amount} %{currency} .", "payCommon_minimumWithdraw": "El monto mínimo de retiro es %{amount} %{currency} .", diff --git a/src/i18n_messages/pt.json b/src/i18n_messages/pt.json index 96a27a73..65649e56 100644 --- a/src/i18n_messages/pt.json +++ b/src/i18n_messages/pt.json @@ -293,6 +293,7 @@ "payCommon_failed": "Pagamento falhou", "payCommon_insufficientFunds": "Fundos insuficientes para enviar este valor.", "payCommon_isPending": "Pagamento pendente", + "payCommon_isInProgress": "Pagamento em andamento", "payCommon_maximumPay": "Valor máximo a pagar é %{amount} %{currency}.", "payCommon_minimumPay": "Valor mínimo de pagamento é %{amount} %{currency}.", "payCommon_minimumWithdraw": "Valor mínimo de saque é %{amount} %{currency}.", diff --git a/src/i18n_messages/sk.json b/src/i18n_messages/sk.json index c21125fe..f7b74bee 100644 --- a/src/i18n_messages/sk.json +++ b/src/i18n_messages/sk.json @@ -293,6 +293,7 @@ "payCommon_failed": "Platba zlyhala.", "payCommon_insufficientFunds": "Nemáš dostatočné prostriedky na poslanie tejto sumy.", "payCommon_isPending": "Platba je bežiaca", + "payCommon_isInProgress": "Platba prebieha", "payCommon_maximumPay": "Najvyššia suma na zaplatenie je %{amount}.", "payCommon_minimumPay": "Minimálna suma platby je %{amount} %{currency} .", "payCommon_minimumWithdraw": "Najmenšia suma na zaplatenie je %{amount} %{currnecy}", diff --git a/src/models/WalletStore.ts b/src/models/WalletStore.ts index ee71fc45..7d11cd91 100644 --- a/src/models/WalletStore.ts +++ b/src/models/WalletStore.ts @@ -29,7 +29,7 @@ import { InFlightRequest, Mint } from './Mint' import { getRootStore } from './helpers/getRootStore' import { Transaction } from './Transaction' -// +// r /* Not persisted, in-memory only model of the cashu-ts wallet instances and wallet keys persisted in the device secure store. @@ -907,7 +907,8 @@ export const WalletStoreModel = types transactionId: number, options?: { increaseCounterBy?: number, - inFlightRequest?: InFlightRequest + inFlightRequest?: InFlightRequest, + preferAsync?: boolean, } ) { const mintInstance = self.getMintModelInstance(mintUrl) @@ -971,7 +972,7 @@ export const WalletStoreModel = types try { // Step 2: Complete the melt (sends to mint and constructs change proofs) - const meltResponse: MeltProofsResponse = yield cashuWallet.completeMelt(meltPreview) + const meltResponse: MeltProofsResponse = yield cashuWallet.completeMelt(meltPreview, undefined, options?.preferAsync) // Remove the stored preview on success currentCounter.removeMeltCounterValue(transactionId) diff --git a/src/screens/TransferScreen.tsx b/src/screens/TransferScreen.tsx index ddc1cfdf..e6db9d3a 100644 --- a/src/screens/TransferScreen.tsx +++ b/src/screens/TransferScreen.tsx @@ -39,6 +39,7 @@ import { LnurlClient, LNURLPayParams } from '../services/lnurlService' import { CurrencyCode, MintUnit, convertToFromSats, getCurrency } from "../services/wallet/currency" import { FeeBadge } from './Wallet/FeeBadge' import { MeltQuoteBolt11Response } from '@cashu/cashu-ts' +import EventEmitter from '../utils/eventEmitter' import { MintHeader } from './Mints/MintHeader' import { MintBalanceSelector } from './Mints/MintBalanceSelector' import numbro from 'numbro' @@ -103,6 +104,7 @@ type TransferAction = | { type: 'QUOTE_FINISHED' } | { type: 'TRANSFER_START' } | { type: 'TRANSFER_COMPLETE'; transaction?: Transaction; transactionStatus: TransactionStatus; finalFee: number; resultModalInfo: { status: TransactionStatus; title?: string; message: string } } + | { type: 'UPDATE_RESULT_MODAL'; transactionStatus: TransactionStatus; resultModalInfo: { status: TransactionStatus; message: string } } | { type: 'TOGGLE_RESULT_MODAL' } | { type: 'SET_INFO'; message: string } | { type: 'CLEAR_INFO' } @@ -196,6 +198,13 @@ function transferReducer(state: TransferState, action: TransferAction): Transfer isResultModalVisible: true, } + case 'UPDATE_RESULT_MODAL': + return { + ...state, + transactionStatus: action.transactionStatus, + resultModalInfo: action.resultModalInfo, + } + case 'TOGGLE_RESULT_MODAL': return { ...state, isResultModalVisible: !state.isResultModalVisible } @@ -434,6 +443,23 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) }, [route.params?.paymentOption]), ) + const [pendingAsyncMeltId, setPendingAsyncMeltId] = useState(undefined) + + useEffect(() => { + if (!pendingAsyncMeltId) return + const handler = (result: { transactionId: number; status: TransactionStatus; message: string }) => { + if (result.transactionId !== pendingAsyncMeltId) return + setPendingAsyncMeltId(undefined) + dispatch({ + type: 'UPDATE_RESULT_MODAL', + transactionStatus: result.status, + resultModalInfo: { status: result.status, message: result.message }, + }) + } + EventEmitter.on('ev_asyncMeltResult', handler) + return () => EventEmitter.off('ev_asyncMeltResult', handler) + }, [pendingAsyncMeltId]) + const handleError = function(e: AppError): void { dispatch({ type: 'SET_ERROR', error: e }) } @@ -750,6 +776,7 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) draftTransactionIdRef.current || undefined ) + // TODO: handle async melt case - show in progress (not pending) state in modal and update when completed await handleTransferTaskResult(result) } catch (e: any) { @@ -805,6 +832,7 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) let resultModalInfo: { status: TransactionStatus; title?: string; message: string } if (error) { + // mint returned error with proofs pending - show pending message in modal instead of error if (status === TransactionStatus.PENDING) { resultModalInfo = { status, message } } else { @@ -815,6 +843,11 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) } } } else { + if (status === TransactionStatus.PENDING) { + // Async melt: monitor is running in background and will emit ev_asyncMeltResult + // when the quote resolves. The listener above will update the modal at that point. + setPendingAsyncMeltId(transaction.id) + } // Success path if (isInvoiceDonation && donationForName && (status === TransactionStatus.COMPLETED || status === TransactionStatus.RECOVERED)) { @@ -1129,7 +1162,8 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) )} {resultModalInfo && - transactionStatus === TransactionStatus.PENDING && ( + transactionStatus === TransactionStatus.PENDING && + !pendingAsyncMeltId && ( <> )} + {resultModalInfo && + transactionStatus === TransactionStatus.PENDING && + !!pendingAsyncMeltId && ( + <> + + +