mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
prepfer-async mode for melting
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
@@ -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} .",
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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<MeltParams>
|
||||
inFlightRequest?: InFlightRequest<MeltParams>,
|
||||
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)
|
||||
|
||||
@@ -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<number | undefined>(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
|
||||
icon="faTriangleExclamation"
|
||||
@@ -1146,6 +1180,25 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props)
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{resultModalInfo &&
|
||||
transactionStatus === TransactionStatus.PENDING &&
|
||||
!!pendingAsyncMeltId && (
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon="faClock"
|
||||
iconColor={colors.palette.iconBlue200}
|
||||
title={translate('payCommon_isInProgress')}
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'commonClose'}
|
||||
onPress={gotoWallet}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleResultModal}
|
||||
|
||||
@@ -102,7 +102,7 @@ const getRandomPictures = async function () {
|
||||
return avatars
|
||||
}
|
||||
|
||||
// TODO remove pubkey params
|
||||
|
||||
const createWalletProfile = async function (walletId: string, seedHash: string) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/profile'
|
||||
const method = 'POST'
|
||||
@@ -126,7 +126,6 @@ const createWalletProfile = async function (walletId: string, seedHash: string)
|
||||
}
|
||||
|
||||
// what is passed in {update} gets updated
|
||||
// TODO remove pubkey from url and params
|
||||
const updateWalletProfile = async function (update: {name?: string, lud16?: string, avatar?: string}) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/profile'
|
||||
const method = 'PUT'
|
||||
@@ -149,7 +148,7 @@ const updateWalletProfile = async function (update: {name?: string, lud16?: stri
|
||||
return walletProfile
|
||||
}
|
||||
|
||||
// TODO remove pubkey from url and params
|
||||
|
||||
const updateWalletProfileNip05 = async function (update: {newPubkey: string, name: string, nip05: string, lud16: string, avatar: string}) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/profile'
|
||||
const method = 'PUT'
|
||||
@@ -175,7 +174,6 @@ const updateWalletProfileNip05 = async function (update: {newPubkey: string, nam
|
||||
}
|
||||
|
||||
|
||||
// TODO remove pubkey from url and params
|
||||
const updateDeviceToken = async function (deviceToken: string) {
|
||||
const url = MINIBITS_SERVER_API_HOST + '/profile'
|
||||
const method = 'PUT'
|
||||
|
||||
@@ -32,6 +32,45 @@ const {
|
||||
|
||||
export const SEND_TASK = 'sendTask'
|
||||
|
||||
const _monitorSentProofs = async (params: {
|
||||
mintUrl: string
|
||||
proofsToSend: CashuProof[]
|
||||
}) => {
|
||||
const { mintUrl, proofsToSend } = params
|
||||
const proofsToSync = proofsStore.getByMint(mintUrl, {isPending: true})
|
||||
const wsMint = new CashuMint(mintUrl)
|
||||
const wsWallet = new CashuWallet(wsMint)
|
||||
|
||||
try {
|
||||
log.trace('[send] Subscribing to proofStateUpdates for sent proof', {secret: proofsToSend[0]})
|
||||
const unsub = await wsWallet.on.proofStateUpdates(
|
||||
[proofsToSend[0]],
|
||||
async (proofState: ProofState) => {
|
||||
log.trace(`Websocket: proof state updated: ${proofState.state} with secret: ${proofsToSend[0].secret}`)
|
||||
if (proofState.state == CheckStateEnum.SPENT) {
|
||||
WalletTask.syncStateWithMintQueueAwaitable({proofsToSync, mintUrl, isPending: true})
|
||||
unsub()
|
||||
}
|
||||
},
|
||||
async (error: any) => {
|
||||
throw error
|
||||
},
|
||||
)
|
||||
} catch (error: any) {
|
||||
log.error(Err.NETWORK_ERROR,
|
||||
"Error in websocket subscription. Starting poller.",
|
||||
error.message,
|
||||
)
|
||||
poller(
|
||||
`syncStateWithMintPoller-${mintUrl}`,
|
||||
WalletTask.syncStateWithMintQueueAwaitable,
|
||||
{interval: 10 * 1000, maxPolls: 3, maxErrors: 1},
|
||||
{proofsToSend, mintUrl, isPending: true},
|
||||
)
|
||||
.then(() => log.trace('[syncStateWithMintPoller]', 'polling completed', {mintUrl}))
|
||||
}
|
||||
}
|
||||
|
||||
export const sendTask = async function (
|
||||
mintBalanceToSendFrom: MintBalance,
|
||||
amountToSend: number,
|
||||
@@ -135,48 +174,9 @@ export const sendTask = async function (
|
||||
|
||||
log.trace('[send] totalBalance after', balanceAfter)
|
||||
|
||||
// Start polling for accepted payment it is not an offline send
|
||||
// Start monitoring for accepted payment if it is not an offline send
|
||||
if(selectedProofs.length === 0) {
|
||||
|
||||
const proofsToSync = proofsStore.getByMint(mintUrl, {isPending: true})
|
||||
|
||||
const wsMint = new CashuMint(mintUrl)
|
||||
const wsWallet = new CashuWallet(wsMint)
|
||||
|
||||
try {
|
||||
log.trace('[send] Subscribing to onProofStateUpdates for proof', {secret: proofsToSend[0]})
|
||||
const unsub = await wsWallet.on.proofStateUpdates(
|
||||
[proofsToSend[0]],
|
||||
async (proofState: ProofState) => {
|
||||
log.trace(`Websocket: proof state updated: ${proofState.state} with secret: ${proofsToSend[0].secret}`)
|
||||
|
||||
if (proofState.state == CheckStateEnum.SPENT) {
|
||||
WalletTask.syncStateWithMintQueueAwaitable({proofsToSync, mintUrl, isPending: true})
|
||||
unsub()
|
||||
}
|
||||
},
|
||||
async (error: any) => {
|
||||
throw error
|
||||
}
|
||||
)
|
||||
} catch (error: any) {
|
||||
log.error(Err.NETWORK_ERROR,
|
||||
"Error in websocket subscription. Starting poller.",
|
||||
error.message
|
||||
)
|
||||
|
||||
poller(
|
||||
`syncStateWithMintPoller-${mintUrl}`,
|
||||
WalletTask.syncStateWithMintQueueAwaitable,
|
||||
{
|
||||
interval: 10 * 1000,
|
||||
maxPolls: 3,
|
||||
maxErrors: 1
|
||||
},
|
||||
{proofsToSend, mintUrl, isPending: true}
|
||||
)
|
||||
.then(() => log.trace('[syncStateWithMintPoller]', 'polling completed', {mintUrl}))
|
||||
}
|
||||
_monitorSentProofs({mintUrl, proofsToSend})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {CashuUtils} from '../cashu/cashuUtils'
|
||||
import AppError, {Err} from '../../utils/AppError'
|
||||
import {MeltProofsResponse, MeltQuoteBolt11Response, MeltQuoteResponse, MeltQuoteState, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {Mint as CashuMint, Wallet as CashuWallet, MeltProofsResponse, MeltQuoteBolt11Response, MeltQuoteResponse, MeltQuoteState, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {rootStoreInstance} from '../../models'
|
||||
import { TransactionTaskResult, WalletTask } from '../walletService'
|
||||
import { MintBalance } from '../../models/Mint'
|
||||
@@ -8,6 +8,7 @@ import { Proof } from '../../models/Proof'
|
||||
import { Transaction, TransactionData, TransactionStatus, TransactionType } from '../../models/Transaction'
|
||||
import { log } from '../logService'
|
||||
import { WalletUtils } from './utils'
|
||||
import { poller } from '../../utils/poller'
|
||||
import {isBefore} from 'date-fns'
|
||||
import { MintUnit, formatCurrency, getCurrency } from './currency'
|
||||
import { NostrEvent } from '../nostrService'
|
||||
@@ -24,6 +25,49 @@ const {
|
||||
|
||||
export const TRANSFER_TASK = 'transferTask'
|
||||
|
||||
const _monitorAsyncMeltQuote = async (params: {
|
||||
mintUrl: string
|
||||
unit: MintUnit
|
||||
quoteId: string
|
||||
proofsToMeltFrom: Proof[]
|
||||
proofsToMeltFromAmount: number
|
||||
amountToTransfer: number
|
||||
meltFeeReserve: number
|
||||
transactionId: number
|
||||
}) => {
|
||||
const { mintUrl, quoteId } = params
|
||||
const wsMint = new CashuMint(mintUrl)
|
||||
const wsWallet = new CashuWallet(wsMint)
|
||||
|
||||
try {
|
||||
log.trace('[transfer] Subscribing to meltQuoteUpdates for async melt', { quoteId })
|
||||
const unsub = await wsWallet.on.meltQuoteUpdates(
|
||||
[quoteId],
|
||||
async (updatedQuote: MeltQuoteBolt11Response) => {
|
||||
if (updatedQuote.state === MeltQuoteState.PAID ||
|
||||
updatedQuote.state === MeltQuoteState.UNPAID) {
|
||||
WalletTask.handlePendingMeltTask(params)
|
||||
unsub()
|
||||
}
|
||||
},
|
||||
async (error: any) => {
|
||||
throw error
|
||||
},
|
||||
)
|
||||
} catch (error: any) {
|
||||
log.error(Err.NETWORK_ERROR,
|
||||
'[transfer] WebSocket error for async melt, starting poller.',
|
||||
error.message,
|
||||
)
|
||||
poller(
|
||||
`meltQuotePoller-${quoteId}`,
|
||||
WalletTask.handlePendingMeltTask,
|
||||
{ interval: 15 * 1000, maxPolls: 8, maxErrors: 2 },
|
||||
params,
|
||||
).then(() => log.trace('[meltQuotePoller] polling completed', { quoteId }))
|
||||
}
|
||||
}
|
||||
|
||||
export const transferTask = async function (
|
||||
mintBalanceToTransferFrom: MintBalance,
|
||||
amountToTransfer: number,
|
||||
@@ -149,6 +193,9 @@ export const transferTask = async function (
|
||||
proofsToMeltFromAmount = CashuUtils.getProofsAmount(proofsToMeltFrom)
|
||||
}
|
||||
|
||||
// TODO: if proofsToMelFromAmount is significantly higher than amountWithFees,
|
||||
// add swap with the mint to get denominations that fit the needed amount
|
||||
|
||||
// move proofs to PENDING state and link them to current tx
|
||||
proofsStore.addOrUpdate(proofsToMeltFrom,
|
||||
{
|
||||
@@ -194,10 +241,11 @@ export const transferTask = async function (
|
||||
unit,
|
||||
meltQuote,
|
||||
proofsToMeltFrom,
|
||||
transactionId,
|
||||
transactionId,
|
||||
{ preferAsync: true },
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (WalletUtils.shouldHealOutputsError(e)) {
|
||||
if (WalletUtils.shouldHealOutputsError(e)) {
|
||||
log.error('[transferTask] Increasing proofsCounter outdated values and repeating payLightningMelt.')
|
||||
meltResponse = await walletStore.payLightningMelt(
|
||||
mintUrl,
|
||||
@@ -205,7 +253,7 @@ export const transferTask = async function (
|
||||
meltQuote,
|
||||
proofsToMeltFrom,
|
||||
transactionId,
|
||||
{increaseCounterBy: 10}
|
||||
{ increaseCounterBy: 10, preferAsync: true },
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
@@ -301,20 +349,43 @@ export const transferTask = async function (
|
||||
nwcEvent
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else if(meltResponse.quote.state === MeltQuoteState.PENDING) {
|
||||
} else if(meltResponse.quote.state === MeltQuoteState.PENDING) {
|
||||
|
||||
log.debug('[transfer] Invoice PENDING', {
|
||||
metResponseQuote: meltResponse.quote,
|
||||
transactionId
|
||||
log.debug('[transfer] Invoice PENDING, async melt in progress', {
|
||||
quoteId: meltResponse.quote.quote,
|
||||
transactionId,
|
||||
})
|
||||
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.update({
|
||||
status: TransactionStatus.PENDING,
|
||||
data: JSON.stringify(transactionData),
|
||||
})
|
||||
|
||||
// Fire-and-forget: monitors quote via websocket (poller fallback) and
|
||||
// updates the transaction + emits ev_asyncMeltResult when resolved.
|
||||
_monitorAsyncMeltQuote({
|
||||
mintUrl,
|
||||
unit,
|
||||
quoteId: meltResponse.quote.quote,
|
||||
proofsToMeltFrom,
|
||||
proofsToMeltFromAmount,
|
||||
amountToTransfer,
|
||||
meltFeeReserve,
|
||||
transactionId,
|
||||
})
|
||||
|
||||
return {
|
||||
taskFunction: TRANSFER_TASK,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: `Lightning payment did not complete in time. Your ecash will remain pending until the payment completes or fails.`,
|
||||
meltQuote,
|
||||
nwcEvent
|
||||
message: 'Lightning payment is in progress. Check your transaction history for the result.',
|
||||
meltQuote: meltResponse.quote,
|
||||
nwcEvent,
|
||||
} as TransactionTaskResult
|
||||
|
||||
} else {
|
||||
|
||||
@@ -148,9 +148,19 @@ type WalletTaskService = {
|
||||
mintQuote: string
|
||||
}) => Promise<{recoveredAmount: number}>
|
||||
recoverMeltQuoteChange: (params: {
|
||||
mintUrl: string,
|
||||
mintUrl: string,
|
||||
meltQuote: string | MeltQuoteResponse
|
||||
}) => Promise<{recoveredAmount: number}>
|
||||
handlePendingMeltTask: (params: {
|
||||
mintUrl: string
|
||||
unit: MintUnit
|
||||
quoteId: string
|
||||
proofsToMeltFrom: Proof[]
|
||||
proofsToMeltFromAmount: number
|
||||
amountToTransfer: number
|
||||
meltFeeReserve: number
|
||||
transactionId: number
|
||||
}) => Promise<void>
|
||||
}
|
||||
|
||||
export interface WalletTaskResult {
|
||||
@@ -2321,6 +2331,113 @@ const recoverMintQuote = async (
|
||||
}
|
||||
|
||||
|
||||
const handlePendingMeltTask = async (params: {
|
||||
mintUrl: string
|
||||
unit: MintUnit
|
||||
quoteId: string
|
||||
proofsToMeltFrom: Proof[]
|
||||
proofsToMeltFromAmount: number
|
||||
amountToTransfer: number
|
||||
meltFeeReserve: number
|
||||
transactionId: number
|
||||
}): Promise<void> => {
|
||||
const { mintUrl, unit, quoteId, proofsToMeltFrom, proofsToMeltFromAmount,
|
||||
amountToTransfer, meltFeeReserve, transactionId } = params
|
||||
|
||||
const transaction = transactionsStore.findById(transactionId)
|
||||
if (!transaction || transaction.status !== TransactionStatus.PENDING) return
|
||||
|
||||
const quote = await walletStore.checkLightningMeltQuote(mintUrl, quoteId)
|
||||
|
||||
if (quote.state === MeltQuoteState.PAID) {
|
||||
proofsStore.moveToSpent(proofsToMeltFrom)
|
||||
|
||||
let totalFeePaid = proofsToMeltFromAmount - amountToTransfer
|
||||
let lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
const meltFeePaid = meltFeeReserve
|
||||
let returnedAmount = 0
|
||||
let outputToken: string | undefined
|
||||
|
||||
// Call completeMelt with the stored MeltPreview to unbind the change blind signatures.
|
||||
// This is a POST to the mint; different from the GET quote check above.
|
||||
try {
|
||||
const mintInstance = mintsStore.findByUrl(mintUrl)
|
||||
if (mintInstance && transaction.keysetId) {
|
||||
const currentCounter = mintInstance.getProofsCounterByKeysetId!(transaction.keysetId)
|
||||
const meltCounterValue = currentCounter?.getMeltCounterValue(transactionId)
|
||||
if (meltCounterValue?.meltPreview) {
|
||||
const cashuWallet = await walletStore.getWallet(mintUrl, unit,
|
||||
{ withSeed: true, keysetId: transaction.keysetId })
|
||||
const { change } = await cashuWallet.completeMelt(meltCounterValue.meltPreview)
|
||||
currentCounter.removeMeltCounterValue(transactionId)
|
||||
if (change.length > 0) {
|
||||
proofsStore.addOrUpdate(change, {
|
||||
mintUrl, tId: transactionId, unit, isPending: false, isSpent: false,
|
||||
})
|
||||
returnedAmount = CashuUtils.getProofsAmount(change)
|
||||
outputToken = getEncodedToken({ mint: mintUrl, proofs: change, unit })
|
||||
totalFeePaid = totalFeePaid - returnedAmount
|
||||
lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error('[handlePendingMelt] change recovery failed, completing without change', e.message)
|
||||
}
|
||||
|
||||
const txData: TransactionData[] = transaction.data ? JSON.parse(transaction.data) : []
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance
|
||||
txData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
lightningFeePaid,
|
||||
meltFeePaid,
|
||||
returnedAmount,
|
||||
//@ts-ignore
|
||||
preimage: quote.payment_preimage,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
const updatePayload: any = {
|
||||
status: TransactionStatus.COMPLETED,
|
||||
data: JSON.stringify(txData),
|
||||
fee: totalFeePaid,
|
||||
balanceAfter,
|
||||
}
|
||||
if (outputToken) updatePayload.outputToken = outputToken
|
||||
//@ts-ignore
|
||||
if (quote.payment_preimage) updatePayload.proof = quote.payment_preimage
|
||||
transaction.update(updatePayload)
|
||||
|
||||
log.debug('[handlePendingMelt] Transaction completed', { transactionId, totalFeePaid })
|
||||
|
||||
EventEmitter.emit('ev_asyncMeltResult', {
|
||||
transactionId,
|
||||
status: TransactionStatus.COMPLETED,
|
||||
message: `Lightning invoice paid. Fee: ${formatCurrency(transaction.fee, getCurrency(unit).code)} ${getCurrency(unit).code}.`,
|
||||
})
|
||||
|
||||
} else if (quote.state === MeltQuoteState.UNPAID) {
|
||||
proofsStore.revertToSpendable(proofsToMeltFrom)
|
||||
|
||||
const txData: TransactionData[] = transaction.data ? JSON.parse(transaction.data) : []
|
||||
txData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(new AppError(Err.MINT_ERROR, 'Async lightning payment failed.')),
|
||||
createdAt: new Date(),
|
||||
})
|
||||
transaction.update({ status: TransactionStatus.ERROR, data: JSON.stringify(txData) })
|
||||
|
||||
log.debug('[handlePendingMelt] Transaction failed (UNPAID)', { transactionId })
|
||||
|
||||
EventEmitter.emit('ev_asyncMeltResult', {
|
||||
transactionId,
|
||||
status: TransactionStatus.ERROR,
|
||||
message: 'Lightning payment failed. Ecash returned to spendable balance.',
|
||||
})
|
||||
}
|
||||
// MeltQuoteState.PENDING: no-op, ws/poller will call again
|
||||
}
|
||||
|
||||
|
||||
const handleClaimQueue = async function (): Promise<void> {
|
||||
|
||||
log.info('[handleClaimQueue] start')
|
||||
@@ -3145,4 +3262,5 @@ export const WalletTask: WalletTaskService = {
|
||||
testQueue,
|
||||
recoverMintQuote,
|
||||
recoverMeltQuoteChange,
|
||||
handlePendingMeltTask,
|
||||
}
|
||||
Reference in New Issue
Block a user