mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
666 lines
25 KiB
TypeScript
666 lines
25 KiB
TypeScript
import {isAlive} from 'mobx-state-tree'
|
||
import {getDecodedToken} from '@cashu/cashu-ts'
|
||
import {log} from '../../logService'
|
||
import {NetworkError} from '../../../utils/AppError'
|
||
import {rootStoreInstance} from '../../../models'
|
||
import {Proof, ProofState} from '../../../models/Proof'
|
||
import {MintStatus} from '../../../models/Mint'
|
||
import {
|
||
TransactionStatus,
|
||
TransactionType,
|
||
} from '../../../models/Transaction'
|
||
import {CashuUtils} from '../../cashu/cashuUtils'
|
||
import {SyncQueue} from '../../syncQueueService'
|
||
import {stopPolling} from '../../../utils/poller'
|
||
import {sendTask} from '../sendTask'
|
||
import {createQueueAwaitable} from '../queueHelper'
|
||
import {receiveBatchTask} from './receiveOperations'
|
||
import {MeltOperationService} from './meltOperations'
|
||
import {
|
||
MAX_SWAP_INPUT_SIZE,
|
||
MAX_SYNC_INPUT_SIZE,
|
||
SWAP_ALL_TASK,
|
||
SWAP_DENOMINATION_TASK,
|
||
SYNC_STATE_WITH_ALL_MINTS_TASK,
|
||
SYNC_STATE_WITH_MINT_TASK,
|
||
SyncStateTaskResult,
|
||
TransactionStateUpdate,
|
||
WalletTaskResult,
|
||
} from '../types'
|
||
|
||
const {
|
||
mintsStore,
|
||
proofsStore,
|
||
transactionsStore,
|
||
walletStore,
|
||
} = rootStoreInstance
|
||
|
||
/**
|
||
* Sync wallet proof state with mint reality (SPENT / PENDING / UNSPENT)
|
||
* Used both for pending proof resolution and recovery of broken wallet state.
|
||
*/
|
||
const syncStateWithMintTask = async function (
|
||
options: {
|
||
proofsToSync: Proof[]
|
||
mintUrl: string
|
||
proofState: ProofState
|
||
},
|
||
): Promise<SyncStateTaskResult> {
|
||
const {proofsToSync, mintUrl, proofState} = options
|
||
const mint = mintsStore.findByUrl(mintUrl)
|
||
|
||
log.trace('[syncStateWithMintTask] start', {mintUrl, proofCount: proofsToSync.length, proofState})
|
||
|
||
const transactionStateUpdates: TransactionStateUpdate[] = []
|
||
const completedTxIds: number[] = []
|
||
const errorTxIds: number[] = []
|
||
const pendingTxIds: number[] = []
|
||
const revertedTxIds: number[] = []
|
||
|
||
try {
|
||
const aliveProofs = proofsToSync.filter(p => isAlive(p))
|
||
|
||
if (aliveProofs.length === 0) {
|
||
const message = `No ${proofState === 'PENDING' ? 'pending ' : ''}proofs to sync with mint`
|
||
log.trace('[syncStateWithMintTask]', message)
|
||
return {
|
||
taskFunction: SYNC_STATE_WITH_MINT_TASK,
|
||
mintUrl,
|
||
message,
|
||
transactionStateUpdates,
|
||
completedTransactionIds: [],
|
||
errorTransactionIds: [],
|
||
revertedTransactionIds: [],
|
||
}
|
||
}
|
||
|
||
const statesFromMint = await walletStore.getProofsStatesFromMint(
|
||
mintUrl,
|
||
mint?.units?.[0] ?? 'sat',
|
||
aliveProofs,
|
||
)
|
||
|
||
if (mint) mint.setStatus(MintStatus.ONLINE)
|
||
|
||
const byMintState = {
|
||
SPENT: statesFromMint.SPENT.map(p => p.secret),
|
||
PENDING: statesFromMint.PENDING.map(p => p.secret),
|
||
UNSPENT: statesFromMint.UNSPENT.map(p => p.secret),
|
||
}
|
||
|
||
const secrets = {
|
||
spent: new Set(byMintState.SPENT),
|
||
pending: new Set(byMintState.PENDING),
|
||
unspent: new Set(byMintState.UNSPENT),
|
||
}
|
||
|
||
log.debug('[syncStateWithMintTask] Mint state', {
|
||
spent: secrets.spent.size,
|
||
pending: secrets.pending.size,
|
||
unspent: secrets.unspent.size,
|
||
proofState,
|
||
})
|
||
|
||
const groupByTId = (proofs: Proof[]) => {
|
||
const map = new Map<number, {proofs: Proof[]; amount: number}>()
|
||
for (const p of proofs) {
|
||
if (!p.tId) continue
|
||
const entry = map.get(p.tId) ?? {proofs: [], amount: 0}
|
||
entry.proofs.push(p)
|
||
entry.amount += p.amount
|
||
map.set(p.tId, entry)
|
||
}
|
||
return map
|
||
}
|
||
|
||
// 1. Proofs now SPENT at mint → transaction succeeded
|
||
if (secrets.spent.size > 0) {
|
||
const spentProofs = aliveProofs.filter(p => secrets.spent.has(p.secret))
|
||
const spentByTx = groupByTId(spentProofs)
|
||
proofsStore.moveToSpent(spentProofs)
|
||
|
||
for (const [tId, {amount: spentAmount}] of spentByTx) {
|
||
const tx = transactionsStore.findById(tId)
|
||
if (!tx) {
|
||
errorTxIds.push(tId)
|
||
transactionStateUpdates.push({
|
||
tId,
|
||
updatedStatus: TransactionStatus.ERROR,
|
||
message: 'Transaction not found in DB',
|
||
})
|
||
continue
|
||
}
|
||
|
||
if (spentAmount < tx.amount) {
|
||
errorTxIds.push(tId)
|
||
transactionStateUpdates.push({
|
||
tId,
|
||
amount: tx.amount,
|
||
spentByMintAmount: spentAmount,
|
||
updatedStatus: TransactionStatus.ERROR,
|
||
message: 'Partial spend detected – proofs reused',
|
||
})
|
||
|
||
if (proofState === 'PENDING') {
|
||
const stillUnspent = aliveProofs.filter(p => secrets.unspent.has(p.secret))
|
||
proofsStore.revertToSpendable(stillUnspent)
|
||
}
|
||
} else if (tx.status !== TransactionStatus.REVERTED) {
|
||
completedTxIds.push(tId)
|
||
const update: TransactionStateUpdate = {
|
||
tId,
|
||
amount: tx.amount,
|
||
spentByMintAmount: spentAmount,
|
||
updatedStatus: TransactionStatus.COMPLETED,
|
||
}
|
||
if (tx.type === TransactionType.TRANSFER && tx.quote) {
|
||
update.meltQuoteToRecover = tx.quote
|
||
}
|
||
transactionStateUpdates.push(update)
|
||
}
|
||
}
|
||
|
||
for (const update of transactionStateUpdates) {
|
||
if (update.meltQuoteToRecover) {
|
||
const {recoveredAmount} = await MeltOperationService.recoverMeltQuoteChange({
|
||
mintUrl,
|
||
meltQuote: update.meltQuoteToRecover,
|
||
})
|
||
update.recoveredChangeAmount = recoveredAmount
|
||
}
|
||
}
|
||
|
||
if (completedTxIds.length > 0) {
|
||
await transactionsStore.updateStatuses(
|
||
completedTxIds,
|
||
TransactionStatus.COMPLETED,
|
||
JSON.stringify({
|
||
status: TransactionStatus.COMPLETED,
|
||
spentStateUpdates: transactionStateUpdates.filter(u => completedTxIds.includes(u.tId)),
|
||
createdAt: new Date(),
|
||
}),
|
||
)
|
||
stopPolling(`syncStateWithMintPoller-${mintUrl}`)
|
||
}
|
||
if (errorTxIds.length > 0) {
|
||
await transactionsStore.updateStatuses(
|
||
errorTxIds,
|
||
TransactionStatus.ERROR,
|
||
JSON.stringify({
|
||
status: TransactionStatus.ERROR,
|
||
spentStateUpdates: transactionStateUpdates.filter(u => errorTxIds.includes(u.tId)),
|
||
createdAt: new Date(),
|
||
}),
|
||
)
|
||
stopPolling(`syncStateWithMintPoller-${mintUrl}`)
|
||
}
|
||
}
|
||
|
||
// 2. Proofs still PENDING at mint → keep pending in wallet
|
||
if (secrets.pending.size > 0) {
|
||
const newPendingProofs = aliveProofs.filter(p => secrets.pending.has(p.secret) && !proofsStore.pendingByMintSecrets.includes(p.secret))
|
||
|
||
if (newPendingProofs.length > 0) {
|
||
proofsStore.registerAsPendingAtMint(newPendingProofs)
|
||
const pendingByTx = groupByTId(newPendingProofs)
|
||
|
||
for (const [tId, {amount: pendingAmount}] of pendingByTx) {
|
||
if (!pendingTxIds.includes(tId)) pendingTxIds.push(tId)
|
||
transactionStateUpdates.push({
|
||
tId,
|
||
pendingByMintAmount: pendingAmount,
|
||
updatedStatus: TransactionStatus.PENDING,
|
||
})
|
||
}
|
||
|
||
if (proofState !== 'PENDING') {
|
||
proofsStore.moveToPending(newPendingProofs)
|
||
}
|
||
|
||
if (pendingTxIds.length > 0) {
|
||
await transactionsStore.updateStatuses(
|
||
pendingTxIds,
|
||
TransactionStatus.PENDING,
|
||
JSON.stringify({
|
||
message: 'Waiting for a payment to settle',
|
||
pendingStateUpdates: transactionStateUpdates.filter(u => u.updatedStatus === TransactionStatus.PENDING),
|
||
createdAt: new Date(),
|
||
}),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. Proofs no longer pending at mint → lightning failed → revert
|
||
const noLongerPendingSecrets = proofsStore.pendingByMintSecrets.filter(
|
||
s => !secrets.pending.has(s),
|
||
)
|
||
|
||
if (noLongerPendingSecrets.length > 0) {
|
||
const revertedProofs: Proof[] = []
|
||
const revertedByTx = new Map<number, number>()
|
||
|
||
for (const secret of noLongerPendingSecrets) {
|
||
const proof = proofsStore.getBySecret(secret)
|
||
if (!proof || !proof.tId) continue
|
||
|
||
revertedProofs.push(proof)
|
||
revertedByTx.set(proof.tId, (revertedByTx.get(proof.tId) ?? 0) + proof.amount)
|
||
if (!revertedTxIds.includes(proof.tId)) revertedTxIds.push(proof.tId)
|
||
}
|
||
|
||
if (revertedProofs.length > 0) {
|
||
proofsStore.revertToSpendable(revertedProofs)
|
||
proofsStore.unregisterFromPendingAtMint(new Set(noLongerPendingSecrets))
|
||
|
||
for (const [tId, amount] of revertedByTx) {
|
||
transactionStateUpdates.push({
|
||
tId,
|
||
movedToSpendableAmount: amount,
|
||
updatedStatus: TransactionStatus.REVERTED,
|
||
})
|
||
}
|
||
|
||
if (revertedTxIds.length > 0) {
|
||
await transactionsStore.updateStatuses(
|
||
revertedTxIds,
|
||
TransactionStatus.REVERTED,
|
||
JSON.stringify({
|
||
message: 'Lightning payment failed – ecash returned to spendable balance',
|
||
revertedStateUpdates: transactionStateUpdates.filter(u => u.updatedStatus === TransactionStatus.REVERTED),
|
||
createdAt: new Date(),
|
||
}),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
taskFunction: SYNC_STATE_WITH_MINT_TASK,
|
||
mintUrl,
|
||
message: `Sync completed for ${aliveProofs.length} ${proofState === 'PENDING' ? 'pending ' : ''}proofs`,
|
||
transactionStateUpdates,
|
||
completedTransactionIds: completedTxIds,
|
||
errorTransactionIds: errorTxIds,
|
||
pendingTransactionIds: pendingTxIds,
|
||
revertedTransactionIds: revertedTxIds,
|
||
}
|
||
} catch (e: any) {
|
||
log.error('[syncStateWithMintTask] failed', {mintUrl, error: e.message, stack: e.stack})
|
||
|
||
if (mint && e instanceof NetworkError) {
|
||
mint.setStatus(MintStatus.OFFLINE)
|
||
}
|
||
|
||
return {
|
||
taskFunction: SYNC_STATE_WITH_MINT_TASK,
|
||
mintUrl,
|
||
message: `Sync failed: ${e.message}`,
|
||
error: e,
|
||
transactionStateUpdates,
|
||
completedTransactionIds: completedTxIds,
|
||
errorTransactionIds: errorTxIds,
|
||
pendingTransactionIds: pendingTxIds,
|
||
revertedTransactionIds: revertedTxIds,
|
||
}
|
||
}
|
||
}
|
||
|
||
const syncStateWithAllMintsTask = async function (options: {
|
||
proofState: ProofState
|
||
}): Promise<SyncStateTaskResult> {
|
||
log.trace('[syncStateWithAllMintsTask] start')
|
||
if (mintsStore.mintCount === 0) {
|
||
return {
|
||
taskFunction: SYNC_STATE_WITH_ALL_MINTS_TASK,
|
||
transactionStateUpdates: [],
|
||
completedTransactionIds: [],
|
||
errorTransactionIds: [],
|
||
revertedTransactionIds: [],
|
||
message: 'No mints',
|
||
}
|
||
}
|
||
|
||
const {proofState} = options
|
||
const maxBatchSize = MAX_SYNC_INPUT_SIZE
|
||
const transactionStateUpdates: TransactionStateUpdate[] = []
|
||
const completedTransactionIds: number[] = []
|
||
const errorTransactionIds: number[] = []
|
||
const revertedTransactionIds: number[] = []
|
||
const errors: string[] = []
|
||
|
||
for (const mint of mintsStore.allMints) {
|
||
const proofsToSync = proofsStore.getByMint(mint.mintUrl, {state: proofState})
|
||
const totalProofsCount = proofsToSync.length
|
||
|
||
if (totalProofsCount === 0) {
|
||
log.trace('[syncStateWithAllMintsTask] No proofs to sync, skipping...', {mint: mint.mintUrl})
|
||
continue
|
||
}
|
||
|
||
if (totalProofsCount > maxBatchSize) {
|
||
for (let i = 0; i < totalProofsCount; i += maxBatchSize) {
|
||
const batch = proofsToSync.slice(i, i + maxBatchSize)
|
||
const result = await syncStateWithMintTask({proofsToSync: batch, mintUrl: mint.mintUrl, proofState})
|
||
|
||
transactionStateUpdates.push(...result.transactionStateUpdates)
|
||
completedTransactionIds.push(...result.completedTransactionIds)
|
||
errorTransactionIds.push(...result.errorTransactionIds)
|
||
revertedTransactionIds.push(...result.revertedTransactionIds)
|
||
|
||
if (result.error) {
|
||
errors.push(result.error.message)
|
||
}
|
||
}
|
||
} else {
|
||
const result = await syncStateWithMintTask({proofsToSync, mintUrl: mint.mintUrl, proofState})
|
||
|
||
transactionStateUpdates.push(...result.transactionStateUpdates)
|
||
completedTransactionIds.push(...result.completedTransactionIds)
|
||
errorTransactionIds.push(...result.errorTransactionIds)
|
||
revertedTransactionIds.push(...result.revertedTransactionIds)
|
||
|
||
if (result.error) {
|
||
errors.push(result.error.message)
|
||
}
|
||
}
|
||
}
|
||
|
||
let totalSpent = 0
|
||
let message = ''
|
||
|
||
for (const update of transactionStateUpdates) {
|
||
if (update.spentByMintAmount) {
|
||
totalSpent += update.spentByMintAmount
|
||
}
|
||
}
|
||
|
||
if (proofState === 'PENDING') {
|
||
message = 'Pending proofs were synced with the mints.'
|
||
} else {
|
||
message = `Sync completed with ${errors.length} errors. Spent ecash with ${totalSpent} amount was cleaned.`
|
||
}
|
||
|
||
return {
|
||
taskFunction: SYNC_STATE_WITH_ALL_MINTS_TASK,
|
||
transactionStateUpdates,
|
||
completedTransactionIds,
|
||
errorTransactionIds,
|
||
revertedTransactionIds,
|
||
errors,
|
||
message,
|
||
}
|
||
}
|
||
|
||
const syncStateWithAllMintsQueueAwaitable = (
|
||
options: {proofState: ProofState},
|
||
): Promise<SyncStateTaskResult> =>
|
||
createQueueAwaitable<SyncStateTaskResult>({
|
||
taskFunction: 'syncSpendableStateTask',
|
||
timeoutMessage: 'Sync all mints state timed out',
|
||
task: () => syncStateWithAllMintsTask({proofState: options.proofState}),
|
||
})
|
||
|
||
const syncStateWithMintQueueAwaitable = (
|
||
options: {
|
||
proofsToSync: Proof[]
|
||
mintUrl: string
|
||
proofState: ProofState
|
||
},
|
||
): Promise<SyncStateTaskResult> => {
|
||
log.trace('[syncStateWithMintQueueAwaitable] start', {
|
||
mintUrl: options.mintUrl,
|
||
proofState: options.proofState,
|
||
proofsToSyncCount: options.proofsToSync.length,
|
||
})
|
||
|
||
return createQueueAwaitable<SyncStateTaskResult>({
|
||
taskFunction: SYNC_STATE_WITH_MINT_TASK,
|
||
prioritized: false,
|
||
timeoutMessage: 'Sync mint state timed out',
|
||
task: () => syncStateWithMintTask(options),
|
||
})
|
||
}
|
||
|
||
/*
|
||
* swapAllTask sends all proofs to pending and swaps them with the mint for standard amount preference
|
||
* This decreases the total number of proofs held by the wallet. Used to optimize exported backup size.
|
||
* Heavy, needs to run using the foreground service. May freeze the wallet.
|
||
*/
|
||
const swapAllTask = async function (): Promise<WalletTaskResult> {
|
||
log.trace('[swapAllTask] start')
|
||
|
||
if (mintsStore.mintCount === 0) {
|
||
return {
|
||
taskFunction: SWAP_ALL_TASK,
|
||
message: 'No mints to swap with.',
|
||
}
|
||
}
|
||
|
||
let initialProofsCount = 0
|
||
let finalProofsCount = 0
|
||
const errors: string[] = []
|
||
const maxBatchSize = MAX_SWAP_INPUT_SIZE
|
||
|
||
for (const mint of mintsStore.allMints) {
|
||
for (const unit of mint.units) {
|
||
const proofsToOptimize = proofsStore.getByMint(mint.mintUrl, {state: 'UNSPENT', unit, ascending: true})
|
||
|
||
if (proofsToOptimize.length === 0) {
|
||
continue
|
||
}
|
||
|
||
initialProofsCount += proofsToOptimize.length
|
||
const mintBalance = mint.balances
|
||
|
||
if (proofsToOptimize.length > maxBatchSize) {
|
||
let index = 0
|
||
for (let i = 0; i < proofsToOptimize.length; i += maxBatchSize) {
|
||
index++
|
||
const batch = proofsToOptimize.slice(i, i + maxBatchSize)
|
||
const batchAmount = CashuUtils.getProofsAmount(batch)
|
||
|
||
const sendResult = await sendTask(
|
||
mintBalance!,
|
||
batchAmount,
|
||
unit,
|
||
`Optimize ecash #${index}`,
|
||
batch,
|
||
)
|
||
|
||
const encodedTokenToReceive: string = sendResult.encodedTokenToSend
|
||
|
||
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
|
||
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
|
||
|
||
const receiveResult = await receiveBatchTask(
|
||
tokenToReceive,
|
||
tokenAmount,
|
||
tokenToReceive.memo as string,
|
||
encodedTokenToReceive,
|
||
)
|
||
|
||
await syncStateWithMintTask({proofsToSync: batch, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||
|
||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||
finalProofsCount += receiveResult.receivedProofsCount
|
||
}
|
||
|
||
if (receiveResult.error) {
|
||
errors.push(receiveResult.error.message)
|
||
}
|
||
}
|
||
} else {
|
||
const proofsAmount = CashuUtils.getProofsAmount(proofsToOptimize)
|
||
|
||
const sendResult = await sendTask(
|
||
mintBalance!,
|
||
proofsAmount,
|
||
unit,
|
||
`Optimize ecash`,
|
||
proofsToOptimize,
|
||
)
|
||
|
||
const encodedTokenToReceive: string = sendResult.encodedTokenToSend
|
||
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
|
||
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
|
||
|
||
const receiveResult = await receiveBatchTask(
|
||
tokenToReceive,
|
||
tokenAmount,
|
||
tokenToReceive.memo as string,
|
||
encodedTokenToReceive,
|
||
)
|
||
|
||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||
finalProofsCount += receiveResult.receivedProofsCount
|
||
}
|
||
|
||
if (receiveResult.error) {
|
||
errors.push(receiveResult.error.message)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
taskFunction: SWAP_ALL_TASK,
|
||
message: `Proofs optimization completed with ${errors.length} errors. Proofs number went from ${initialProofsCount} to ${finalProofsCount}`,
|
||
initialProofsCount,
|
||
finalProofsCount,
|
||
errors,
|
||
}
|
||
}
|
||
|
||
/*
|
||
* swapByDenominationTask swaps only proofs with the given denomination (amount) across all mints.
|
||
*/
|
||
const swapByDenominationTask = async function (denomination: number): Promise<WalletTaskResult> {
|
||
log.trace('[swapByDenominationTask] start', {denomination})
|
||
|
||
if (mintsStore.mintCount === 0) {
|
||
return {
|
||
taskFunction: SWAP_DENOMINATION_TASK,
|
||
message: 'No mints to swap with.',
|
||
}
|
||
}
|
||
|
||
let initialProofsCount = 0
|
||
let finalProofsCount = 0
|
||
const errors: string[] = []
|
||
const maxBatchSize = MAX_SWAP_INPUT_SIZE
|
||
|
||
for (const mint of mintsStore.allMints) {
|
||
for (const unit of mint.units) {
|
||
const allProofs = proofsStore.getByMint(mint.mintUrl, {state: 'UNSPENT', unit, ascending: true})
|
||
const proofsToOptimize = allProofs.filter(p => p.amount === denomination)
|
||
|
||
if (proofsToOptimize.length === 0) {
|
||
continue
|
||
}
|
||
|
||
initialProofsCount += proofsToOptimize.length
|
||
const mintBalance = mint.balances
|
||
|
||
if (proofsToOptimize.length > maxBatchSize) {
|
||
let index = 0
|
||
for (let i = 0; i < proofsToOptimize.length; i += maxBatchSize) {
|
||
index++
|
||
const batch = proofsToOptimize.slice(i, i + maxBatchSize)
|
||
const batchAmount = CashuUtils.getProofsAmount(batch)
|
||
|
||
const sendResult = await sendTask(
|
||
mintBalance!,
|
||
batchAmount,
|
||
unit,
|
||
`Optimize denomination ${denomination} #${index}`,
|
||
batch,
|
||
)
|
||
|
||
const encodedTokenToReceive: string = sendResult.encodedTokenToSend
|
||
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
|
||
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
|
||
|
||
const receiveResult = await receiveBatchTask(
|
||
tokenToReceive,
|
||
tokenAmount,
|
||
tokenToReceive.memo as string,
|
||
encodedTokenToReceive,
|
||
)
|
||
|
||
await syncStateWithMintTask({proofsToSync: batch, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||
|
||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||
finalProofsCount += receiveResult.receivedProofsCount
|
||
}
|
||
if (receiveResult.error) {
|
||
errors.push(receiveResult.error.message)
|
||
}
|
||
}
|
||
} else {
|
||
const proofsAmount = CashuUtils.getProofsAmount(proofsToOptimize)
|
||
|
||
const sendResult = await sendTask(
|
||
mintBalance!,
|
||
proofsAmount,
|
||
unit,
|
||
`Optimize denomination ${denomination}`,
|
||
proofsToOptimize,
|
||
)
|
||
|
||
const encodedTokenToReceive: string = sendResult.encodedTokenToSend
|
||
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
|
||
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
|
||
|
||
const receiveResult = await receiveBatchTask(
|
||
tokenToReceive,
|
||
tokenAmount,
|
||
tokenToReceive.memo as string,
|
||
encodedTokenToReceive,
|
||
)
|
||
|
||
await syncStateWithMintTask({proofsToSync: proofsToOptimize, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||
|
||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||
finalProofsCount += receiveResult.receivedProofsCount
|
||
}
|
||
if (receiveResult.error) {
|
||
errors.push(receiveResult.error.message)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
taskFunction: SWAP_DENOMINATION_TASK,
|
||
message: `Denomination ${denomination} optimization completed with ${errors.length} errors. Proofs went from ${initialProofsCount} to ${finalProofsCount}`,
|
||
initialProofsCount,
|
||
finalProofsCount,
|
||
errors,
|
||
}
|
||
}
|
||
|
||
const swapAllQueue = async function (): Promise<void> {
|
||
const now = new Date().getTime()
|
||
return SyncQueue.addPrioritizedTask(
|
||
`swapAllTask-${now}`,
|
||
async () => await swapAllTask(),
|
||
)
|
||
}
|
||
|
||
const swapByDenominationQueue = async function (denomination: number): Promise<void> {
|
||
const now = new Date().getTime()
|
||
return SyncQueue.addPrioritizedTask(
|
||
`swapDenominationTask-${denomination}-${now}`,
|
||
async () => await swapByDenominationTask(denomination),
|
||
)
|
||
}
|
||
|
||
export const SyncOperationService = {
|
||
syncStateWithAllMintsQueueAwaitable,
|
||
syncStateWithMintQueueAwaitable,
|
||
syncStateWithMintTask,
|
||
swapAllQueue,
|
||
swapByDenominationQueue,
|
||
}
|